Compare commits

..

107 Commits

Author SHA1 Message Date
J. Nick Koston
347501d895 wifi fixed vector 2025-10-12 19:39:55 -10:00
J. Nick Koston
4c00861760 add comments for bot 2025-10-11 17:35:31 -10:00
J. Nick Koston
2ff3e7fb2b add comments for bot 2025-10-11 17:34:51 -10:00
J. Nick Koston
b0c20d7adb [core] Optimize looping_components_ with FixedVector to save flash 2025-10-11 16:54:40 -10:00
Jonathan Swoboda
2cc5e24b38 [esp32] Change Arduino dev & latest to 3.3.2 (#11169) 2025-10-11 20:44:44 -04:00
J. Nick Koston
3afa73b449 [ci] Filter out components without tests from CI test jobs (#11134 followup) (#11178) 2025-10-11 18:27:18 -05:00
J. Nick Koston
dcf2697a2a Group component tests to reduce CI time (#11134) 2025-10-12 07:21:45 +13:00
J. Nick Koston
6a11700a6b [mdns] Restore mdns_txt_record() public API for external components (#11158) 2025-10-12 07:21:37 +13:00
J. Nick Koston
9bd9b043c8 [esp32_ble_tracker] Replace std::vector with StaticVector for listeners and clients (#11173) 2025-10-11 05:47:42 -10:00
J. Nick Koston
cb602c9b1a [esp32_ble] Partial revert of #10862 - Fix GATT client notifications (#11171) 2025-10-11 05:47:23 -10:00
dependabot[bot]
b54beb357a Bump github/codeql-action from 4.30.7 to 4.30.8 (#11163)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-10 13:46:27 -10:00
J. Nick Koston
6abc2efd96 [json] Fix PSRAM allocator dangling pointer crash (#11165) 2025-10-10 21:18:57 +00:00
J. Nick Koston
be51093a7e [ci][tests] Remove all redundant ESP32-C3 Arduino tests (#11154) 2025-10-10 16:02:18 +13:00
J. Nick Koston
52219c4dcc [datetime][ci][tests] Replace test.all.yaml with minimal platform cover (#11151) 2025-10-09 13:45:59 -10:00
J. Nick Koston
590cae13c0 [ci][tests] Remove redundant ESP32-C3 Arduino tests for non-variant-specific components (#11152) 2025-10-09 18:41:50 -05:00
J. Nick Koston
e15429b0f5 [opentherm][ci][tests] Remove redundant ESP32 Arduino tests and simplify conditionals (#11149) 2025-10-09 23:38:34 +00:00
J. Nick Koston
b5cc668a45 [ci][logger][tests] Remove redundant ESP32 Arduino test files (#11144) 2025-10-09 13:30:05 -10:00
Jonathan Swoboda
a1b0ae78e0 [stale] Increase operations-per-run (#11135)
CI passed, stuck on status
2025-10-09 19:10:09 -04:00
J. Nick Koston
fcc8a809e6 [ci][debug][tests] Remove redundant ESP32 variant Arduino test files (#11146) 2025-10-09 16:57:40 -05:00
J. Nick Koston
48474c0f8c [ci][time][tests] Remove redundant ESP32 Arduino test files (#11147) 2025-10-09 16:57:11 -05:00
J. Nick Koston
9f9c95dd09 [network][ci][tests] Remove redundant ESP32 Arduino test files (#11148) 2025-10-09 16:56:53 -05:00
J. Nick Koston
a74fcbc8b6 [esp32_ble_beacon, esp32_ble_tracker] Remove unused Arduino includes and redundant tests (#11140) 2025-10-09 11:42:25 -10:00
J. Nick Koston
c8b898f9c5 [ci][mdns][tests] Remove redundant ESP32 Arduino test files (#11143) 2025-10-09 11:40:47 -10:00
J. Nick Koston
81bf2688b4 [esp32] Update migration warning for Arduino-as-IDF-component transition (#11142) 2025-10-09 11:36:31 -10:00
Jonathan Swoboda
87d2c9868f [esp32] Update IDF 5.5 and Arduino 3.3 to use 55.03.31-1 (#11120) 2025-10-09 21:27:36 +00:00
J. Nick Koston
5ca407e27c [mdns] Store TXT record values in flash to reduce heap usage (#11114) 2025-10-10 09:01:58 +13:00
dependabot[bot]
5bbc2ab482 Bump pyupgrade from 3.20.0 to 3.21.0 (#11139)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-09 19:40:40 +00:00
J. Nick Koston
309e8b4c92 [ci][improv_serial][tests] Remove redundant ESP32 Arduino test files (#11138) 2025-10-09 19:17:04 +00:00
Jesse Hills
eee2987c99 Merge branch 'beta' into dev 2025-10-10 07:53:53 +13:00
J. Nick Koston
061e55f8c5 [ci][ethernet][tests] Remove redundant Arduino tests for ethernet PHYs (#11137) 2025-10-09 08:45:45 -10:00
Jesse Hills
9ad462d8c6 Merge pull request #11115 from esphome/bump-2025.10.0b1
2025.10.0b1
2025-10-10 07:28:02 +13:00
J. Nick Koston
56334b7832 [ci][tests] Remove redundant ESP32 Arduino test files (#11136) 2025-10-10 07:26:41 +13:00
J. Nick Koston
a4b7e0c700 [canbus][mcp23xxx_base] Mark virtual methods as pure virtual to fix linker errors (#11133) 2025-10-09 07:41:49 -10:00
Jeff Brown
84ad7ee0e4 [esp32] Accept more framework URL schemes as sources (#11125) 2025-10-09 13:10:48 -04:00
dependabot[bot]
d006008539 Bump esphome-dashboard from 20250904.0 to 20251009.0 (#11123)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-09 09:26:38 -04:00
J. Nick Koston
f1af9d978c [ci] Reduce component test group size to 10 to prevent runner disk exhaustion (#11122) 2025-10-09 10:36:13 +13:00
J. Nick Koston
6bb1e4c9c0 [ci] Reduce component test group size to 10 to prevent runner disk exhaustion (#11122) 2025-10-09 10:35:52 +13:00
J. Nick Koston
785df05631 [ci] Reduce component test group size to prevent runner disk exhaustion (#11121) 2025-10-09 07:53:49 +13:00
J. Nick Koston
82bdb08884 [ci] Reduce component test group size to prevent runner disk exhaustion (#11121) 2025-10-08 14:24:26 -04:00
Jesse Hills
b709ff84c3 Bump version to 2025.11.0-dev 2025-10-08 21:14:45 +13:00
Jesse Hills
93266ad08f Bump version to 2025.10.0b1 2025-10-08 21:14:44 +13:00
Jesse Hills
2fac813f18 [epaper_spi] New epaper component (#10462)
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
Co-authored-by: Tudor Sandu <tm.sandu@gmail.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-10-08 21:11:06 +13:00
Jesse Hills
a62c7a03dd [api] Add support for getting action responses from home-assistant (#10948)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-10-08 20:47:36 +13:00
J. Nick Koston
ec63247ae0 [mdns] Fix delete/malloc bug and store string constants in flash (#11105) 2025-10-08 04:19:29 +00:00
carlessolegrau
0fe6e7169c [modbus_controller] courtesy response (#10027)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-10-08 16:40:49 +13:00
dependabot[bot]
a0f4de1bfb Bump aioesphomeapi from 41.12.0 to 41.13.0 (#11113)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-08 03:35:17 +00:00
Jesse Hills
a541549d23 [core] Fix dynamic auto load priority (#11112) 2025-10-07 17:05:09 -10:00
Jonathan Swoboda
b74715fe14 [esp32] Fix issue when framework source is set (#11106) 2025-10-07 22:55:59 -04:00
J. Nick Koston
5aff20a624 [api] Add message size limits to prevent memory exhaustion (#10936) 2025-10-08 00:47:31 +00:00
Kevin Ahrendt
7682b4e9a3 [audio] Update esp-audio-libs 2.0.1 to use new FLAC decoder (#10974)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-10-07 23:35:42 +00:00
Jonathan Swoboda
6eabf709c6 [esp32] Hide build warnings (#11102) 2025-10-08 10:27:56 +13:00
J. Nick Koston
6209d4b493 [api] Optimize frame helpers to eliminate double-move overhead (#11092) 2025-10-08 10:16:44 +13:00
J. Nick Koston
f10c361454 [esp32_ble] Refactor ESPBTUUID comparison with direct returns and memcmp (#11074) 2025-10-08 09:34:08 +13:00
J. Nick Koston
27456c1370 [esp32_ble] Refactor ESPBTUUID::from_raw to use parse_hex helpers (#11073) 2025-10-08 09:32:47 +13:00
J. Nick Koston
1aeefbe547 [light] Reduce flash usage by eliminating duplicate validation code (#11030)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-08 09:23:57 +13:00
dependabot[bot]
3f3bce7ef4 Bump ruff from 0.13.3 to 0.14.0 (#11107)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-10-07 19:27:21 +00:00
Jesse Hills
0acc58d5a1 [core] Update helpers for new auto load functionality (#11097) 2025-10-07 14:24:28 -05:00
dependabot[bot]
0b4ef0fea2 Bump github/codeql-action from 3.30.6 to 4.30.7 (#11109)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-07 14:22:36 -05:00
dependabot[bot]
a067bdb769 Bump aioesphomeapi from 41.11.0 to 41.12.0 (#11108)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-07 14:21:35 -05:00
Jesse Hills
301e7a7ac5 [const] Move CONF_CAPTURE_RESPONSE to const.py (#11096) 2025-10-07 03:15:56 -05:00
J. Nick Koston
ac566b7fd6 [clang-tidy] Include sdkconfig.defaults in hash calculation (#11091) 2025-10-06 22:06:30 +00:00
J. Nick Koston
fddb8b35f2 [esp32] Fix clang-tidy error for Arduino watchdog function declarations (#11085) 2025-10-06 16:54:13 -04:00
Jesse Hills
27e1095cd7 [core] Allow AUTO_LOAD to receive the component config to determine if it should load other components (#10961)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-10-06 15:36:27 -05:00
Mariusz Kryński
fa4541a4f3 [mcp2515] setup filters (#10486) 2025-10-06 16:10:46 -04:00
J. Nick Koston
24dcc1843e [time] Fix clang-tidy sign comparison errors (#11080) 2025-10-06 14:34:40 -05:00
J. Nick Koston
f670d775ac [api] Fix clang-tidy sign comparison error (#11081) 2025-10-06 19:26:58 +00:00
J. Nick Koston
59a31adac2 [waveshare_epaper] Fix clang-tidy sign comparison errors (#11079) 2025-10-07 08:14:28 +13:00
dependabot[bot]
a3c0acc7c9 Bump pylint from 3.3.8 to 3.3.9 (#11082)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-06 14:13:48 -05:00
dependabot[bot]
ad2c5b96a9 Bump zeroconf from 0.147.2 to 0.148.0 (#11083)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-06 19:11:04 +00:00
J. Nick Koston
9adc3bd943 [veml7700] Fix clang-tidy sign comparison errors (#11078) 2025-10-06 18:24:05 +00:00
J. Nick Koston
ad296a7d74 [uponor_smatrix] Fix clang-tidy sign comparison errors (#11076) 2025-10-06 18:20:56 +00:00
J. Nick Koston
fdd422c42a [tormatic] Fix clang-tidy sign comparison error (#11075) 2025-10-06 13:14:52 -05:00
J. Nick Koston
3d82301c3d [graph] Fix clang-tidy sign comparison error (#11051) 2025-10-06 13:28:43 -04:00
J. Nick Koston
2fa49be17d [haier] Fix clang-tidy sign comparison error (#11053) 2025-10-06 13:27:15 -04:00
J. Nick Koston
75867842ea [rtttl] Fix clang-tidy sign comparison error (#11065) 2025-10-06 13:26:59 -04:00
Stephen Kent
cba85c0925 [remote_receiver] Add signal demodulation support on ESP32 (#8711)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-10-06 13:24:58 -04:00
J. Nick Koston
42d1269aaf [esp32_ble_server] Use early returns in is_created() and is_failed() methods (#11072) 2025-10-06 17:16:25 +00:00
J. Nick Koston
f4df17673b [esp32_ble_server] Refactor property setters to reduce code duplication (#11071) 2025-10-06 12:15:06 -05:00
J. Nick Koston
e340397b41 [mipi_spi] Fix clang-tidy sign comparison errors (#11070) 2025-10-06 13:10:41 -04:00
J. Nick Koston
abeadc7830 [remote_base] Fix clang-tidy sign comparison error (#11064) 2025-10-06 11:49:50 -05:00
Beormund
8d4b347e5c [lm75b] Add LM75B temperature sensor component (#10534) 2025-10-06 12:36:33 -04:00
J. Nick Koston
a7f556c25f [esp32_ble] Fix clang-tidy sign comparison error (#11048) 2025-10-06 11:28:41 -05:00
J. Nick Koston
3f4250fcd7 [st7567_i2c] Fix clang-tidy sign comparison warning (#11067) 2025-10-06 12:27:34 -04:00
J. Nick Koston
b532e04ae4 [st7789v] Fix clang-tidy sign comparison errors (#11068)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-06 16:24:52 +00:00
Jesse Hills
697cab45dd [json] Add parse_json overload for const char * (#11039)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-10-06 11:19:31 -05:00
J. Nick Koston
a88182c8e3 [statsd] Fix clang-tidy sign comparison error (#11069) 2025-10-06 12:12:39 -04:00
J. Nick Koston
8cfb6578d1 [graphical_display_menu] Fix clang-tidy sign comparison errors (#11052) 2025-10-06 12:10:54 -04:00
J. Nick Koston
eb16d322cd [audio, i2s_audio] Fix clang-tidy sign comparison errors (#11044) 2025-10-06 12:07:44 -04:00
J. Nick Koston
22e06ba063 [matrix_keypad] Fix clang-tidy sign comparison error (#11059)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-10-06 11:53:24 -04:00
Mort
7147479f90 [qmc5883l] Added drdy_pin option to allow it to run max rate (#10901)
Co-authored-by: Lamer Mortification <lamer_mortification@yahoo.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-10-06 11:48:05 -04:00
J. Nick Koston
e55df1babc [key_collector] Fix clang-tidy sign comparison errors (#11056)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-10-06 11:34:18 -04:00
J. Nick Koston
4c8fc5f4e6 [pid] Fix clang-tidy sign comparison error (#11063) 2025-10-06 11:20:59 -04:00
J. Nick Koston
646508006c [ili9xxx] Fix clang-tidy sign comparison errors (#11054) 2025-10-06 11:02:54 -04:00
J. Nick Koston
9384f0683b [kamstrup_kmp] Fix clang-tidy sign comparison errors (#11055) 2025-10-06 11:02:02 -04:00
J. Nick Koston
5e7f5bf890 [ltr501] Fix clang-tidy sign comparison errors (#11057) 2025-10-06 10:59:36 -04:00
J. Nick Koston
2a8796437d [ltr_als_ps] Fix clang-tidy sign comparison errors (#11058) 2025-10-06 10:59:02 -04:00
J. Nick Koston
1635767aa2 [max7219digit] Fix clang-tidy sign comparison error (#11060) 2025-10-06 10:56:44 -04:00
J. Nick Koston
192856e8d1 [nau7802] Fix clang-tidy sign comparison errors (#11062) 2025-10-06 09:52:47 -05:00
J. Nick Koston
71be5a5f65 [mixer] Fix clang-tidy sign comparison errors (#11061) 2025-10-06 14:48:39 +00:00
J. Nick Koston
f86b83cda5 [fingerprint_grow] Fix clang-tidy sign comparison error (#11050) 2025-10-06 10:15:56 -04:00
J. Nick Koston
74c055745f [esp32_can] Fix clang-tidy sign comparison error (#11049) 2025-10-06 10:14:24 -04:00
J. Nick Koston
3edcdc7d80 [es7210] Fix clang-tidy sign comparison errors (#11047) 2025-10-06 10:13:12 -04:00
J. Nick Koston
94fea68e3e [daikin_arc] Fix clang-tidy sign comparison errors (#11046) 2025-10-06 10:12:40 -04:00
J. Nick Koston
6880f9fc5c [cm1106] Fix clang-tidy sign comparison error (#11045) 2025-10-06 10:09:10 -04:00
J. Nick Koston
26ebac8cb8 [bl0906, bl0942] Fix clang-tidy sign comparison errors (#11043)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-06 10:08:33 -04:00
J. Nick Koston
5cf0046601 [animation] Fix clang-tidy sign comparison errors (#11042) 2025-10-06 10:07:44 -04:00
J. Nick Koston
c68017ddb4 [online_image] Fix clang-tidy sign comparison errors (#11041) 2025-10-06 10:07:04 -04:00
3027 changed files with 11563 additions and 11635 deletions

View File

@@ -186,6 +186,11 @@ This document provides essential context for AI models interacting with this pro
└── components/[component]/ # Component-specific tests └── components/[component]/ # Component-specific tests
``` ```
Run them using `script/test_build_components`. Use `-c <component>` to test specific components and `-t <target>` for specific platforms. Run them using `script/test_build_components`. Use `-c <component>` to test specific components and `-t <target>` for specific platforms.
* **Testing All Components Together:** To verify that all components can be tested together without ID conflicts or configuration issues, use:
```bash
./script/test_component_grouping.py -e config --all
```
This tests all components in a single build to catch conflicts that might not appear when testing components individually. Use `-e config` for fast configuration validation, or `-e compile` for full compilation testing.
* **Debugging and Troubleshooting:** * **Debugging and Troubleshooting:**
* **Debug Tools:** * **Debug Tools:**
- `esphome config <file>.yaml` to validate configuration. - `esphome config <file>.yaml` to validate configuration.

View File

@@ -1 +1 @@
499db61c1aa55b98b6629df603a56a1ba7aff5a9a7c781a5c1552a9dcd186c08 049d60eed541730efaa4c0dc5d337b4287bf29b6daa350b5dfc1f23915f1c52f

View File

@@ -6,6 +6,7 @@ on:
- ".clang-tidy" - ".clang-tidy"
- "platformio.ini" - "platformio.ini"
- "requirements_dev.txt" - "requirements_dev.txt"
- "sdkconfig.defaults"
- ".clang-tidy.hash" - ".clang-tidy.hash"
- "script/clang_tidy_hash.py" - "script/clang_tidy_hash.py"
- ".github/workflows/ci-clang-tidy-hash.yml" - ".github/workflows/ci-clang-tidy-hash.yml"

View File

@@ -177,6 +177,7 @@ jobs:
clang-tidy: ${{ steps.determine.outputs.clang-tidy }} clang-tidy: ${{ steps.determine.outputs.clang-tidy }}
python-linters: ${{ steps.determine.outputs.python-linters }} python-linters: ${{ steps.determine.outputs.python-linters }}
changed-components: ${{ steps.determine.outputs.changed-components }} changed-components: ${{ steps.determine.outputs.changed-components }}
changed-components-with-tests: ${{ steps.determine.outputs.changed-components-with-tests }}
component-test-count: ${{ steps.determine.outputs.component-test-count }} component-test-count: ${{ steps.determine.outputs.component-test-count }}
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
@@ -204,6 +205,7 @@ jobs:
echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT
echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT
echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT
echo "changed-components-with-tests=$(echo "$output" | jq -c '.changed_components_with_tests')" >> $GITHUB_OUTPUT
echo "component-test-count=$(echo "$output" | jq -r '.component_test_count')" >> $GITHUB_OUTPUT echo "component-test-count=$(echo "$output" | jq -r '.component_test_count')" >> $GITHUB_OUTPUT
integration-tests: integration-tests:
@@ -367,12 +369,13 @@ jobs:
fail-fast: false fail-fast: false
max-parallel: 2 max-parallel: 2
matrix: matrix:
file: ${{ fromJson(needs.determine-jobs.outputs.changed-components) }} file: ${{ fromJson(needs.determine-jobs.outputs.changed-components-with-tests) }}
steps: steps:
- name: Install dependencies - name: Cache apt packages
run: | uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.5.3
sudo apt-get update with:
sudo apt-get install libsdl2-dev packages: libsdl2-dev
version: 1.0
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
@@ -381,17 +384,17 @@ jobs:
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }} cache-key: ${{ needs.common.outputs.cache-key }}
- name: test_build_components -e config -c ${{ matrix.file }} - name: Validate config for ${{ matrix.file }}
run: | run: |
. venv/bin/activate . venv/bin/activate
./script/test_build_components -e config -c ${{ matrix.file }} python3 script/test_build_components.py -e config -c ${{ matrix.file }}
- name: test_build_components -e compile -c ${{ matrix.file }} - name: Compile config for ${{ matrix.file }}
run: | run: |
. venv/bin/activate . venv/bin/activate
./script/test_build_components -e compile -c ${{ matrix.file }} python3 script/test_build_components.py -e compile -c ${{ matrix.file }}
test-build-components-splitter: test-build-components-splitter:
name: Split components for testing into 20 groups maximum name: Split components for intelligent grouping (40 weighted per batch)
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
needs: needs:
- common - common
@@ -402,14 +405,26 @@ jobs:
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Split components into 20 groups - name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Split components intelligently based on bus configurations
id: split id: split
run: | run: |
components=$(echo '${{ needs.determine-jobs.outputs.changed-components }}' | jq -c '.[]' | shuf | jq -s -c '[_nwise(20) | join(" ")]') . venv/bin/activate
echo "components=$components" >> $GITHUB_OUTPUT
# Use intelligent splitter that groups components with same bus configs
components='${{ needs.determine-jobs.outputs.changed-components-with-tests }}'
echo "Splitting components intelligently..."
output=$(python3 script/split_components_for_ci.py --components "$components" --batch-size 40 --output github)
echo "$output" >> $GITHUB_OUTPUT
test-build-components-split: test-build-components-split:
name: Test split components name: Test components batch (${{ matrix.components }})
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
needs: needs:
- common - common
@@ -418,17 +433,23 @@ jobs:
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) >= 100 if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) >= 100
strategy: strategy:
fail-fast: false fail-fast: false
max-parallel: 4 max-parallel: 5
matrix: matrix:
components: ${{ fromJson(needs.test-build-components-splitter.outputs.matrix) }} components: ${{ fromJson(needs.test-build-components-splitter.outputs.matrix) }}
steps: steps:
- name: Show disk space
run: |
echo "Available disk space:"
df -h
- name: List components - name: List components
run: echo ${{ matrix.components }} run: echo ${{ matrix.components }}
- name: Install dependencies - name: Cache apt packages
run: | uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.5.3
sudo apt-get update with:
sudo apt-get install libsdl2-dev packages: libsdl2-dev
version: 1.0
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
@@ -437,20 +458,37 @@ jobs:
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }} cache-key: ${{ needs.common.outputs.cache-key }}
- name: Validate config - name: Validate and compile components with intelligent grouping
run: | run: |
. venv/bin/activate . venv/bin/activate
for component in ${{ matrix.components }}; do # Use /mnt for build files (70GB available vs ~29GB on /)
./script/test_build_components -e config -c $component # Bind mount PlatformIO directory to /mnt (tools, packages, build cache all go there)
done sudo mkdir -p /mnt/platformio
- name: Compile config sudo chown $USER:$USER /mnt/platformio
run: | mkdir -p ~/.platformio
. venv/bin/activate sudo mount --bind /mnt/platformio ~/.platformio
mkdir build_cache
export PLATFORMIO_BUILD_CACHE_DIR=$PWD/build_cache # Bind mount test build directory to /mnt
for component in ${{ matrix.components }}; do sudo mkdir -p /mnt/test_build_components_build
./script/test_build_components -e compile -c $component sudo chown $USER:$USER /mnt/test_build_components_build
done mkdir -p tests/test_build_components/build
sudo mount --bind /mnt/test_build_components_build tests/test_build_components/build
# Convert space-separated components to comma-separated for Python script
components_csv=$(echo "${{ matrix.components }}" | tr ' ' ',')
echo "Testing components: $components_csv"
echo ""
# Run config validation with grouping
python3 script/test_build_components.py -e config -c "$components_csv" -f
echo ""
echo "Config validation passed! Starting compilation..."
echo ""
# Run compilation with grouping
python3 script/test_build_components.py -e compile -c "$components_csv" -f
pre-commit-ci-lite: pre-commit-ci-lite:
name: pre-commit.ci lite name: pre-commit.ci lite

View File

@@ -58,7 +58,7 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.30.6 uses: github/codeql-action/init@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }} build-mode: ${{ matrix.build-mode }}
@@ -86,6 +86,6 @@ jobs:
exit 1 exit 1
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.30.6 uses: github/codeql-action/analyze@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8
with: with:
category: "/language:${{matrix.language}}" category: "/language:${{matrix.language}}"

View File

@@ -23,7 +23,7 @@ jobs:
with: with:
debug-only: ${{ github.ref != 'refs/heads/dev' }} # Dry-run when not run on dev branch debug-only: ${{ github.ref != 'refs/heads/dev' }} # Dry-run when not run on dev branch
remove-stale-when-updated: true remove-stale-when-updated: true
operations-per-run: 150 operations-per-run: 400
# The 90 day stale policy for PRs # The 90 day stale policy for PRs
# - PRs # - PRs

View File

@@ -11,7 +11,7 @@ ci:
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: v0.13.3 rev: v0.14.0
hooks: hooks:
# Run the linter. # Run the linter.
- id: ruff - id: ruff

View File

@@ -139,6 +139,7 @@ esphome/components/ens160_base/* @latonita @vincentscode
esphome/components/ens160_i2c/* @latonita esphome/components/ens160_i2c/* @latonita
esphome/components/ens160_spi/* @latonita esphome/components/ens160_spi/* @latonita
esphome/components/ens210/* @itn3rd77 esphome/components/ens210/* @itn3rd77
esphome/components/epaper_spi/* @esphome/core
esphome/components/es7210/* @kahrendt esphome/components/es7210/* @kahrendt
esphome/components/es7243e/* @kbx81 esphome/components/es7243e/* @kbx81
esphome/components/es8156/* @kbx81 esphome/components/es8156/* @kbx81
@@ -256,6 +257,7 @@ esphome/components/libretiny_pwm/* @kuba2k2
esphome/components/light/* @esphome/core esphome/components/light/* @esphome/core
esphome/components/lightwaverf/* @max246 esphome/components/lightwaverf/* @max246
esphome/components/lilygo_t5_47/touchscreen/* @jesserockz esphome/components/lilygo_t5_47/touchscreen/* @jesserockz
esphome/components/lm75b/* @beormund
esphome/components/ln882x/* @lamauny esphome/components/ln882x/* @lamauny
esphome/components/lock/* @esphome/core esphome/components/lock/* @esphome/core
esphome/components/logger/* @esphome/core esphome/components/logger/* @esphome/core
@@ -428,6 +430,7 @@ esphome/components/speaker/media_player/* @kahrendt @synesthesiam
esphome/components/spi/* @clydebarrow @esphome/core esphome/components/spi/* @clydebarrow @esphome/core
esphome/components/spi_device/* @clydebarrow esphome/components/spi_device/* @clydebarrow
esphome/components/spi_led_strip/* @clydebarrow esphome/components/spi_led_strip/* @clydebarrow
esphome/components/split_buffer/* @jesserockz
esphome/components/sprinkler/* @kbx81 esphome/components/sprinkler/* @kbx81
esphome/components/sps30/* @martgras esphome/components/sps30/* @martgras
esphome/components/ssd1322_base/* @kbx81 esphome/components/ssd1322_base/* @kbx81

View File

@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version # could be handy for archiving the generated documentation or if some version
# control system is used. # control system is used.
PROJECT_NUMBER = 2025.10.0-dev PROJECT_NUMBER = 2025.11.0-dev
# Using the PROJECT_BRIEF tag one can provide an optional one line description # Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a # for a project that appears at the top of each page and should give viewer a

View File

@@ -1002,6 +1002,12 @@ def parse_args(argv):
action="append", action="append",
default=[], default=[],
) )
options_parser.add_argument(
"--testing-mode",
help="Enable testing mode (disables validation checks for grouped component testing)",
action="store_true",
default=False,
)
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description=f"ESPHome {const.__version__}", parents=[options_parser] description=f"ESPHome {const.__version__}", parents=[options_parser]
@@ -1260,6 +1266,7 @@ def run_esphome(argv):
args = parse_args(argv) args = parse_args(argv)
CORE.dashboard = args.dashboard CORE.dashboard = args.dashboard
CORE.testing_mode = args.testing_mode
# Create address cache from command-line arguments # Create address cache from command-line arguments
CORE.address_cache = AddressCache.from_cli_args( CORE.address_cache = AddressCache.from_cli_args(

View File

@@ -26,12 +26,12 @@ uint32_t Animation::get_animation_frame_count() const { return this->animation_f
int Animation::get_current_frame() const { return this->current_frame_; } int Animation::get_current_frame() const { return this->current_frame_; }
void Animation::next_frame() { void Animation::next_frame() {
this->current_frame_++; this->current_frame_++;
if (loop_count_ && this->current_frame_ == loop_end_frame_ && if (loop_count_ && static_cast<uint32_t>(this->current_frame_) == loop_end_frame_ &&
(this->loop_current_iteration_ < loop_count_ || loop_count_ < 0)) { (this->loop_current_iteration_ < loop_count_ || loop_count_ < 0)) {
this->current_frame_ = loop_start_frame_; this->current_frame_ = loop_start_frame_;
this->loop_current_iteration_++; this->loop_current_iteration_++;
} }
if (this->current_frame_ >= animation_frame_count_) { if (static_cast<uint32_t>(this->current_frame_) >= animation_frame_count_) {
this->loop_current_iteration_ = 1; this->loop_current_iteration_ = 1;
this->current_frame_ = 0; this->current_frame_ = 0;
} }

View File

@@ -9,6 +9,7 @@ import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_ACTION, CONF_ACTION,
CONF_ACTIONS, CONF_ACTIONS,
CONF_CAPTURE_RESPONSE,
CONF_DATA, CONF_DATA,
CONF_DATA_TEMPLATE, CONF_DATA_TEMPLATE,
CONF_EVENT, CONF_EVENT,
@@ -17,30 +18,50 @@ from esphome.const import (
CONF_MAX_CONNECTIONS, CONF_MAX_CONNECTIONS,
CONF_ON_CLIENT_CONNECTED, CONF_ON_CLIENT_CONNECTED,
CONF_ON_CLIENT_DISCONNECTED, CONF_ON_CLIENT_DISCONNECTED,
CONF_ON_ERROR,
CONF_ON_SUCCESS,
CONF_PASSWORD, CONF_PASSWORD,
CONF_PORT, CONF_PORT,
CONF_REBOOT_TIMEOUT, CONF_REBOOT_TIMEOUT,
CONF_RESPONSE_TEMPLATE,
CONF_SERVICE, CONF_SERVICE,
CONF_SERVICES, CONF_SERVICES,
CONF_TAG, CONF_TAG,
CONF_TRIGGER_ID, CONF_TRIGGER_ID,
CONF_VARIABLES, CONF_VARIABLES,
) )
from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
from esphome.cpp_generator import TemplateArgsType
from esphome.types import ConfigType from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DOMAIN = "api" DOMAIN = "api"
DEPENDENCIES = ["network"] DEPENDENCIES = ["network"]
AUTO_LOAD = ["socket"]
CODEOWNERS = ["@esphome/core"] CODEOWNERS = ["@esphome/core"]
def AUTO_LOAD(config: ConfigType) -> list[str]:
"""Conditionally auto-load json only when capture_response is used."""
base = ["socket"]
# Check if any homeassistant.action/homeassistant.service has capture_response: true
# This flag is set during config validation in _validate_response_config
if not config or CORE.data.get(DOMAIN, {}).get(CONF_CAPTURE_RESPONSE, False):
return base + ["json"]
return base
api_ns = cg.esphome_ns.namespace("api") api_ns = cg.esphome_ns.namespace("api")
APIServer = api_ns.class_("APIServer", cg.Component, cg.Controller) APIServer = api_ns.class_("APIServer", cg.Component, cg.Controller)
HomeAssistantServiceCallAction = api_ns.class_( HomeAssistantServiceCallAction = api_ns.class_(
"HomeAssistantServiceCallAction", automation.Action "HomeAssistantServiceCallAction", automation.Action
) )
ActionResponse = api_ns.class_("ActionResponse")
HomeAssistantActionResponseTrigger = api_ns.class_(
"HomeAssistantActionResponseTrigger", automation.Trigger
)
APIConnectedCondition = api_ns.class_("APIConnectedCondition", Condition) APIConnectedCondition = api_ns.class_("APIConnectedCondition", Condition)
UserServiceTrigger = api_ns.class_("UserServiceTrigger", automation.Trigger) UserServiceTrigger = api_ns.class_("UserServiceTrigger", automation.Trigger)
@@ -288,6 +309,29 @@ async def to_code(config):
KEY_VALUE_SCHEMA = cv.Schema({cv.string: cv.templatable(cv.string_strict)}) KEY_VALUE_SCHEMA = cv.Schema({cv.string: cv.templatable(cv.string_strict)})
def _validate_response_config(config: ConfigType) -> ConfigType:
# Validate dependencies:
# - response_template requires capture_response: true
# - capture_response: true requires on_success
if CONF_RESPONSE_TEMPLATE in config and not config[CONF_CAPTURE_RESPONSE]:
raise cv.Invalid(
f"`{CONF_RESPONSE_TEMPLATE}` requires `{CONF_CAPTURE_RESPONSE}: true` to be set.",
path=[CONF_RESPONSE_TEMPLATE],
)
if config[CONF_CAPTURE_RESPONSE] and CONF_ON_SUCCESS not in config:
raise cv.Invalid(
f"`{CONF_CAPTURE_RESPONSE}: true` requires `{CONF_ON_SUCCESS}` to be set.",
path=[CONF_CAPTURE_RESPONSE],
)
# Track if any action uses capture_response for AUTO_LOAD
if config[CONF_CAPTURE_RESPONSE]:
CORE.data.setdefault(DOMAIN, {})[CONF_CAPTURE_RESPONSE] = True
return config
HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All( HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
cv.Schema( cv.Schema(
{ {
@@ -303,10 +347,15 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
cv.Optional(CONF_VARIABLES, default={}): cv.Schema( cv.Optional(CONF_VARIABLES, default={}): cv.Schema(
{cv.string: cv.returning_lambda} {cv.string: cv.returning_lambda}
), ),
cv.Optional(CONF_RESPONSE_TEMPLATE): cv.templatable(cv.string),
cv.Optional(CONF_CAPTURE_RESPONSE, default=False): cv.boolean,
cv.Optional(CONF_ON_SUCCESS): automation.validate_automation(single=True),
cv.Optional(CONF_ON_ERROR): automation.validate_automation(single=True),
} }
), ),
cv.has_exactly_one_key(CONF_SERVICE, CONF_ACTION), cv.has_exactly_one_key(CONF_SERVICE, CONF_ACTION),
cv.rename_key(CONF_SERVICE, CONF_ACTION), cv.rename_key(CONF_SERVICE, CONF_ACTION),
_validate_response_config,
) )
@@ -320,7 +369,12 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
HomeAssistantServiceCallAction, HomeAssistantServiceCallAction,
HOMEASSISTANT_ACTION_ACTION_SCHEMA, HOMEASSISTANT_ACTION_ACTION_SCHEMA,
) )
async def homeassistant_service_to_code(config, action_id, template_arg, args): async def homeassistant_service_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
):
cg.add_define("USE_API_HOMEASSISTANT_SERVICES") cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
serv = await cg.get_variable(config[CONF_ID]) serv = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, serv, False) var = cg.new_Pvariable(action_id, template_arg, serv, False)
@@ -335,6 +389,40 @@ async def homeassistant_service_to_code(config, action_id, template_arg, args):
for key, value in config[CONF_VARIABLES].items(): for key, value in config[CONF_VARIABLES].items():
templ = await cg.templatable(value, args, None) templ = await cg.templatable(value, args, None)
cg.add(var.add_variable(key, templ)) cg.add(var.add_variable(key, templ))
if on_error := config.get(CONF_ON_ERROR):
cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES")
cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES_ERRORS")
cg.add(var.set_wants_status())
await automation.build_automation(
var.get_error_trigger(),
[(cg.std_string, "error"), *args],
on_error,
)
if on_success := config.get(CONF_ON_SUCCESS):
cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES")
cg.add(var.set_wants_status())
if config[CONF_CAPTURE_RESPONSE]:
cg.add(var.set_wants_response())
cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON")
await automation.build_automation(
var.get_success_trigger_with_response(),
[(cg.JsonObjectConst, "response"), *args],
on_success,
)
if response_template := config.get(CONF_RESPONSE_TEMPLATE):
templ = await cg.templatable(response_template, args, cg.std_string)
cg.add(var.set_response_template(templ))
else:
await automation.build_automation(
var.get_success_trigger(),
args,
on_success,
)
return var return var

View File

@@ -780,6 +780,22 @@ message HomeassistantActionRequest {
repeated HomeassistantServiceMap data_template = 3; repeated HomeassistantServiceMap data_template = 3;
repeated HomeassistantServiceMap variables = 4; repeated HomeassistantServiceMap variables = 4;
bool is_event = 5; bool is_event = 5;
uint32 call_id = 6 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES"];
bool wants_response = 7 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"];
string response_template = 8 [(no_zero_copy) = true, (field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"];
}
// Message sent by Home Assistant to ESPHome with service call response data
message HomeassistantActionResponse {
option (id) = 130;
option (source) = SOURCE_CLIENT;
option (no_delay) = true;
option (ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES";
uint32 call_id = 1; // Matches the call_id from HomeassistantActionRequest
bool success = 2; // Whether the service call succeeded
string error_message = 3; // Error message if success = false
bytes response_data = 4 [(pointer_to_buffer) = true, (field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"];
} }
// ==================== IMPORT HOME ASSISTANT STATES ==================== // ==================== IMPORT HOME ASSISTANT STATES ====================

View File

@@ -8,9 +8,9 @@
#endif #endif
#include <cerrno> #include <cerrno>
#include <cinttypes> #include <cinttypes>
#include <utility>
#include <functional> #include <functional>
#include <limits> #include <limits>
#include <utility>
#include "esphome/components/network/util.h" #include "esphome/components/network/util.h"
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/entity_base.h" #include "esphome/core/entity_base.h"
@@ -1549,6 +1549,20 @@ void APIConnection::execute_service(const ExecuteServiceRequest &msg) {
} }
} }
#endif #endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
void APIConnection::on_homeassistant_action_response(const HomeassistantActionResponse &msg) {
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
if (msg.response_data_len > 0) {
this->parent_->handle_action_response(msg.call_id, msg.success, msg.error_message, msg.response_data,
msg.response_data_len);
} else
#endif
{
this->parent_->handle_action_response(msg.call_id, msg.success, msg.error_message);
}
};
#endif
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) { bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) {
NoiseEncryptionSetKeyResponse resp; NoiseEncryptionSetKeyResponse resp;

View File

@@ -129,7 +129,10 @@ class APIConnection final : public APIServerConnection {
return; return;
this->send_message(call, HomeassistantActionRequest::MESSAGE_TYPE); this->send_message(call, HomeassistantActionRequest::MESSAGE_TYPE);
} }
#endif #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
void on_homeassistant_action_response(const HomeassistantActionResponse &msg) override;
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
#endif // USE_API_HOMEASSISTANT_SERVICES
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override; void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override; void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override;

View File

@@ -18,6 +18,17 @@ namespace esphome::api {
// uncomment to log raw packets // uncomment to log raw packets
//#define HELPER_LOG_PACKETS //#define HELPER_LOG_PACKETS
// Maximum message size limits to prevent OOM on constrained devices
// Handshake messages are limited to a small size for security
static constexpr uint16_t MAX_HANDSHAKE_SIZE = 128;
// Data message limits vary by platform based on available memory
#ifdef USE_ESP8266
static constexpr uint16_t MAX_MESSAGE_SIZE = 8192; // 8 KiB for ESP8266
#else
static constexpr uint16_t MAX_MESSAGE_SIZE = 32768; // 32 KiB for ESP32 and other platforms
#endif
// Forward declaration // Forward declaration
struct ClientInfo; struct ClientInfo;

View File

@@ -132,26 +132,16 @@ APIError APINoiseFrameHelper::loop() {
return APIFrameHelper::loop(); return APIFrameHelper::loop();
} }
/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter /** Read a packet into the rx_buf_.
* *
* @param frame: The struct to hold the frame information in. * @return APIError::OK if a full packet is in rx_buf_
* msg_start: points to the start of the payload - this pointer is only valid until the next
* try_receive_raw_ call
*
* @return 0 if a full packet is in rx_buf_
* @return -1 if error, check errno.
* *
* errno EWOULDBLOCK: Packet could not be read without blocking. Try again later. * errno EWOULDBLOCK: Packet could not be read without blocking. Try again later.
* errno ENOMEM: Not enough memory for reading packet. * errno ENOMEM: Not enough memory for reading packet.
* errno API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame. * errno API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame.
* errno API_ERROR_HANDSHAKE_PACKET_LEN: Packet too big for this phase. * errno API_ERROR_HANDSHAKE_PACKET_LEN: Packet too big for this phase.
*/ */
APIError APINoiseFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) { APIError APINoiseFrameHelper::try_read_frame_() {
if (frame == nullptr) {
HELPER_LOG("Bad argument for try_read_frame_");
return APIError::BAD_ARG;
}
// read header // read header
if (rx_header_buf_len_ < 3) { if (rx_header_buf_len_ < 3) {
// no header information yet // no header information yet
@@ -178,16 +168,17 @@ APIError APINoiseFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
// read body // read body
uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2]; uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2];
if (state_ != State::DATA && msg_size > 128) { // Check against size limits to prevent OOM: MAX_HANDSHAKE_SIZE for handshake, MAX_MESSAGE_SIZE for data
// for handshake message only permit up to 128 bytes uint16_t limit = (state_ == State::DATA) ? MAX_MESSAGE_SIZE : MAX_HANDSHAKE_SIZE;
if (msg_size > limit) {
state_ = State::FAILED; state_ = State::FAILED;
HELPER_LOG("Bad packet len for handshake: %d", msg_size); HELPER_LOG("Bad packet: message size %u exceeds maximum %u", msg_size, limit);
return APIError::BAD_HANDSHAKE_PACKET_LEN; return (state_ == State::DATA) ? APIError::BAD_DATA_PACKET : APIError::BAD_HANDSHAKE_PACKET_LEN;
} }
// reserve space for body // Reserve space for body
if (rx_buf_.size() != msg_size) { if (this->rx_buf_.size() != msg_size) {
rx_buf_.resize(msg_size); this->rx_buf_.resize(msg_size);
} }
if (rx_buf_len_ < msg_size) { if (rx_buf_len_ < msg_size) {
@@ -205,12 +196,12 @@ APIError APINoiseFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
} }
} }
LOG_PACKET_RECEIVED(rx_buf_); LOG_PACKET_RECEIVED(this->rx_buf_);
*frame = std::move(rx_buf_);
// consume msg // Clear state for next frame (rx_buf_ still contains data for caller)
rx_buf_ = {}; this->rx_buf_len_ = 0;
rx_buf_len_ = 0; this->rx_header_buf_len_ = 0;
rx_header_buf_len_ = 0;
return APIError::OK; return APIError::OK;
} }
@@ -232,18 +223,17 @@ APIError APINoiseFrameHelper::state_action_() {
} }
if (state_ == State::CLIENT_HELLO) { if (state_ == State::CLIENT_HELLO) {
// waiting for client hello // waiting for client hello
std::vector<uint8_t> frame; aerr = this->try_read_frame_();
aerr = try_read_frame_(&frame);
if (aerr != APIError::OK) { if (aerr != APIError::OK) {
return handle_handshake_frame_error_(aerr); return handle_handshake_frame_error_(aerr);
} }
// ignore contents, may be used in future for flags // ignore contents, may be used in future for flags
// Resize for: existing prologue + 2 size bytes + frame data // Resize for: existing prologue + 2 size bytes + frame data
size_t old_size = prologue_.size(); size_t old_size = this->prologue_.size();
prologue_.resize(old_size + 2 + frame.size()); this->prologue_.resize(old_size + 2 + this->rx_buf_.size());
prologue_[old_size] = (uint8_t) (frame.size() >> 8); this->prologue_[old_size] = (uint8_t) (this->rx_buf_.size() >> 8);
prologue_[old_size + 1] = (uint8_t) frame.size(); this->prologue_[old_size + 1] = (uint8_t) this->rx_buf_.size();
std::memcpy(prologue_.data() + old_size + 2, frame.data(), frame.size()); std::memcpy(this->prologue_.data() + old_size + 2, this->rx_buf_.data(), this->rx_buf_.size());
state_ = State::SERVER_HELLO; state_ = State::SERVER_HELLO;
} }
@@ -285,24 +275,23 @@ APIError APINoiseFrameHelper::state_action_() {
int action = noise_handshakestate_get_action(handshake_); int action = noise_handshakestate_get_action(handshake_);
if (action == NOISE_ACTION_READ_MESSAGE) { if (action == NOISE_ACTION_READ_MESSAGE) {
// waiting for handshake msg // waiting for handshake msg
std::vector<uint8_t> frame; aerr = this->try_read_frame_();
aerr = try_read_frame_(&frame);
if (aerr != APIError::OK) { if (aerr != APIError::OK) {
return handle_handshake_frame_error_(aerr); return handle_handshake_frame_error_(aerr);
} }
if (frame.empty()) { if (this->rx_buf_.empty()) {
send_explicit_handshake_reject_(LOG_STR("Empty handshake message")); send_explicit_handshake_reject_(LOG_STR("Empty handshake message"));
return APIError::BAD_HANDSHAKE_ERROR_BYTE; return APIError::BAD_HANDSHAKE_ERROR_BYTE;
} else if (frame[0] != 0x00) { } else if (this->rx_buf_[0] != 0x00) {
HELPER_LOG("Bad handshake error byte: %u", frame[0]); HELPER_LOG("Bad handshake error byte: %u", this->rx_buf_[0]);
send_explicit_handshake_reject_(LOG_STR("Bad handshake error byte")); send_explicit_handshake_reject_(LOG_STR("Bad handshake error byte"));
return APIError::BAD_HANDSHAKE_ERROR_BYTE; return APIError::BAD_HANDSHAKE_ERROR_BYTE;
} }
NoiseBuffer mbuf; NoiseBuffer mbuf;
noise_buffer_init(mbuf); noise_buffer_init(mbuf);
noise_buffer_set_input(mbuf, frame.data() + 1, frame.size() - 1); noise_buffer_set_input(mbuf, this->rx_buf_.data() + 1, this->rx_buf_.size() - 1);
err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr); err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr);
if (err != 0) { if (err != 0) {
// Special handling for MAC failure // Special handling for MAC failure
@@ -379,35 +368,33 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reaso
state_ = orig_state; state_ = orig_state;
} }
APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
int err; APIError aerr = this->state_action_();
APIError aerr;
aerr = state_action_();
if (aerr != APIError::OK) { if (aerr != APIError::OK) {
return aerr; return aerr;
} }
if (state_ != State::DATA) { if (this->state_ != State::DATA) {
return APIError::WOULD_BLOCK; return APIError::WOULD_BLOCK;
} }
std::vector<uint8_t> frame; aerr = this->try_read_frame_();
aerr = try_read_frame_(&frame);
if (aerr != APIError::OK) if (aerr != APIError::OK)
return aerr; return aerr;
NoiseBuffer mbuf; NoiseBuffer mbuf;
noise_buffer_init(mbuf); noise_buffer_init(mbuf);
noise_buffer_set_inout(mbuf, frame.data(), frame.size(), frame.size()); noise_buffer_set_inout(mbuf, this->rx_buf_.data(), this->rx_buf_.size(), this->rx_buf_.size());
err = noise_cipherstate_decrypt(recv_cipher_, &mbuf); int err = noise_cipherstate_decrypt(this->recv_cipher_, &mbuf);
APIError decrypt_err = APIError decrypt_err =
handle_noise_error_(err, LOG_STR("noise_cipherstate_decrypt"), APIError::CIPHERSTATE_DECRYPT_FAILED); handle_noise_error_(err, LOG_STR("noise_cipherstate_decrypt"), APIError::CIPHERSTATE_DECRYPT_FAILED);
if (decrypt_err != APIError::OK) if (decrypt_err != APIError::OK) {
return decrypt_err; return decrypt_err;
}
uint16_t msg_size = mbuf.size; uint16_t msg_size = mbuf.size;
uint8_t *msg_data = frame.data(); uint8_t *msg_data = this->rx_buf_.data();
if (msg_size < 4) { if (msg_size < 4) {
state_ = State::FAILED; this->state_ = State::FAILED;
HELPER_LOG("Bad data packet: size %d too short", msg_size); HELPER_LOG("Bad data packet: size %d too short", msg_size);
return APIError::BAD_DATA_PACKET; return APIError::BAD_DATA_PACKET;
} }
@@ -415,12 +402,12 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
uint16_t type = (((uint16_t) msg_data[0]) << 8) | msg_data[1]; uint16_t type = (((uint16_t) msg_data[0]) << 8) | msg_data[1];
uint16_t data_len = (((uint16_t) msg_data[2]) << 8) | msg_data[3]; uint16_t data_len = (((uint16_t) msg_data[2]) << 8) | msg_data[3];
if (data_len > msg_size - 4) { if (data_len > msg_size - 4) {
state_ = State::FAILED; this->state_ = State::FAILED;
HELPER_LOG("Bad data packet: data_len %u greater than msg_size %u", data_len, msg_size); HELPER_LOG("Bad data packet: data_len %u greater than msg_size %u", data_len, msg_size);
return APIError::BAD_DATA_PACKET; return APIError::BAD_DATA_PACKET;
} }
buffer->container = std::move(frame); buffer->container = std::move(this->rx_buf_);
buffer->data_offset = 4; buffer->data_offset = 4;
buffer->data_len = data_len; buffer->data_len = data_len;
buffer->type = type; buffer->type = type;

View File

@@ -28,7 +28,7 @@ class APINoiseFrameHelper final : public APIFrameHelper {
protected: protected:
APIError state_action_(); APIError state_action_();
APIError try_read_frame_(std::vector<uint8_t> *frame); APIError try_read_frame_();
APIError write_frame_(const uint8_t *data, uint16_t len); APIError write_frame_(const uint8_t *data, uint16_t len);
APIError init_handshake_(); APIError init_handshake_();
APIError check_handshake_finished_(); APIError check_handshake_finished_();

View File

@@ -47,21 +47,13 @@ APIError APIPlaintextFrameHelper::loop() {
return APIFrameHelper::loop(); return APIFrameHelper::loop();
} }
/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter /** Read a packet into the rx_buf_.
*
* @param frame: The struct to hold the frame information in.
* msg: store the parsed frame in that struct
* *
* @return See APIError * @return See APIError
* *
* error API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame. * error API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame.
*/ */
APIError APIPlaintextFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) { APIError APIPlaintextFrameHelper::try_read_frame_() {
if (frame == nullptr) {
HELPER_LOG("Bad argument for try_read_frame_");
return APIError::BAD_ARG;
}
// read header // read header
while (!rx_header_parsed_) { while (!rx_header_parsed_) {
// Now that we know when the socket is ready, we can read up to 3 bytes // Now that we know when the socket is ready, we can read up to 3 bytes
@@ -123,10 +115,10 @@ APIError APIPlaintextFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
continue; continue;
} }
if (msg_size_varint->as_uint32() > std::numeric_limits<uint16_t>::max()) { if (msg_size_varint->as_uint32() > MAX_MESSAGE_SIZE) {
state_ = State::FAILED; state_ = State::FAILED;
HELPER_LOG("Bad packet: message size %" PRIu32 " exceeds maximum %u", msg_size_varint->as_uint32(), HELPER_LOG("Bad packet: message size %" PRIu32 " exceeds maximum %u", msg_size_varint->as_uint32(),
std::numeric_limits<uint16_t>::max()); MAX_MESSAGE_SIZE);
return APIError::BAD_DATA_PACKET; return APIError::BAD_DATA_PACKET;
} }
rx_header_parsed_len_ = msg_size_varint->as_uint16(); rx_header_parsed_len_ = msg_size_varint->as_uint16();
@@ -150,9 +142,9 @@ APIError APIPlaintextFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
} }
// header reading done // header reading done
// reserve space for body // Reserve space for body
if (rx_buf_.size() != rx_header_parsed_len_) { if (this->rx_buf_.size() != this->rx_header_parsed_len_) {
rx_buf_.resize(rx_header_parsed_len_); this->rx_buf_.resize(this->rx_header_parsed_len_);
} }
if (rx_buf_len_ < rx_header_parsed_len_) { if (rx_buf_len_ < rx_header_parsed_len_) {
@@ -170,24 +162,22 @@ APIError APIPlaintextFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
} }
} }
LOG_PACKET_RECEIVED(rx_buf_); LOG_PACKET_RECEIVED(this->rx_buf_);
*frame = std::move(rx_buf_);
// consume msg // Clear state for next frame (rx_buf_ still contains data for caller)
rx_buf_ = {}; this->rx_buf_len_ = 0;
rx_buf_len_ = 0; this->rx_header_buf_pos_ = 0;
rx_header_buf_pos_ = 0; this->rx_header_parsed_ = false;
rx_header_parsed_ = false;
return APIError::OK; return APIError::OK;
} }
APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
APIError aerr;
if (state_ != State::DATA) { APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
if (this->state_ != State::DATA) {
return APIError::WOULD_BLOCK; return APIError::WOULD_BLOCK;
} }
std::vector<uint8_t> frame; APIError aerr = this->try_read_frame_();
aerr = try_read_frame_(&frame);
if (aerr != APIError::OK) { if (aerr != APIError::OK) {
if (aerr == APIError::BAD_INDICATOR) { if (aerr == APIError::BAD_INDICATOR) {
// Make sure to tell the remote that we don't // Make sure to tell the remote that we don't
@@ -220,10 +210,10 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
return aerr; return aerr;
} }
buffer->container = std::move(frame); buffer->container = std::move(this->rx_buf_);
buffer->data_offset = 0; buffer->data_offset = 0;
buffer->data_len = rx_header_parsed_len_; buffer->data_len = this->rx_header_parsed_len_;
buffer->type = rx_header_parsed_type_; buffer->type = this->rx_header_parsed_type_;
return APIError::OK; return APIError::OK;
} }
APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) { APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {

View File

@@ -24,7 +24,7 @@ class APIPlaintextFrameHelper final : public APIFrameHelper {
APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override; APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override;
protected: protected:
APIError try_read_frame_(std::vector<uint8_t> *frame); APIError try_read_frame_();
// Group 2-byte aligned types // Group 2-byte aligned types
uint16_t rx_header_parsed_type_ = 0; uint16_t rx_header_parsed_type_ = 0;

View File

@@ -884,6 +884,15 @@ void HomeassistantActionRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_message(4, it, true); buffer.encode_message(4, it, true);
} }
buffer.encode_bool(5, this->is_event); buffer.encode_bool(5, this->is_event);
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
buffer.encode_uint32(6, this->call_id);
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
buffer.encode_bool(7, this->wants_response);
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
buffer.encode_string(8, this->response_template);
#endif
} }
void HomeassistantActionRequest::calculate_size(ProtoSize &size) const { void HomeassistantActionRequest::calculate_size(ProtoSize &size) const {
size.add_length(1, this->service_ref_.size()); size.add_length(1, this->service_ref_.size());
@@ -891,6 +900,48 @@ void HomeassistantActionRequest::calculate_size(ProtoSize &size) const {
size.add_repeated_message(1, this->data_template); size.add_repeated_message(1, this->data_template);
size.add_repeated_message(1, this->variables); size.add_repeated_message(1, this->variables);
size.add_bool(1, this->is_event); size.add_bool(1, this->is_event);
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
size.add_uint32(1, this->call_id);
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
size.add_bool(1, this->wants_response);
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
size.add_length(1, this->response_template.size());
#endif
}
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
bool HomeassistantActionResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 1:
this->call_id = value.as_uint32();
break;
case 2:
this->success = value.as_bool();
break;
default:
return false;
}
return true;
}
bool HomeassistantActionResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 3:
this->error_message = value.as_string();
break;
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
case 4: {
// Use raw data directly to avoid allocation
this->response_data = value.data();
this->response_data_len = value.size();
break;
}
#endif
default:
return false;
}
return true;
} }
#endif #endif
#ifdef USE_API_HOMEASSISTANT_STATES #ifdef USE_API_HOMEASSISTANT_STATES

View File

@@ -1104,7 +1104,7 @@ class HomeassistantServiceMap final : public ProtoMessage {
class HomeassistantActionRequest final : public ProtoMessage { class HomeassistantActionRequest final : public ProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 35; static constexpr uint8_t MESSAGE_TYPE = 35;
static constexpr uint8_t ESTIMATED_SIZE = 113; static constexpr uint8_t ESTIMATED_SIZE = 128;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "homeassistant_action_request"; } const char *message_name() const override { return "homeassistant_action_request"; }
#endif #endif
@@ -1114,6 +1114,15 @@ class HomeassistantActionRequest final : public ProtoMessage {
std::vector<HomeassistantServiceMap> data_template{}; std::vector<HomeassistantServiceMap> data_template{};
std::vector<HomeassistantServiceMap> variables{}; std::vector<HomeassistantServiceMap> variables{};
bool is_event{false}; bool is_event{false};
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
uint32_t call_id{0};
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
bool wants_response{false};
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
std::string response_template{};
#endif
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@@ -1123,6 +1132,30 @@ class HomeassistantActionRequest final : public ProtoMessage {
protected: protected:
}; };
#endif #endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
class HomeassistantActionResponse final : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 130;
static constexpr uint8_t ESTIMATED_SIZE = 34;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "homeassistant_action_response"; }
#endif
uint32_t call_id{0};
bool success{false};
std::string error_message{};
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
const uint8_t *response_data{nullptr};
uint16_t response_data_len{0};
#endif
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
#endif
#ifdef USE_API_HOMEASSISTANT_STATES #ifdef USE_API_HOMEASSISTANT_STATES
class SubscribeHomeAssistantStatesRequest final : public ProtoMessage { class SubscribeHomeAssistantStatesRequest final : public ProtoMessage {
public: public:

View File

@@ -1122,6 +1122,28 @@ void HomeassistantActionRequest::dump_to(std::string &out) const {
out.append("\n"); out.append("\n");
} }
dump_field(out, "is_event", this->is_event); dump_field(out, "is_event", this->is_event);
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
dump_field(out, "call_id", this->call_id);
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
dump_field(out, "wants_response", this->wants_response);
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
dump_field(out, "response_template", this->response_template);
#endif
}
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
void HomeassistantActionResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "HomeassistantActionResponse");
dump_field(out, "call_id", this->call_id);
dump_field(out, "success", this->success);
dump_field(out, "error_message", this->error_message);
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
out.append(" response_data: ");
out.append(format_hex_pretty(this->response_data, this->response_data_len));
out.append("\n");
#endif
} }
#endif #endif
#ifdef USE_API_HOMEASSISTANT_STATES #ifdef USE_API_HOMEASSISTANT_STATES

View File

@@ -610,6 +610,17 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
this->on_z_wave_proxy_request(msg); this->on_z_wave_proxy_request(msg);
break; break;
} }
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
case HomeassistantActionResponse::MESSAGE_TYPE: {
HomeassistantActionResponse msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_homeassistant_action_response: %s", msg.dump().c_str());
#endif
this->on_homeassistant_action_response(msg);
break;
}
#endif #endif
default: default:
break; break;

View File

@@ -66,6 +66,9 @@ class APIServerConnectionBase : public ProtoService {
virtual void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &value){}; virtual void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &value){};
#endif #endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
virtual void on_homeassistant_action_response(const HomeassistantActionResponse &value){};
#endif
#ifdef USE_API_HOMEASSISTANT_STATES #ifdef USE_API_HOMEASSISTANT_STATES
virtual void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &value){}; virtual void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &value){};
#endif #endif

View File

@@ -9,12 +9,16 @@
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/util.h" #include "esphome/core/util.h"
#include "esphome/core/version.h" #include "esphome/core/version.h"
#ifdef USE_API_HOMEASSISTANT_SERVICES
#include "homeassistant_service.h"
#endif
#ifdef USE_LOGGER #ifdef USE_LOGGER
#include "esphome/components/logger/logger.h" #include "esphome/components/logger/logger.h"
#endif #endif
#include <algorithm> #include <algorithm>
#include <utility>
namespace esphome::api { namespace esphome::api {
@@ -400,7 +404,38 @@ void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call
client->send_homeassistant_action(call); client->send_homeassistant_action(call);
} }
} }
#endif #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
void APIServer::register_action_response_callback(uint32_t call_id, ActionResponseCallback callback) {
this->action_response_callbacks_.push_back({call_id, std::move(callback)});
}
void APIServer::handle_action_response(uint32_t call_id, bool success, const std::string &error_message) {
for (auto it = this->action_response_callbacks_.begin(); it != this->action_response_callbacks_.end(); ++it) {
if (it->call_id == call_id) {
auto callback = std::move(it->callback);
this->action_response_callbacks_.erase(it);
ActionResponse response(success, error_message);
callback(response);
return;
}
}
}
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
void APIServer::handle_action_response(uint32_t call_id, bool success, const std::string &error_message,
const uint8_t *response_data, size_t response_data_len) {
for (auto it = this->action_response_callbacks_.begin(); it != this->action_response_callbacks_.end(); ++it) {
if (it->call_id == call_id) {
auto callback = std::move(it->callback);
this->action_response_callbacks_.erase(it);
ActionResponse response(success, error_message, response_data, response_data_len);
callback(response);
return;
}
}
}
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
#endif // USE_API_HOMEASSISTANT_SERVICES
#ifdef USE_API_HOMEASSISTANT_STATES #ifdef USE_API_HOMEASSISTANT_STATES
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute, void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,

View File

@@ -16,6 +16,7 @@
#include "user_services.h" #include "user_services.h"
#endif #endif
#include <map>
#include <vector> #include <vector>
namespace esphome::api { namespace esphome::api {
@@ -111,7 +112,17 @@ class APIServer : public Component, public Controller {
#ifdef USE_API_HOMEASSISTANT_SERVICES #ifdef USE_API_HOMEASSISTANT_SERVICES
void send_homeassistant_action(const HomeassistantActionRequest &call); void send_homeassistant_action(const HomeassistantActionRequest &call);
#endif #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
// Action response handling
using ActionResponseCallback = std::function<void(const class ActionResponse &)>;
void register_action_response_callback(uint32_t call_id, ActionResponseCallback callback);
void handle_action_response(uint32_t call_id, bool success, const std::string &error_message);
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
void handle_action_response(uint32_t call_id, bool success, const std::string &error_message,
const uint8_t *response_data, size_t response_data_len);
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
#endif // USE_API_HOMEASSISTANT_SERVICES
#ifdef USE_API_SERVICES #ifdef USE_API_SERVICES
void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }
#endif #endif
@@ -187,6 +198,13 @@ class APIServer : public Component, public Controller {
#ifdef USE_API_SERVICES #ifdef USE_API_SERVICES
std::vector<UserServiceDescriptor *> user_services_; std::vector<UserServiceDescriptor *> user_services_;
#endif #endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
struct PendingActionResponse {
uint32_t call_id;
ActionResponseCallback callback;
};
std::vector<PendingActionResponse> action_response_callbacks_;
#endif
// Group smaller types together // Group smaller types together
uint16_t port_{6053}; uint16_t port_{6053};

View File

@@ -3,8 +3,13 @@
#include "api_server.h" #include "api_server.h"
#ifdef USE_API #ifdef USE_API
#ifdef USE_API_HOMEASSISTANT_SERVICES #ifdef USE_API_HOMEASSISTANT_SERVICES
#include <functional>
#include <utility>
#include <vector> #include <vector>
#include "api_pb2.h" #include "api_pb2.h"
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
#include "esphome/components/json/json_util.h"
#endif
#include "esphome/core/automation.h" #include "esphome/core/automation.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
@@ -44,9 +49,47 @@ template<typename... Ts> class TemplatableKeyValuePair {
TemplatableStringValue<Ts...> value; TemplatableStringValue<Ts...> value;
}; };
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
// Represents the response data from a Home Assistant action
class ActionResponse {
public:
ActionResponse(bool success, std::string error_message = "")
: success_(success), error_message_(std::move(error_message)) {}
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
ActionResponse(bool success, std::string error_message, const uint8_t *data, size_t data_len)
: success_(success), error_message_(std::move(error_message)) {
if (data == nullptr || data_len == 0)
return;
this->json_document_ = json::parse_json(data, data_len);
}
#endif
bool is_success() const { return this->success_; }
const std::string &get_error_message() const { return this->error_message_; }
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
// Get data as parsed JSON object (const version returns read-only view)
JsonObjectConst get_json() const { return this->json_document_.as<JsonObjectConst>(); }
#endif
protected:
bool success_;
std::string error_message_;
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
JsonDocument json_document_;
#endif
};
// Callback type for action responses
template<typename... Ts> using ActionResponseCallback = std::function<void(const ActionResponse &, Ts...)>;
#endif
template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts...> { template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts...> {
public: public:
explicit HomeAssistantServiceCallAction(APIServer *parent, bool is_event) : parent_(parent), is_event_(is_event) {} explicit HomeAssistantServiceCallAction(APIServer *parent, bool is_event) : parent_(parent) {
this->flags_.is_event = is_event;
}
template<typename T> void set_service(T service) { this->service_ = service; } template<typename T> void set_service(T service) { this->service_ = service; }
@@ -61,11 +104,29 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
this->variables_.emplace_back(std::move(key), value); this->variables_.emplace_back(std::move(key), value);
} }
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
template<typename T> void set_response_template(T response_template) {
this->response_template_ = response_template;
this->flags_.has_response_template = true;
}
void set_wants_status() { this->flags_.wants_status = true; }
void set_wants_response() { this->flags_.wants_response = true; }
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
Trigger<JsonObjectConst, Ts...> *get_success_trigger_with_response() const {
return this->success_trigger_with_response_;
}
#endif
Trigger<Ts...> *get_success_trigger() const { return this->success_trigger_; }
Trigger<std::string, Ts...> *get_error_trigger() const { return this->error_trigger_; }
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
void play(Ts... x) override { void play(Ts... x) override {
HomeassistantActionRequest resp; HomeassistantActionRequest resp;
std::string service_value = this->service_.value(x...); std::string service_value = this->service_.value(x...);
resp.set_service(StringRef(service_value)); resp.set_service(StringRef(service_value));
resp.is_event = this->is_event_; resp.is_event = this->flags_.is_event;
for (auto &it : this->data_) { for (auto &it : this->data_) {
resp.data.emplace_back(); resp.data.emplace_back();
auto &kv = resp.data.back(); auto &kv = resp.data.back();
@@ -84,18 +145,74 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
kv.set_key(StringRef(it.key)); kv.set_key(StringRef(it.key));
kv.value = it.value.value(x...); kv.value = it.value.value(x...);
} }
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
if (this->flags_.wants_status) {
// Generate a unique call ID for this service call
static uint32_t call_id_counter = 1;
uint32_t call_id = call_id_counter++;
resp.call_id = call_id;
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
if (this->flags_.wants_response) {
resp.wants_response = true;
// Set response template if provided
if (this->flags_.has_response_template) {
std::string response_template_value = this->response_template_.value(x...);
resp.response_template = response_template_value;
}
}
#endif
auto captured_args = std::make_tuple(x...);
this->parent_->register_action_response_callback(call_id, [this, captured_args](const ActionResponse &response) {
std::apply(
[this, &response](auto &&...args) {
if (response.is_success()) {
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
if (this->flags_.wants_response) {
this->success_trigger_with_response_->trigger(response.get_json(), args...);
} else
#endif
{
this->success_trigger_->trigger(args...);
}
} else {
this->error_trigger_->trigger(response.get_error_message(), args...);
}
},
captured_args);
});
}
#endif
this->parent_->send_homeassistant_action(resp); this->parent_->send_homeassistant_action(resp);
} }
protected: protected:
APIServer *parent_; APIServer *parent_;
bool is_event_;
TemplatableStringValue<Ts...> service_{}; TemplatableStringValue<Ts...> service_{};
std::vector<TemplatableKeyValuePair<Ts...>> data_; std::vector<TemplatableKeyValuePair<Ts...>> data_;
std::vector<TemplatableKeyValuePair<Ts...>> data_template_; std::vector<TemplatableKeyValuePair<Ts...>> data_template_;
std::vector<TemplatableKeyValuePair<Ts...>> variables_; std::vector<TemplatableKeyValuePair<Ts...>> variables_;
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
TemplatableStringValue<Ts...> response_template_{""};
Trigger<JsonObjectConst, Ts...> *success_trigger_with_response_ = new Trigger<JsonObjectConst, Ts...>();
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
Trigger<Ts...> *success_trigger_ = new Trigger<Ts...>();
Trigger<std::string, Ts...> *error_trigger_ = new Trigger<std::string, Ts...>();
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
struct Flags {
uint8_t is_event : 1;
uint8_t wants_status : 1;
uint8_t wants_response : 1;
uint8_t has_response_template : 1;
uint8_t reserved : 5;
} flags_{0};
}; };
} // namespace esphome::api } // namespace esphome::api
#endif #endif
#endif #endif

View File

@@ -35,7 +35,7 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
msg.set_name(StringRef(this->name_)); msg.set_name(StringRef(this->name_));
msg.key = this->key_; msg.key = this->key_;
std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...}; std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
for (int i = 0; i < sizeof...(Ts); i++) { for (size_t i = 0; i < sizeof...(Ts); i++) {
msg.args.emplace_back(); msg.args.emplace_back();
auto &arg = msg.args.back(); auto &arg = msg.args.back();
arg.type = arg_types[i]; arg.type = arg_types[i];

View File

@@ -165,4 +165,4 @@ def final_validate_audio_schema(
async def to_code(config): async def to_code(config):
cg.add_library("esphome/esp-audio-libs", "1.1.4") cg.add_library("esphome/esp-audio-libs", "2.0.1")

View File

@@ -57,7 +57,7 @@ const char *audio_file_type_to_string(AudioFileType file_type) {
void scale_audio_samples(const int16_t *audio_samples, int16_t *output_buffer, int16_t scale_factor, void scale_audio_samples(const int16_t *audio_samples, int16_t *output_buffer, int16_t scale_factor,
size_t samples_to_scale) { size_t samples_to_scale) {
// Note the assembly dsps_mulc function has audio glitches if the input and output buffers are the same. // Note the assembly dsps_mulc function has audio glitches if the input and output buffers are the same.
for (int i = 0; i < samples_to_scale; i++) { for (size_t i = 0; i < samples_to_scale; i++) {
int32_t acc = (int32_t) audio_samples[i] * (int32_t) scale_factor; int32_t acc = (int32_t) audio_samples[i] * (int32_t) scale_factor;
output_buffer[i] = (int16_t) (acc >> 15); output_buffer[i] = (int16_t) (acc >> 15);
} }

View File

@@ -229,18 +229,18 @@ FileDecoderState AudioDecoder::decode_flac_() {
auto result = this->flac_decoder_->read_header(this->input_transfer_buffer_->get_buffer_start(), auto result = this->flac_decoder_->read_header(this->input_transfer_buffer_->get_buffer_start(),
this->input_transfer_buffer_->available()); this->input_transfer_buffer_->available());
if (result == esp_audio_libs::flac::FLAC_DECODER_HEADER_OUT_OF_DATA) { if (result > esp_audio_libs::flac::FLAC_DECODER_HEADER_OUT_OF_DATA) {
return FileDecoderState::POTENTIALLY_FAILED; // Serrious error reading FLAC header, there is no recovery
}
if (result != esp_audio_libs::flac::FLAC_DECODER_SUCCESS) {
// Couldn't read FLAC header
return FileDecoderState::FAILED; return FileDecoderState::FAILED;
} }
size_t bytes_consumed = this->flac_decoder_->get_bytes_index(); size_t bytes_consumed = this->flac_decoder_->get_bytes_index();
this->input_transfer_buffer_->decrease_buffer_length(bytes_consumed); this->input_transfer_buffer_->decrease_buffer_length(bytes_consumed);
if (result == esp_audio_libs::flac::FLAC_DECODER_HEADER_OUT_OF_DATA) {
return FileDecoderState::MORE_TO_PROCESS;
}
// Reallocate the output transfer buffer to the smallest necessary size // Reallocate the output transfer buffer to the smallest necessary size
this->free_buffer_required_ = flac_decoder_->get_output_buffer_size_bytes(); this->free_buffer_required_ = flac_decoder_->get_output_buffer_size_bytes();
if (!this->output_transfer_buffer_->reallocate(this->free_buffer_required_)) { if (!this->output_transfer_buffer_->reallocate(this->free_buffer_required_)) {
@@ -256,9 +256,9 @@ FileDecoderState AudioDecoder::decode_flac_() {
} }
uint32_t output_samples = 0; uint32_t output_samples = 0;
auto result = this->flac_decoder_->decode_frame( auto result = this->flac_decoder_->decode_frame(this->input_transfer_buffer_->get_buffer_start(),
this->input_transfer_buffer_->get_buffer_start(), this->input_transfer_buffer_->available(), this->input_transfer_buffer_->available(),
reinterpret_cast<int16_t *>(this->output_transfer_buffer_->get_buffer_end()), &output_samples); this->output_transfer_buffer_->get_buffer_end(), &output_samples);
if (result == esp_audio_libs::flac::FLAC_DECODER_ERROR_OUT_OF_DATA) { if (result == esp_audio_libs::flac::FLAC_DECODER_ERROR_OUT_OF_DATA) {
// Not an issue, just needs more data that we'll get next time. // Not an issue, just needs more data that we'll get next time.

View File

@@ -97,10 +97,10 @@ void BL0906::handle_actions_() {
return; return;
} }
ActionCallbackFuncPtr ptr_func = nullptr; ActionCallbackFuncPtr ptr_func = nullptr;
for (int i = 0; i < this->action_queue_.size(); i++) { for (size_t i = 0; i < this->action_queue_.size(); i++) {
ptr_func = this->action_queue_[i]; ptr_func = this->action_queue_[i];
if (ptr_func) { if (ptr_func) {
ESP_LOGI(TAG, "HandleActionCallback[%d]", i); ESP_LOGI(TAG, "HandleActionCallback[%zu]", i);
(this->*ptr_func)(); (this->*ptr_func)();
} }
} }

View File

@@ -51,7 +51,7 @@ void BL0942::loop() {
if (!avail) { if (!avail) {
return; return;
} }
if (avail < sizeof(buffer)) { if (static_cast<size_t>(avail) < sizeof(buffer)) {
if (!this->rx_start_) { if (!this->rx_start_) {
this->rx_start_ = millis(); this->rx_start_ = millis();
} else if (millis() > this->rx_start_ + PKT_TIMEOUT_MS) { } else if (millis() > this->rx_start_ + PKT_TIMEOUT_MS) {
@@ -148,7 +148,7 @@ void BL0942::setup() {
this->write_reg_(BL0942_REG_USR_WRPROT, 0); this->write_reg_(BL0942_REG_USR_WRPROT, 0);
if (this->read_reg_(BL0942_REG_MODE) != mode) if (static_cast<uint32_t>(this->read_reg_(BL0942_REG_MODE)) != mode)
this->status_set_warning(LOG_STR("BL0942 setup failed!")); this->status_set_warning(LOG_STR("BL0942 setup failed!"));
this->flush(); this->flush();

View File

@@ -105,9 +105,9 @@ class Canbus : public Component {
CallbackManager<void(uint32_t can_id, bool extended_id, bool rtr, const std::vector<uint8_t> &data)> CallbackManager<void(uint32_t can_id, bool extended_id, bool rtr, const std::vector<uint8_t> &data)>
callback_manager_{}; callback_manager_{};
virtual bool setup_internal(); virtual bool setup_internal() = 0;
virtual Error send_message(struct CanFrame *frame); virtual Error send_message(struct CanFrame *frame) = 0;
virtual Error read_message(struct CanFrame *frame); virtual Error read_message(struct CanFrame *frame) = 0;
}; };
template<typename... Ts> class CanbusSendAction : public Action<Ts...>, public Parented<Canbus> { template<typename... Ts> class CanbusSendAction : public Action<Ts...>, public Parented<Canbus> {

View File

@@ -13,7 +13,7 @@ static const uint8_t C_M1106_CMD_SET_CO2_CALIB_RESPONSE[4] = {0x16, 0x01, 0x03,
uint8_t cm1106_checksum(const uint8_t *response, size_t len) { uint8_t cm1106_checksum(const uint8_t *response, size_t len) {
uint8_t crc = 0; uint8_t crc = 0;
for (int i = 0; i < len - 1; i++) { for (size_t i = 0; i < len - 1; i++) {
crc -= response[i]; crc -= response[i];
} }
return crc; return crc;

View File

@@ -26,7 +26,7 @@ void DaikinArcClimate::transmit_query_() {
uint8_t remote_header[8] = {0x11, 0xDA, 0x27, 0x00, 0x84, 0x87, 0x20, 0x00}; uint8_t remote_header[8] = {0x11, 0xDA, 0x27, 0x00, 0x84, 0x87, 0x20, 0x00};
// Calculate checksum // Calculate checksum
for (int i = 0; i < sizeof(remote_header) - 1; i++) { for (size_t i = 0; i < sizeof(remote_header) - 1; i++) {
remote_header[sizeof(remote_header) - 1] += remote_header[i]; remote_header[sizeof(remote_header) - 1] += remote_header[i];
} }
@@ -102,7 +102,7 @@ void DaikinArcClimate::transmit_state() {
remote_state[9] = fan_speed & 0xff; remote_state[9] = fan_speed & 0xff;
// Calculate checksum // Calculate checksum
for (int i = 0; i < sizeof(remote_header) - 1; i++) { for (size_t i = 0; i < sizeof(remote_header) - 1; i++) {
remote_header[sizeof(remote_header) - 1] += remote_header[i]; remote_header[sizeof(remote_header) - 1] += remote_header[i];
} }
@@ -350,7 +350,7 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) {
bool valid_daikin_frame = false; bool valid_daikin_frame = false;
if (data.expect_item(DAIKIN_HEADER_MARK, DAIKIN_HEADER_SPACE)) { if (data.expect_item(DAIKIN_HEADER_MARK, DAIKIN_HEADER_SPACE)) {
valid_daikin_frame = true; valid_daikin_frame = true;
int bytes_count = data.size() / 2 / 8; size_t bytes_count = data.size() / 2 / 8;
std::unique_ptr<char[]> buf(new char[bytes_count * 3 + 1]); std::unique_ptr<char[]> buf(new char[bytes_count * 3 + 1]);
buf[0] = '\0'; buf[0] = '\0';
for (size_t i = 0; i < bytes_count; i++) { for (size_t i = 0; i < bytes_count; i++) {
@@ -370,7 +370,7 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) {
if (!valid_daikin_frame) { if (!valid_daikin_frame) {
char sbuf[16 * 10 + 1]; char sbuf[16 * 10 + 1];
sbuf[0] = '\0'; sbuf[0] = '\0';
for (size_t j = 0; j < data.size(); j++) { for (size_t j = 0; j < static_cast<size_t>(data.size()); j++) {
if ((j - 2) % 16 == 0) { if ((j - 2) % 16 == 0) {
if (j > 0) { if (j > 0) {
ESP_LOGD(TAG, "DATA %04x: %s", (j - 16 > 0xffff ? 0 : j - 16), sbuf); ESP_LOGD(TAG, "DATA %04x: %s", (j - 16 > 0xffff ? 0 : j - 16), sbuf);
@@ -380,19 +380,26 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) {
char type_ch = ' '; char type_ch = ' ';
// debug_tolerance = 25% // debug_tolerance = 25%
if (DAIKIN_DBG_LOWER(DAIKIN_ARC_PRE_MARK) <= data[j] && data[j] <= DAIKIN_DBG_UPPER(DAIKIN_ARC_PRE_MARK)) if (static_cast<int32_t>(DAIKIN_DBG_LOWER(DAIKIN_ARC_PRE_MARK)) <= data[j] &&
data[j] <= static_cast<int32_t>(DAIKIN_DBG_UPPER(DAIKIN_ARC_PRE_MARK)))
type_ch = 'P'; type_ch = 'P';
if (DAIKIN_DBG_LOWER(DAIKIN_ARC_PRE_SPACE) <= -data[j] && -data[j] <= DAIKIN_DBG_UPPER(DAIKIN_ARC_PRE_SPACE)) if (static_cast<int32_t>(DAIKIN_DBG_LOWER(DAIKIN_ARC_PRE_SPACE)) <= -data[j] &&
-data[j] <= static_cast<int32_t>(DAIKIN_DBG_UPPER(DAIKIN_ARC_PRE_SPACE)))
type_ch = 'a'; type_ch = 'a';
if (DAIKIN_DBG_LOWER(DAIKIN_HEADER_MARK) <= data[j] && data[j] <= DAIKIN_DBG_UPPER(DAIKIN_HEADER_MARK)) if (static_cast<int32_t>(DAIKIN_DBG_LOWER(DAIKIN_HEADER_MARK)) <= data[j] &&
data[j] <= static_cast<int32_t>(DAIKIN_DBG_UPPER(DAIKIN_HEADER_MARK)))
type_ch = 'H'; type_ch = 'H';
if (DAIKIN_DBG_LOWER(DAIKIN_HEADER_SPACE) <= -data[j] && -data[j] <= DAIKIN_DBG_UPPER(DAIKIN_HEADER_SPACE)) if (static_cast<int32_t>(DAIKIN_DBG_LOWER(DAIKIN_HEADER_SPACE)) <= -data[j] &&
-data[j] <= static_cast<int32_t>(DAIKIN_DBG_UPPER(DAIKIN_HEADER_SPACE)))
type_ch = 'h'; type_ch = 'h';
if (DAIKIN_DBG_LOWER(DAIKIN_BIT_MARK) <= data[j] && data[j] <= DAIKIN_DBG_UPPER(DAIKIN_BIT_MARK)) if (static_cast<int32_t>(DAIKIN_DBG_LOWER(DAIKIN_BIT_MARK)) <= data[j] &&
data[j] <= static_cast<int32_t>(DAIKIN_DBG_UPPER(DAIKIN_BIT_MARK)))
type_ch = 'B'; type_ch = 'B';
if (DAIKIN_DBG_LOWER(DAIKIN_ONE_SPACE) <= -data[j] && -data[j] <= DAIKIN_DBG_UPPER(DAIKIN_ONE_SPACE)) if (static_cast<int32_t>(DAIKIN_DBG_LOWER(DAIKIN_ONE_SPACE)) <= -data[j] &&
-data[j] <= static_cast<int32_t>(DAIKIN_DBG_UPPER(DAIKIN_ONE_SPACE)))
type_ch = '1'; type_ch = '1';
if (DAIKIN_DBG_LOWER(DAIKIN_ZERO_SPACE) <= -data[j] && -data[j] <= DAIKIN_DBG_UPPER(DAIKIN_ZERO_SPACE)) if (static_cast<int32_t>(DAIKIN_DBG_LOWER(DAIKIN_ZERO_SPACE)) <= -data[j] &&
-data[j] <= static_cast<int32_t>(DAIKIN_DBG_UPPER(DAIKIN_ZERO_SPACE)))
type_ch = '0'; type_ch = '0';
if (abs(data[j]) > 100000) { if (abs(data[j]) > 100000) {
@@ -400,7 +407,7 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) {
} else { } else {
sprintf(sbuf, "%s%-5d[%c] ", sbuf, (int) (round(data[j] / 10.) * 10), type_ch); sprintf(sbuf, "%s%-5d[%c] ", sbuf, (int) (round(data[j] / 10.) * 10), type_ch);
} }
if (j == data.size() - 1) { if (j + 1 == static_cast<size_t>(data.size())) {
ESP_LOGD(TAG, "DATA %04x: %s", (j - 8 > 0xffff ? 0 : j - 8), sbuf); ESP_LOGD(TAG, "DATA %04x: %s", (j - 8 > 0xffff ? 0 : j - 8), sbuf);
} }
} }

View File

@@ -5,7 +5,7 @@ namespace dashboard_import {
static std::string g_package_import_url; // NOLINT static std::string g_package_import_url; // NOLINT
std::string get_package_import_url() { return g_package_import_url; } const std::string &get_package_import_url() { return g_package_import_url; }
void set_package_import_url(std::string url) { g_package_import_url = std::move(url); } void set_package_import_url(std::string url) { g_package_import_url = std::move(url); }
} // namespace dashboard_import } // namespace dashboard_import

View File

@@ -5,7 +5,7 @@
namespace esphome { namespace esphome {
namespace dashboard_import { namespace dashboard_import {
std::string get_package_import_url(); const std::string &get_package_import_url();
void set_package_import_url(std::string url); void set_package_import_url(std::string url);
} // namespace dashboard_import } // namespace dashboard_import

View File

@@ -0,0 +1 @@
CODEOWNERS = ["@esphome/core"]

View File

@@ -0,0 +1,80 @@
from esphome import core, pins
import esphome.codegen as cg
from esphome.components import display, spi
import esphome.config_validation as cv
from esphome.const import (
CONF_BUSY_PIN,
CONF_DC_PIN,
CONF_ID,
CONF_LAMBDA,
CONF_MODEL,
CONF_PAGES,
CONF_RESET_DURATION,
CONF_RESET_PIN,
)
AUTO_LOAD = ["split_buffer"]
DEPENDENCIES = ["spi"]
epaper_spi_ns = cg.esphome_ns.namespace("epaper_spi")
EPaperBase = epaper_spi_ns.class_(
"EPaperBase", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer
)
EPaperSpectraE6 = epaper_spi_ns.class_("EPaperSpectraE6", EPaperBase)
EPaper7p3InSpectraE6 = epaper_spi_ns.class_("EPaper7p3InSpectraE6", EPaperSpectraE6)
MODELS = {
"7.3in-spectra-e6": EPaper7p3InSpectraE6,
}
CONFIG_SCHEMA = cv.All(
display.FULL_DISPLAY_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(EPaperBase),
cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema,
cv.Required(CONF_MODEL): cv.one_of(*MODELS, lower=True, space="-"),
cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
cv.Optional(CONF_BUSY_PIN): pins.gpio_input_pin_schema,
cv.Optional(CONF_RESET_DURATION): cv.All(
cv.positive_time_period_milliseconds,
cv.Range(max=core.TimePeriod(milliseconds=500)),
),
}
)
.extend(cv.polling_component_schema("60s"))
.extend(spi.spi_device_schema()),
cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
)
FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
"epaper_spi", require_miso=False, require_mosi=True
)
async def to_code(config):
model = MODELS[config[CONF_MODEL]]
rhs = model.new()
var = cg.Pvariable(config[CONF_ID], rhs, model)
await display.register_display(var, config)
await spi.register_spi_device(var, config)
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
cg.add(var.set_dc_pin(dc))
if CONF_LAMBDA in config:
lambda_ = await cg.process_lambda(
config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void
)
cg.add(var.set_writer(lambda_))
if CONF_RESET_PIN in config:
reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN])
cg.add(var.set_reset_pin(reset))
if CONF_BUSY_PIN in config:
busy = await cg.gpio_pin_expression(config[CONF_BUSY_PIN])
cg.add(var.set_busy_pin(busy))
if CONF_RESET_DURATION in config:
cg.add(var.set_reset_duration(config[CONF_RESET_DURATION]))

View File

@@ -0,0 +1,227 @@
#include "epaper_spi.h"
#include <cinttypes>
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::epaper_spi {
static const char *const TAG = "epaper_spi";
static const LogString *epaper_state_to_string(EPaperState state) {
switch (state) {
case EPaperState::IDLE:
return LOG_STR("IDLE");
case EPaperState::UPDATE:
return LOG_STR("UPDATE");
case EPaperState::RESET:
return LOG_STR("RESET");
case EPaperState::INITIALISE:
return LOG_STR("INITIALISE");
case EPaperState::TRANSFER_DATA:
return LOG_STR("TRANSFER_DATA");
case EPaperState::POWER_ON:
return LOG_STR("POWER_ON");
case EPaperState::REFRESH_SCREEN:
return LOG_STR("REFRESH_SCREEN");
case EPaperState::POWER_OFF:
return LOG_STR("POWER_OFF");
case EPaperState::DEEP_SLEEP:
return LOG_STR("DEEP_SLEEP");
default:
return LOG_STR("UNKNOWN");
}
}
void EPaperBase::setup() {
if (!this->init_buffer_(this->get_buffer_length())) {
this->mark_failed("Failed to initialise buffer");
return;
}
this->setup_pins_();
this->spi_setup();
}
bool EPaperBase::init_buffer_(size_t buffer_length) {
if (!this->buffer_.init(buffer_length)) {
return false;
}
this->clear();
return true;
}
void EPaperBase::setup_pins_() {
this->dc_pin_->setup(); // OUTPUT
this->dc_pin_->digital_write(false);
if (this->reset_pin_ != nullptr) {
this->reset_pin_->setup(); // OUTPUT
this->reset_pin_->digital_write(true);
}
if (this->busy_pin_ != nullptr) {
this->busy_pin_->setup(); // INPUT
}
}
float EPaperBase::get_setup_priority() const { return setup_priority::PROCESSOR; }
void EPaperBase::command(uint8_t value) {
this->start_command_();
this->write_byte(value);
this->end_command_();
}
void EPaperBase::data(uint8_t value) {
this->start_data_();
this->write_byte(value);
this->end_data_();
}
// write a command followed by zero or more bytes of data.
// The command is the first byte, length is the length of data only in the second byte, followed by the data.
// [COMMAND, LENGTH, DATA...]
void EPaperBase::cmd_data(const uint8_t *data) {
const uint8_t command = data[0];
const uint8_t length = data[1];
const uint8_t *ptr = data + 2;
ESP_LOGVV(TAG, "Command: 0x%02X, Length: %d, Data: %s", command, length,
format_hex_pretty(ptr, length, '.', false).c_str());
this->dc_pin_->digital_write(false);
this->enable();
this->write_byte(command);
if (length > 0) {
this->dc_pin_->digital_write(true);
this->write_array(ptr, length);
}
this->disable();
}
bool EPaperBase::is_idle_() {
if (this->busy_pin_ == nullptr) {
return true;
}
return !this->busy_pin_->digital_read();
}
void EPaperBase::reset() {
if (this->reset_pin_ != nullptr) {
this->reset_pin_->digital_write(false);
this->disable_loop();
this->set_timeout(this->reset_duration_, [this] {
this->reset_pin_->digital_write(true);
this->set_timeout(20, [this] { this->enable_loop(); });
});
}
}
void EPaperBase::update() {
if (!this->state_queue_.empty()) {
ESP_LOGE(TAG, "Display update already in progress - %s",
LOG_STR_ARG(epaper_state_to_string(this->state_queue_.front())));
return;
}
this->state_queue_.push(EPaperState::UPDATE);
this->state_queue_.push(EPaperState::RESET);
this->state_queue_.push(EPaperState::INITIALISE);
this->state_queue_.push(EPaperState::TRANSFER_DATA);
this->state_queue_.push(EPaperState::POWER_ON);
this->state_queue_.push(EPaperState::REFRESH_SCREEN);
this->state_queue_.push(EPaperState::POWER_OFF);
this->state_queue_.push(EPaperState::DEEP_SLEEP);
this->state_queue_.push(EPaperState::IDLE);
this->enable_loop();
}
void EPaperBase::loop() {
if (this->waiting_for_idle_) {
if (this->is_idle_()) {
this->waiting_for_idle_ = false;
} else {
if (App.get_loop_component_start_time() - this->waiting_for_idle_last_print_ >= 1000) {
ESP_LOGV(TAG, "Waiting for idle");
this->waiting_for_idle_last_print_ = App.get_loop_component_start_time();
}
return;
}
}
auto state = this->state_queue_.front();
switch (state) {
case EPaperState::IDLE:
this->disable_loop();
break;
case EPaperState::UPDATE:
this->do_update_(); // Calls ESPHome (current page) lambda
break;
case EPaperState::RESET:
this->reset();
break;
case EPaperState::INITIALISE:
this->initialise_();
break;
case EPaperState::TRANSFER_DATA:
if (!this->transfer_data()) {
return; // Not done yet, come back next loop
}
break;
case EPaperState::POWER_ON:
this->power_on();
break;
case EPaperState::REFRESH_SCREEN:
this->refresh_screen();
break;
case EPaperState::POWER_OFF:
this->power_off();
break;
case EPaperState::DEEP_SLEEP:
this->deep_sleep();
break;
}
this->state_queue_.pop();
}
void EPaperBase::start_command_() {
this->dc_pin_->digital_write(false);
this->enable();
}
void EPaperBase::end_command_() { this->disable(); }
void EPaperBase::start_data_() {
this->dc_pin_->digital_write(true);
this->enable();
}
void EPaperBase::end_data_() { this->disable(); }
void EPaperBase::on_safe_shutdown() { this->deep_sleep(); }
void EPaperBase::initialise_() {
size_t index = 0;
const auto &sequence = this->init_sequence_;
const size_t sequence_size = this->init_sequence_length_;
while (index != sequence_size) {
if (sequence_size - index < 2) {
this->mark_failed("Malformed init sequence");
return;
}
const auto *ptr = sequence + index;
const uint8_t length = ptr[1];
if (sequence_size - index < length + 2) {
this->mark_failed("Malformed init sequence");
return;
}
this->cmd_data(ptr);
index += length + 2;
}
this->power_on();
}
} // namespace esphome::epaper_spi

View File

@@ -0,0 +1,93 @@
#pragma once
#include "esphome/components/display/display_buffer.h"
#include "esphome/components/spi/spi.h"
#include "esphome/components/split_buffer/split_buffer.h"
#include "esphome/core/component.h"
#include <queue>
namespace esphome::epaper_spi {
enum class EPaperState : uint8_t {
IDLE,
UPDATE,
RESET,
INITIALISE,
TRANSFER_DATA,
POWER_ON,
REFRESH_SCREEN,
POWER_OFF,
DEEP_SLEEP,
};
static const uint8_t MAX_TRANSFER_TIME = 10; // Transfer in 10ms blocks to allow the loop to run
class EPaperBase : public display::DisplayBuffer,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
spi::DATA_RATE_2MHZ> {
public:
EPaperBase(const uint8_t *init_sequence, const size_t init_sequence_length)
: init_sequence_length_(init_sequence_length), init_sequence_(init_sequence) {}
void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; }
float get_setup_priority() const override;
void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; }
void set_busy_pin(GPIOPin *busy) { this->busy_pin_ = busy; }
void set_reset_duration(uint32_t reset_duration) { this->reset_duration_ = reset_duration; }
void command(uint8_t value);
void data(uint8_t value);
void cmd_data(const uint8_t *data);
void update() override;
void loop() override;
void setup() override;
void on_safe_shutdown() override;
protected:
bool is_idle_();
void setup_pins_();
virtual void reset();
void initialise_();
bool init_buffer_(size_t buffer_length);
virtual int get_width_controller() { return this->get_width_internal(); };
virtual void deep_sleep() = 0;
/**
* Send data to the device via SPI
* @return true if done, false if should be called next loop
*/
virtual bool transfer_data() = 0;
virtual void refresh_screen() = 0;
virtual void power_on() = 0;
virtual void power_off() = 0;
virtual uint32_t get_buffer_length() = 0;
void start_command_();
void end_command_();
void start_data_();
void end_data_();
const size_t init_sequence_length_{0};
size_t current_data_index_{0};
uint32_t reset_duration_{200};
uint32_t waiting_for_idle_last_print_{0};
GPIOPin *dc_pin_;
GPIOPin *busy_pin_{nullptr};
GPIOPin *reset_pin_{nullptr};
const uint8_t *init_sequence_{nullptr};
bool waiting_for_idle_{false};
split_buffer::SplitBuffer buffer_;
std::queue<EPaperState> state_queue_{{EPaperState::IDLE}};
};
} // namespace esphome::epaper_spi

View File

@@ -0,0 +1,42 @@
#include "epaper_spi_model_7p3in_spectra_e6.h"
namespace esphome::epaper_spi {
static constexpr const char *const TAG = "epaper_spi.7.3in-spectra-e6";
void EPaper7p3InSpectraE6::power_on() {
ESP_LOGI(TAG, "Power on");
this->command(0x04);
this->waiting_for_idle_ = true;
}
void EPaper7p3InSpectraE6::power_off() {
ESP_LOGI(TAG, "Power off");
this->command(0x02);
this->data(0x00);
this->waiting_for_idle_ = true;
}
void EPaper7p3InSpectraE6::refresh_screen() {
ESP_LOGI(TAG, "Refresh");
this->command(0x12);
this->data(0x00);
this->waiting_for_idle_ = true;
}
void EPaper7p3InSpectraE6::deep_sleep() {
ESP_LOGI(TAG, "Deep sleep");
this->command(0x07);
this->data(0xA5);
}
void EPaper7p3InSpectraE6::dump_config() {
LOG_DISPLAY("", "E-Paper SPI", this);
ESP_LOGCONFIG(TAG, " Model: 7.3in Spectra E6");
LOG_PIN(" Reset Pin: ", this->reset_pin_);
LOG_PIN(" DC Pin: ", this->dc_pin_);
LOG_PIN(" Busy Pin: ", this->busy_pin_);
LOG_UPDATE_INTERVAL(this);
}
} // namespace esphome::epaper_spi

View File

@@ -0,0 +1,45 @@
#pragma once
#include "epaper_spi_spectra_e6.h"
namespace esphome::epaper_spi {
class EPaper7p3InSpectraE6 : public EPaperSpectraE6 {
static constexpr const uint16_t WIDTH = 800;
static constexpr const uint16_t HEIGHT = 480;
// clang-format off
// Command, data length, data
static constexpr uint8_t INIT_SEQUENCE[] = {
0xAA, 6, 0x49, 0x55, 0x20, 0x08, 0x09, 0x18,
0x01, 1, 0x3F,
0x00, 2, 0x5F, 0x69,
0x03, 4, 0x00, 0x54, 0x00, 0x44,
0x05, 4, 0x40, 0x1F, 0x1F, 0x2C,
0x06, 4, 0x6F, 0x1F, 0x17, 0x49,
0x08, 4, 0x6F, 0x1F, 0x1F, 0x22,
0x30, 1, 0x03,
0x50, 1, 0x3F,
0x60, 2, 0x02, 0x00,
0x61, 4, WIDTH / 256, WIDTH % 256, HEIGHT / 256, HEIGHT % 256,
0x84, 1, 0x01,
0xE3, 1, 0x2F,
};
// clang-format on
public:
EPaper7p3InSpectraE6() : EPaperSpectraE6(INIT_SEQUENCE, sizeof(INIT_SEQUENCE)) {}
void dump_config() override;
protected:
int get_width_internal() override { return WIDTH; };
int get_height_internal() override { return HEIGHT; };
void refresh_screen() override;
void power_on() override;
void power_off() override;
void deep_sleep() override;
};
} // namespace esphome::epaper_spi

View File

@@ -0,0 +1,135 @@
#include "epaper_spi_spectra_e6.h"
#include "esphome/core/log.h"
namespace esphome::epaper_spi {
static constexpr const char *const TAG = "epaper_spi.6c";
static inline uint8_t color_to_hex(Color color) {
if (color.red > 127) {
if (color.green > 170) {
if (color.blue > 127) {
return 0x1; // White
} else {
return 0x2; // Yellow
}
} else {
return 0x3; // Red (or Magenta)
}
} else {
if (color.green > 127) {
if (color.blue > 127) {
return 0x5; // Cyan -> Blue
} else {
return 0x6; // Green
}
} else {
if (color.blue > 127) {
return 0x5; // Blue
} else {
return 0x0; // Black
}
}
}
}
void EPaperSpectraE6::fill(Color color) {
uint8_t pixel_color;
if (color.is_on()) {
pixel_color = color_to_hex(color);
} else {
pixel_color = 0x1;
}
// We store 8 bitset<3> in 3 bytes
// | byte 1 | byte 2 | byte 3 |
// |aaabbbaa|abbbaaab|bbaaabbb|
uint8_t byte_1 = pixel_color << 5 | pixel_color << 2 | pixel_color >> 1;
uint8_t byte_2 = pixel_color << 7 | pixel_color << 4 | pixel_color << 1 | pixel_color >> 2;
uint8_t byte_3 = pixel_color << 6 | pixel_color << 3 | pixel_color << 0;
const size_t buffer_length = this->get_buffer_length();
for (size_t i = 0; i < buffer_length; i += 3) {
this->buffer_[i + 0] = byte_1;
this->buffer_[i + 1] = byte_2;
this->buffer_[i + 2] = byte_3;
}
}
uint32_t EPaperSpectraE6::get_buffer_length() {
// 6 colors buffer, 1 pixel = 3 bits, we will store 8 pixels in 24 bits = 3 bytes
return this->get_width_controller() * this->get_height_internal() / 8u * 3u;
}
void HOT EPaperSpectraE6::draw_absolute_pixel_internal(int x, int y, Color color) {
if (x >= this->get_width_internal() || y >= this->get_height_internal() || x < 0 || y < 0)
return;
uint8_t pixel_bits = color_to_hex(color);
uint32_t pixel_position = x + y * this->get_width_controller();
uint32_t first_bit_position = pixel_position * 3;
uint32_t byte_position = first_bit_position / 8u;
uint32_t byte_subposition = first_bit_position % 8u;
if (byte_subposition <= 5) {
this->buffer_[byte_position] = (this->buffer_[byte_position] & (0xFF ^ (0b111 << (5 - byte_subposition)))) |
(pixel_bits << (5 - byte_subposition));
} else {
this->buffer_[byte_position] = (this->buffer_[byte_position] & (0xFF ^ (0b111 >> (byte_subposition - 5)))) |
(pixel_bits >> (byte_subposition - 5));
this->buffer_[byte_position + 1] =
(this->buffer_[byte_position + 1] & (0xFF ^ (0xFF & (0b111 << (13 - byte_subposition))))) |
(pixel_bits << (13 - byte_subposition));
}
}
bool HOT EPaperSpectraE6::transfer_data() {
const uint32_t start_time = App.get_loop_component_start_time();
if (this->current_data_index_ == 0) {
ESP_LOGV(TAG, "Sending data");
this->command(0x10);
}
uint8_t bytes_to_send[4]{0};
const size_t buffer_length = this->get_buffer_length();
for (size_t i = this->current_data_index_; i < buffer_length; i += 3) {
const uint32_t triplet = encode_uint24(this->buffer_[i + 0], this->buffer_[i + 1], this->buffer_[i + 2]);
// 8 pixels are stored in 3 bytes
// |aaabbbaa|abbbaaab|bbaaabbb|
// | byte 1 | byte 2 | byte 3 |
bytes_to_send[0] = ((triplet >> 17) & 0b01110000) | ((triplet >> 18) & 0b00000111);
bytes_to_send[1] = ((triplet >> 11) & 0b01110000) | ((triplet >> 12) & 0b00000111);
bytes_to_send[2] = ((triplet >> 5) & 0b01110000) | ((triplet >> 6) & 0b00000111);
bytes_to_send[3] = ((triplet << 1) & 0b01110000) | ((triplet << 0) & 0b00000111);
this->start_data_();
this->write_array(bytes_to_send, sizeof(bytes_to_send));
this->end_data_();
if (millis() - start_time > MAX_TRANSFER_TIME) {
// Let the main loop run and come back next loop
this->current_data_index_ = i + 3;
return false;
}
}
// Finished the entire dataset
this->current_data_index_ = 0;
return true;
}
void EPaperSpectraE6::reset() {
if (this->reset_pin_ != nullptr) {
this->disable_loop();
this->reset_pin_->digital_write(true);
this->set_timeout(20, [this] {
this->reset_pin_->digital_write(false);
delay(2);
this->reset_pin_->digital_write(true);
this->set_timeout(20, [this] { this->enable_loop(); });
});
}
}
} // namespace esphome::epaper_spi

View File

@@ -0,0 +1,23 @@
#pragma once
#include "epaper_spi.h"
namespace esphome::epaper_spi {
class EPaperSpectraE6 : public EPaperBase {
public:
EPaperSpectraE6(const uint8_t *init_sequence, const size_t init_sequence_length)
: EPaperBase(init_sequence, init_sequence_length) {}
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
void fill(Color color) override;
protected:
void draw_absolute_pixel_internal(int x, int y, Color color) override;
uint32_t get_buffer_length() override;
bool transfer_data() override;
void reset() override;
};
} // namespace esphome::epaper_spi

View File

@@ -97,12 +97,12 @@ bool ES7210::set_mic_gain(float mic_gain) {
} }
bool ES7210::configure_sample_rate_() { bool ES7210::configure_sample_rate_() {
int mclk_fre = this->sample_rate_ * MCLK_DIV_FRE; uint32_t mclk_fre = this->sample_rate_ * MCLK_DIV_FRE;
int coeff = -1; int coeff = -1;
for (int i = 0; i < (sizeof(ES7210_COEFFICIENTS) / sizeof(ES7210_COEFFICIENTS[0])); ++i) { for (size_t i = 0; i < (sizeof(ES7210_COEFFICIENTS) / sizeof(ES7210_COEFFICIENTS[0])); ++i) {
if (ES7210_COEFFICIENTS[i].lrclk == this->sample_rate_ && ES7210_COEFFICIENTS[i].mclk == mclk_fre) if (ES7210_COEFFICIENTS[i].lrclk == this->sample_rate_ && ES7210_COEFFICIENTS[i].mclk == mclk_fre)
coeff = i; coeff = static_cast<int>(i);
} }
if (coeff >= 0) { if (coeff >= 0) {

View File

@@ -304,6 +304,17 @@ def _format_framework_espidf_version(ver: cv.Version, release: str) -> str:
return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.zip" return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.zip"
def _is_framework_url(source: str) -> str:
# platformio accepts many URL schemes for framework repositories and archives including http, https, git, file, and symlink
import urllib.parse
try:
parsed = urllib.parse.urlparse(source)
except ValueError:
return False
return bool(parsed.scheme)
# NOTE: Keep this in mind when updating the recommended version: # NOTE: Keep this in mind when updating the recommended version:
# * New framework historically have had some regressions, especially for WiFi. # * New framework historically have had some regressions, especially for WiFi.
# The new version needs to be thoroughly validated before changing the # The new version needs to be thoroughly validated before changing the
@@ -314,11 +325,12 @@ def _format_framework_espidf_version(ver: cv.Version, release: str) -> str:
# - https://github.com/espressif/arduino-esp32/releases # - https://github.com/espressif/arduino-esp32/releases
ARDUINO_FRAMEWORK_VERSION_LOOKUP = { ARDUINO_FRAMEWORK_VERSION_LOOKUP = {
"recommended": cv.Version(3, 2, 1), "recommended": cv.Version(3, 2, 1),
"latest": cv.Version(3, 3, 1), "latest": cv.Version(3, 3, 2),
"dev": cv.Version(3, 3, 1), "dev": cv.Version(3, 3, 2),
} }
ARDUINO_PLATFORM_VERSION_LOOKUP = { ARDUINO_PLATFORM_VERSION_LOOKUP = {
cv.Version(3, 3, 1): cv.Version(55, 3, 31), cv.Version(3, 3, 2): cv.Version(55, 3, 31, "1"),
cv.Version(3, 3, 1): cv.Version(55, 3, 31, "1"),
cv.Version(3, 3, 0): cv.Version(55, 3, 30, "2"), cv.Version(3, 3, 0): cv.Version(55, 3, 30, "2"),
cv.Version(3, 2, 1): cv.Version(54, 3, 21, "2"), cv.Version(3, 2, 1): cv.Version(54, 3, 21, "2"),
cv.Version(3, 2, 0): cv.Version(54, 3, 20), cv.Version(3, 2, 0): cv.Version(54, 3, 20),
@@ -336,8 +348,8 @@ ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
"dev": cv.Version(5, 5, 1), "dev": cv.Version(5, 5, 1),
} }
ESP_IDF_PLATFORM_VERSION_LOOKUP = { ESP_IDF_PLATFORM_VERSION_LOOKUP = {
cv.Version(5, 5, 1): cv.Version(55, 3, 31), cv.Version(5, 5, 1): cv.Version(55, 3, 31, "1"),
cv.Version(5, 5, 0): cv.Version(55, 3, 31), cv.Version(5, 5, 0): cv.Version(55, 3, 31, "1"),
cv.Version(5, 4, 2): cv.Version(54, 3, 21, "2"), cv.Version(5, 4, 2): cv.Version(54, 3, 21, "2"),
cv.Version(5, 4, 1): cv.Version(54, 3, 21, "2"), cv.Version(5, 4, 1): cv.Version(54, 3, 21, "2"),
cv.Version(5, 4, 0): cv.Version(54, 3, 21, "2"), cv.Version(5, 4, 0): cv.Version(54, 3, 21, "2"),
@@ -352,8 +364,8 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = {
# - https://github.com/pioarduino/platform-espressif32/releases # - https://github.com/pioarduino/platform-espressif32/releases
PLATFORM_VERSION_LOOKUP = { PLATFORM_VERSION_LOOKUP = {
"recommended": cv.Version(54, 3, 21, "2"), "recommended": cv.Version(54, 3, 21, "2"),
"latest": cv.Version(55, 3, 31), "latest": cv.Version(55, 3, 31, "1"),
"dev": "https://github.com/pioarduino/platform-espressif32.git#develop", "dev": cv.Version(55, 3, 31, "1"),
} }
@@ -386,6 +398,10 @@ def _check_versions(value):
value[CONF_SOURCE] = value.get( value[CONF_SOURCE] = value.get(
CONF_SOURCE, _format_framework_arduino_version(version) CONF_SOURCE, _format_framework_arduino_version(version)
) )
if _is_framework_url(value[CONF_SOURCE]):
value[CONF_SOURCE] = (
f"pioarduino/framework-arduinoespressif32@{value[CONF_SOURCE]}"
)
else: else:
if version < cv.Version(5, 0, 0): if version < cv.Version(5, 0, 0):
raise cv.Invalid("Only ESP-IDF 5.0+ is supported.") raise cv.Invalid("Only ESP-IDF 5.0+ is supported.")
@@ -395,6 +411,8 @@ def _check_versions(value):
CONF_SOURCE, CONF_SOURCE,
_format_framework_espidf_version(version, value.get(CONF_RELEASE, None)), _format_framework_espidf_version(version, value.get(CONF_RELEASE, None)),
) )
if _is_framework_url(value[CONF_SOURCE]):
value[CONF_SOURCE] = f"pioarduino/framework-espidf@{value[CONF_SOURCE]}"
if CONF_PLATFORM_VERSION not in value: if CONF_PLATFORM_VERSION not in value:
if platform_lookup is None: if platform_lookup is None:
@@ -639,6 +657,7 @@ def _show_framework_migration_message(name: str, variant: str) -> None:
+ "Why change? ESP-IDF offers:\n" + "Why change? ESP-IDF offers:\n"
+ color(AnsiFore.GREEN, " ✨ Up to 40% smaller binaries\n") + color(AnsiFore.GREEN, " ✨ Up to 40% smaller binaries\n")
+ color(AnsiFore.GREEN, " 🚀 Better performance and optimization\n") + color(AnsiFore.GREEN, " 🚀 Better performance and optimization\n")
+ color(AnsiFore.GREEN, " ⚡ 2-3x faster compile times\n")
+ color(AnsiFore.GREEN, " 📦 Custom-built firmware for your exact needs\n") + color(AnsiFore.GREEN, " 📦 Custom-built firmware for your exact needs\n")
+ color( + color(
AnsiFore.GREEN, AnsiFore.GREEN,
@@ -646,7 +665,6 @@ def _show_framework_migration_message(name: str, variant: str) -> None:
) )
+ "\n" + "\n"
+ "Trade-offs:\n" + "Trade-offs:\n"
+ color(AnsiFore.YELLOW, " ⏱️ Compile times are ~25% longer\n")
+ color(AnsiFore.YELLOW, " 🔄 Some components need migration\n") + color(AnsiFore.YELLOW, " 🔄 Some components need migration\n")
+ "\n" + "\n"
+ "What should I do?\n" + "What should I do?\n"

View File

@@ -285,6 +285,10 @@ def consume_connection_slots(
def validate_connection_slots(max_connections: int) -> None: def validate_connection_slots(max_connections: int) -> None:
"""Validate that BLE connection slots don't exceed the configured maximum.""" """Validate that BLE connection slots don't exceed the configured maximum."""
# Skip validation in testing mode to allow component grouping
if CORE.testing_mode:
return
ble_data = CORE.data.get(KEY_ESP32_BLE, {}) ble_data = CORE.data.get(KEY_ESP32_BLE, {})
used_slots = ble_data.get(KEY_USED_CONNECTION_SLOTS, []) used_slots = ble_data.get(KEY_USED_CONNECTION_SLOTS, [])
num_used = len(used_slots) num_used = len(used_slots)
@@ -332,12 +336,16 @@ def final_validation(config):
# Check if BLE Server is needed # Check if BLE Server is needed
has_ble_server = "esp32_ble_server" in full_config has_ble_server = "esp32_ble_server" in full_config
add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", has_ble_server)
# Check if BLE Client is needed (via esp32_ble_tracker or esp32_ble_client) # Check if BLE Client is needed (via esp32_ble_tracker or esp32_ble_client)
has_ble_client = ( has_ble_client = (
"esp32_ble_tracker" in full_config or "esp32_ble_client" in full_config "esp32_ble_tracker" in full_config or "esp32_ble_client" in full_config
) )
# ESP-IDF BLE stack requires GATT Server to be enabled when GATT Client is enabled
# This is an internal dependency in the Bluedroid stack (tested ESP-IDF 5.4.2-5.5.1)
# See: https://github.com/espressif/esp-idf/issues/17724
add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", has_ble_server or has_ble_client)
add_idf_sdkconfig_option("CONFIG_BT_GATTC_ENABLE", has_ble_client) add_idf_sdkconfig_option("CONFIG_BT_GATTC_ENABLE", has_ble_client)
# Handle max_connections: check for deprecated location in esp32_ble_tracker # Handle max_connections: check for deprecated location in esp32_ble_tracker

View File

@@ -152,7 +152,7 @@ void BLEAdvertising::loop() {
if (now - this->last_advertisement_time_ > this->advertising_cycle_time_) { if (now - this->last_advertisement_time_ > this->advertising_cycle_time_) {
this->stop(); this->stop();
this->current_adv_index_ += 1; this->current_adv_index_ += 1;
if (this->current_adv_index_ >= this->raw_advertisements_callbacks_.size()) { if (static_cast<size_t>(this->current_adv_index_) >= this->raw_advertisements_callbacks_.size()) {
this->current_adv_index_ = -1; this->current_adv_index_ = -1;
} }
this->start(); this->start();

View File

@@ -42,32 +42,18 @@ ESPBTUUID ESPBTUUID::from_raw_reversed(const uint8_t *data) {
ESPBTUUID ESPBTUUID::from_raw(const std::string &data) { ESPBTUUID ESPBTUUID::from_raw(const std::string &data) {
ESPBTUUID ret; ESPBTUUID ret;
if (data.length() == 4) { if (data.length() == 4) {
ret.uuid_.len = ESP_UUID_LEN_16; // 16-bit UUID as 4-character hex string
ret.uuid_.uuid.uuid16 = 0; auto parsed = parse_hex<uint16_t>(data);
for (uint i = 0; i < data.length(); i += 2) { if (parsed.has_value()) {
uint8_t msb = data.c_str()[i]; ret.uuid_.len = ESP_UUID_LEN_16;
uint8_t lsb = data.c_str()[i + 1]; ret.uuid_.uuid.uuid16 = parsed.value();
uint8_t lsb_shift = i <= 2 ? (2 - i) * 4 : 0;
if (msb > '9')
msb -= 7;
if (lsb > '9')
lsb -= 7;
ret.uuid_.uuid.uuid16 += (((msb & 0x0F) << 4) | (lsb & 0x0F)) << lsb_shift;
} }
} else if (data.length() == 8) { } else if (data.length() == 8) {
ret.uuid_.len = ESP_UUID_LEN_32; // 32-bit UUID as 8-character hex string
ret.uuid_.uuid.uuid32 = 0; auto parsed = parse_hex<uint32_t>(data);
for (uint i = 0; i < data.length(); i += 2) { if (parsed.has_value()) {
uint8_t msb = data.c_str()[i]; ret.uuid_.len = ESP_UUID_LEN_32;
uint8_t lsb = data.c_str()[i + 1]; ret.uuid_.uuid.uuid32 = parsed.value();
uint8_t lsb_shift = i <= 6 ? (6 - i) * 4 : 0;
if (msb > '9')
msb -= 7;
if (lsb > '9')
lsb -= 7;
ret.uuid_.uuid.uuid32 += (((msb & 0x0F) << 4) | (lsb & 0x0F)) << lsb_shift;
} }
} else if (data.length() == 16) { // how we can have 16 byte length string reprezenting 128 bit uuid??? needs to be } else if (data.length() == 16) { // how we can have 16 byte length string reprezenting 128 bit uuid??? needs to be
// investigated (lack of time) // investigated (lack of time)
@@ -145,28 +131,16 @@ bool ESPBTUUID::operator==(const ESPBTUUID &uuid) const {
if (this->uuid_.len == uuid.uuid_.len) { if (this->uuid_.len == uuid.uuid_.len) {
switch (this->uuid_.len) { switch (this->uuid_.len) {
case ESP_UUID_LEN_16: case ESP_UUID_LEN_16:
if (uuid.uuid_.uuid.uuid16 == this->uuid_.uuid.uuid16) { return this->uuid_.uuid.uuid16 == uuid.uuid_.uuid.uuid16;
return true;
}
break;
case ESP_UUID_LEN_32: case ESP_UUID_LEN_32:
if (uuid.uuid_.uuid.uuid32 == this->uuid_.uuid.uuid32) { return this->uuid_.uuid.uuid32 == uuid.uuid_.uuid.uuid32;
return true;
}
break;
case ESP_UUID_LEN_128: case ESP_UUID_LEN_128:
for (uint8_t i = 0; i < ESP_UUID_LEN_128; i++) { return memcmp(this->uuid_.uuid.uuid128, uuid.uuid_.uuid.uuid128, ESP_UUID_LEN_128) == 0;
if (uuid.uuid_.uuid.uuid128[i] != this->uuid_.uuid.uuid128[i]) { default:
return false; return false;
}
}
return true;
break;
} }
} else {
return this->as_128bit() == uuid.as_128bit();
} }
return false; return this->as_128bit() == uuid.as_128bit();
} }
esp_bt_uuid_t ESPBTUUID::get_uuid() const { return this->uuid_; } esp_bt_uuid_t ESPBTUUID::get_uuid() const { return this->uuid_; }
std::string ESPBTUUID::to_string() const { std::string ESPBTUUID::to_string() const {

View File

@@ -14,10 +14,6 @@
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#ifdef USE_ARDUINO
#include <esp32-hal-bt.h>
#endif
namespace esphome { namespace esphome {
namespace esp32_ble_beacon { namespace esp32_ble_beacon {

View File

@@ -125,69 +125,49 @@ bool BLECharacteristic::is_created() {
if (this->state_ != CREATING_DEPENDENTS) if (this->state_ != CREATING_DEPENDENTS)
return false; return false;
bool created = true;
for (auto *descriptor : this->descriptors_) { for (auto *descriptor : this->descriptors_) {
created &= descriptor->is_created(); if (!descriptor->is_created())
return false;
} }
if (created) // All descriptors are created if we reach here
this->state_ = CREATED; this->state_ = CREATED;
return this->state_ == CREATED; return true;
} }
bool BLECharacteristic::is_failed() { bool BLECharacteristic::is_failed() {
if (this->state_ == FAILED) if (this->state_ == FAILED)
return true; return true;
bool failed = false;
for (auto *descriptor : this->descriptors_) { for (auto *descriptor : this->descriptors_) {
failed |= descriptor->is_failed(); if (descriptor->is_failed()) {
this->state_ = FAILED;
return true;
}
}
return false;
}
void BLECharacteristic::set_property_bit_(esp_gatt_char_prop_t bit, bool value) {
if (value) {
this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | bit);
} else {
this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~bit);
} }
if (failed)
this->state_ = FAILED;
return this->state_ == FAILED;
} }
void BLECharacteristic::set_broadcast_property(bool value) { void BLECharacteristic::set_broadcast_property(bool value) {
if (value) { this->set_property_bit_(ESP_GATT_CHAR_PROP_BIT_BROADCAST, value);
this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_BROADCAST);
} else {
this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_BROADCAST);
}
} }
void BLECharacteristic::set_indicate_property(bool value) { void BLECharacteristic::set_indicate_property(bool value) {
if (value) { this->set_property_bit_(ESP_GATT_CHAR_PROP_BIT_INDICATE, value);
this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_INDICATE);
} else {
this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_INDICATE);
}
} }
void BLECharacteristic::set_notify_property(bool value) { void BLECharacteristic::set_notify_property(bool value) {
if (value) { this->set_property_bit_(ESP_GATT_CHAR_PROP_BIT_NOTIFY, value);
this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_NOTIFY);
} else {
this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_NOTIFY);
}
}
void BLECharacteristic::set_read_property(bool value) {
if (value) {
this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_READ);
} else {
this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_READ);
}
}
void BLECharacteristic::set_write_property(bool value) {
if (value) {
this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_WRITE);
} else {
this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_WRITE);
}
} }
void BLECharacteristic::set_read_property(bool value) { this->set_property_bit_(ESP_GATT_CHAR_PROP_BIT_READ, value); }
void BLECharacteristic::set_write_property(bool value) { this->set_property_bit_(ESP_GATT_CHAR_PROP_BIT_WRITE, value); }
void BLECharacteristic::set_write_no_response_property(bool value) { void BLECharacteristic::set_write_no_response_property(bool value) {
if (value) { this->set_property_bit_(ESP_GATT_CHAR_PROP_BIT_WRITE_NR, value);
this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_WRITE_NR);
} else {
this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_WRITE_NR);
}
} }
void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,

View File

@@ -97,6 +97,8 @@ class BLECharacteristic {
void remove_client_from_notify_list_(uint16_t conn_id); void remove_client_from_notify_list_(uint16_t conn_id);
ClientNotificationEntry *find_client_in_notify_list_(uint16_t conn_id); ClientNotificationEntry *find_client_in_notify_list_(uint16_t conn_id);
void set_property_bit_(esp_gatt_char_prop_t bit, bool value);
std::unique_ptr<std::function<void(std::span<const uint8_t>, uint16_t)>> on_write_callback_; std::unique_ptr<std::function<void(std::span<const uint8_t>, uint16_t)>> on_write_callback_;
std::unique_ptr<std::function<void(uint16_t)>> on_read_callback_; std::unique_ptr<std::function<void(uint16_t)>> on_read_callback_;

View File

@@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass
import logging import logging
from esphome import automation from esphome import automation
@@ -52,9 +53,19 @@ class BLEFeatures(StrEnum):
ESP_BT_DEVICE = "ESP_BT_DEVICE" ESP_BT_DEVICE = "ESP_BT_DEVICE"
# Dataclass for registration counts
@dataclass
class RegistrationCounts:
listeners: int = 0
clients: int = 0
# Set to track which features are needed by components # Set to track which features are needed by components
_required_features: set[BLEFeatures] = set() _required_features: set[BLEFeatures] = set()
# Track registration counts for StaticVector sizing
_registration_counts = RegistrationCounts()
def register_ble_features(features: set[BLEFeatures]) -> None: def register_ble_features(features: set[BLEFeatures]) -> None:
"""Register BLE features that a component needs. """Register BLE features that a component needs.
@@ -257,12 +268,14 @@ async def to_code(config):
register_ble_features({BLEFeatures.ESP_BT_DEVICE}) register_ble_features({BLEFeatures.ESP_BT_DEVICE})
for conf in config.get(CONF_ON_BLE_ADVERTISE, []): for conf in config.get(CONF_ON_BLE_ADVERTISE, []):
_registration_counts.listeners += 1
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
if CONF_MAC_ADDRESS in conf: if CONF_MAC_ADDRESS in conf:
addr_list = [it.as_hex for it in conf[CONF_MAC_ADDRESS]] addr_list = [it.as_hex for it in conf[CONF_MAC_ADDRESS]]
cg.add(trigger.set_addresses(addr_list)) cg.add(trigger.set_addresses(addr_list))
await automation.build_automation(trigger, [(ESPBTDeviceConstRef, "x")], conf) await automation.build_automation(trigger, [(ESPBTDeviceConstRef, "x")], conf)
for conf in config.get(CONF_ON_BLE_SERVICE_DATA_ADVERTISE, []): for conf in config.get(CONF_ON_BLE_SERVICE_DATA_ADVERTISE, []):
_registration_counts.listeners += 1
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
if len(conf[CONF_SERVICE_UUID]) == len(bt_uuid16_format): if len(conf[CONF_SERVICE_UUID]) == len(bt_uuid16_format):
cg.add(trigger.set_service_uuid16(as_hex(conf[CONF_SERVICE_UUID]))) cg.add(trigger.set_service_uuid16(as_hex(conf[CONF_SERVICE_UUID])))
@@ -275,6 +288,7 @@ async def to_code(config):
cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex)) cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex))
await automation.build_automation(trigger, [(adv_data_t_const_ref, "x")], conf) await automation.build_automation(trigger, [(adv_data_t_const_ref, "x")], conf)
for conf in config.get(CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE, []): for conf in config.get(CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE, []):
_registration_counts.listeners += 1
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
if len(conf[CONF_MANUFACTURER_ID]) == len(bt_uuid16_format): if len(conf[CONF_MANUFACTURER_ID]) == len(bt_uuid16_format):
cg.add(trigger.set_manufacturer_uuid16(as_hex(conf[CONF_MANUFACTURER_ID]))) cg.add(trigger.set_manufacturer_uuid16(as_hex(conf[CONF_MANUFACTURER_ID])))
@@ -287,6 +301,7 @@ async def to_code(config):
cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex)) cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex))
await automation.build_automation(trigger, [(adv_data_t_const_ref, "x")], conf) await automation.build_automation(trigger, [(adv_data_t_const_ref, "x")], conf)
for conf in config.get(CONF_ON_SCAN_END, []): for conf in config.get(CONF_ON_SCAN_END, []):
_registration_counts.listeners += 1
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf) await automation.build_automation(trigger, [], conf)
@@ -320,6 +335,17 @@ async def _add_ble_features():
cg.add_define("USE_ESP32_BLE_DEVICE") cg.add_define("USE_ESP32_BLE_DEVICE")
cg.add_define("USE_ESP32_BLE_UUID") cg.add_define("USE_ESP32_BLE_UUID")
# Add defines for StaticVector sizing based on registration counts
# Only define if count > 0 to avoid allocating unnecessary memory
if _registration_counts.listeners > 0:
cg.add_define(
"ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT", _registration_counts.listeners
)
if _registration_counts.clients > 0:
cg.add_define(
"ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT", _registration_counts.clients
)
ESP32_BLE_START_SCAN_ACTION_SCHEMA = cv.Schema( ESP32_BLE_START_SCAN_ACTION_SCHEMA = cv.Schema(
{ {
@@ -369,6 +395,7 @@ async def register_ble_device(
var: cg.SafeExpType, config: ConfigType var: cg.SafeExpType, config: ConfigType
) -> cg.SafeExpType: ) -> cg.SafeExpType:
register_ble_features({BLEFeatures.ESP_BT_DEVICE}) register_ble_features({BLEFeatures.ESP_BT_DEVICE})
_registration_counts.listeners += 1
paren = await cg.get_variable(config[CONF_ESP32_BLE_ID]) paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
cg.add(paren.register_listener(var)) cg.add(paren.register_listener(var))
return var return var
@@ -376,6 +403,7 @@ async def register_ble_device(
async def register_client(var: cg.SafeExpType, config: ConfigType) -> cg.SafeExpType: async def register_client(var: cg.SafeExpType, config: ConfigType) -> cg.SafeExpType:
register_ble_features({BLEFeatures.ESP_BT_DEVICE}) register_ble_features({BLEFeatures.ESP_BT_DEVICE})
_registration_counts.clients += 1
paren = await cg.get_variable(config[CONF_ESP32_BLE_ID]) paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
cg.add(paren.register_client(var)) cg.add(paren.register_client(var))
return var return var
@@ -389,6 +417,7 @@ async def register_raw_ble_device(
This does NOT register the ESP_BT_DEVICE feature, meaning ESPBTDevice This does NOT register the ESP_BT_DEVICE feature, meaning ESPBTDevice
will not be compiled in if this is the only registration method used. will not be compiled in if this is the only registration method used.
""" """
_registration_counts.listeners += 1
paren = await cg.get_variable(config[CONF_ESP32_BLE_ID]) paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
cg.add(paren.register_listener(var)) cg.add(paren.register_listener(var))
return var return var
@@ -402,6 +431,7 @@ async def register_raw_client(
This does NOT register the ESP_BT_DEVICE feature, meaning ESPBTDevice This does NOT register the ESP_BT_DEVICE feature, meaning ESPBTDevice
will not be compiled in if this is the only registration method used. will not be compiled in if this is the only registration method used.
""" """
_registration_counts.clients += 1
paren = await cg.get_variable(config[CONF_ESP32_BLE_ID]) paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
cg.add(paren.register_client(var)) cg.add(paren.register_client(var))
return var return var

View File

@@ -25,10 +25,6 @@
#include <esp_coexist.h> #include <esp_coexist.h>
#endif #endif
#ifdef USE_ARDUINO
#include <esp32-hal-bt.h>
#endif
#define MBEDTLS_AES_ALT #define MBEDTLS_AES_ALT
#include <aes_alt.h> #include <aes_alt.h>
@@ -78,9 +74,11 @@ void ESP32BLETracker::setup() {
[this](ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) { [this](ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) {
if (state == ota::OTA_STARTED) { if (state == ota::OTA_STARTED) {
this->stop_scan(); this->stop_scan();
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
for (auto *client : this->clients_) { for (auto *client : this->clients_) {
client->disconnect(); client->disconnect();
} }
#endif
} }
}); });
#endif #endif
@@ -210,8 +208,10 @@ void ESP32BLETracker::start_scan_(bool first) {
this->set_scanner_state_(ScannerState::STARTING); this->set_scanner_state_(ScannerState::STARTING);
ESP_LOGD(TAG, "Starting scan, set scanner state to STARTING."); ESP_LOGD(TAG, "Starting scan, set scanner state to STARTING.");
if (!first) { if (!first) {
#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
for (auto *listener : this->listeners_) for (auto *listener : this->listeners_)
listener->on_scan_end(); listener->on_scan_end();
#endif
} }
#ifdef USE_ESP32_BLE_DEVICE #ifdef USE_ESP32_BLE_DEVICE
this->already_discovered_.clear(); this->already_discovered_.clear();
@@ -240,20 +240,25 @@ void ESP32BLETracker::start_scan_(bool first) {
} }
void ESP32BLETracker::register_client(ESPBTClient *client) { void ESP32BLETracker::register_client(ESPBTClient *client) {
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
client->app_id = ++this->app_id_; client->app_id = ++this->app_id_;
this->clients_.push_back(client); this->clients_.push_back(client);
this->recalculate_advertisement_parser_types(); this->recalculate_advertisement_parser_types();
#endif
} }
void ESP32BLETracker::register_listener(ESPBTDeviceListener *listener) { void ESP32BLETracker::register_listener(ESPBTDeviceListener *listener) {
#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
listener->set_parent(this); listener->set_parent(this);
this->listeners_.push_back(listener); this->listeners_.push_back(listener);
this->recalculate_advertisement_parser_types(); this->recalculate_advertisement_parser_types();
#endif
} }
void ESP32BLETracker::recalculate_advertisement_parser_types() { void ESP32BLETracker::recalculate_advertisement_parser_types() {
this->raw_advertisements_ = false; this->raw_advertisements_ = false;
this->parse_advertisements_ = false; this->parse_advertisements_ = false;
#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
for (auto *listener : this->listeners_) { for (auto *listener : this->listeners_) {
if (listener->get_advertisement_parser_type() == AdvertisementParserType::PARSED_ADVERTISEMENTS) { if (listener->get_advertisement_parser_type() == AdvertisementParserType::PARSED_ADVERTISEMENTS) {
this->parse_advertisements_ = true; this->parse_advertisements_ = true;
@@ -261,6 +266,8 @@ void ESP32BLETracker::recalculate_advertisement_parser_types() {
this->raw_advertisements_ = true; this->raw_advertisements_ = true;
} }
} }
#endif
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
for (auto *client : this->clients_) { for (auto *client : this->clients_) {
if (client->get_advertisement_parser_type() == AdvertisementParserType::PARSED_ADVERTISEMENTS) { if (client->get_advertisement_parser_type() == AdvertisementParserType::PARSED_ADVERTISEMENTS) {
this->parse_advertisements_ = true; this->parse_advertisements_ = true;
@@ -268,6 +275,7 @@ void ESP32BLETracker::recalculate_advertisement_parser_types() {
this->raw_advertisements_ = true; this->raw_advertisements_ = true;
} }
} }
#endif
} }
void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
@@ -286,10 +294,12 @@ void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_ga
default: default:
break; break;
} }
// Forward all events to clients (scan results are handled separately via gap_scan_event_handler) // Forward all events to clients (scan results are handled separately via gap_scan_event_handler)
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
for (auto *client : this->clients_) { for (auto *client : this->clients_) {
client->gap_event_handler(event, param); client->gap_event_handler(event, param);
} }
#endif
} }
void ESP32BLETracker::gap_scan_event_handler(const BLEScanResult &scan_result) { void ESP32BLETracker::gap_scan_event_handler(const BLEScanResult &scan_result) {
@@ -352,9 +362,11 @@ void ESP32BLETracker::gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_
void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) { esp_ble_gattc_cb_param_t *param) {
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
for (auto *client : this->clients_) { for (auto *client : this->clients_) {
client->gattc_event_handler(event, gattc_if, param); client->gattc_event_handler(event, gattc_if, param);
} }
#endif
} }
void ESP32BLETracker::set_scanner_state_(ScannerState state) { void ESP32BLETracker::set_scanner_state_(ScannerState state) {
@@ -708,12 +720,16 @@ bool ESPBTDevice::resolve_irk(const uint8_t *irk) const {
void ESP32BLETracker::process_scan_result_(const BLEScanResult &scan_result) { void ESP32BLETracker::process_scan_result_(const BLEScanResult &scan_result) {
// Process raw advertisements // Process raw advertisements
if (this->raw_advertisements_) { if (this->raw_advertisements_) {
#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
for (auto *listener : this->listeners_) { for (auto *listener : this->listeners_) {
listener->parse_devices(&scan_result, 1); listener->parse_devices(&scan_result, 1);
} }
#endif
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
for (auto *client : this->clients_) { for (auto *client : this->clients_) {
client->parse_devices(&scan_result, 1); client->parse_devices(&scan_result, 1);
} }
#endif
} }
// Process parsed advertisements // Process parsed advertisements
@@ -723,16 +739,20 @@ void ESP32BLETracker::process_scan_result_(const BLEScanResult &scan_result) {
device.parse_scan_rst(scan_result); device.parse_scan_rst(scan_result);
bool found = false; bool found = false;
#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
for (auto *listener : this->listeners_) { for (auto *listener : this->listeners_) {
if (listener->parse_device(device)) if (listener->parse_device(device))
found = true; found = true;
} }
#endif
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
for (auto *client : this->clients_) { for (auto *client : this->clients_) {
if (client->parse_device(device)) { if (client->parse_device(device)) {
found = true; found = true;
} }
} }
#endif
if (!found && !this->scan_continuous_) { if (!found && !this->scan_continuous_) {
this->print_bt_device_info(device); this->print_bt_device_info(device);
@@ -749,8 +769,10 @@ void ESP32BLETracker::cleanup_scan_state_(bool is_stop_complete) {
// Reset timeout state machine instead of cancelling scheduler timeout // Reset timeout state machine instead of cancelling scheduler timeout
this->scan_timeout_state_ = ScanTimeoutState::INACTIVE; this->scan_timeout_state_ = ScanTimeoutState::INACTIVE;
#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
for (auto *listener : this->listeners_) for (auto *listener : this->listeners_)
listener->on_scan_end(); listener->on_scan_end();
#endif
this->set_scanner_state_(ScannerState::IDLE); this->set_scanner_state_(ScannerState::IDLE);
} }
@@ -774,6 +796,7 @@ void ESP32BLETracker::handle_scanner_failure_() {
void ESP32BLETracker::try_promote_discovered_clients_() { void ESP32BLETracker::try_promote_discovered_clients_() {
// Only promote the first discovered client to avoid multiple simultaneous connections // Only promote the first discovered client to avoid multiple simultaneous connections
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
for (auto *client : this->clients_) { for (auto *client : this->clients_) {
if (client->state() != ClientState::DISCOVERED) { if (client->state() != ClientState::DISCOVERED) {
continue; continue;
@@ -795,6 +818,7 @@ void ESP32BLETracker::try_promote_discovered_clients_() {
client->connect(); client->connect();
break; break;
} }
#endif
} }
const char *ESP32BLETracker::scanner_state_to_string_(ScannerState state) const { const char *ESP32BLETracker::scanner_state_to_string_(ScannerState state) const {

View File

@@ -302,6 +302,7 @@ class ESP32BLETracker : public Component,
/// Count clients in each state /// Count clients in each state
ClientStateCounts count_client_states_() const { ClientStateCounts count_client_states_() const {
ClientStateCounts counts; ClientStateCounts counts;
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
for (auto *client : this->clients_) { for (auto *client : this->clients_) {
switch (client->state()) { switch (client->state()) {
case ClientState::DISCONNECTING: case ClientState::DISCONNECTING:
@@ -317,12 +318,17 @@ class ESP32BLETracker : public Component,
break; break;
} }
} }
#endif
return counts; return counts;
} }
// Group 1: Large objects (12+ bytes) - vectors and callback manager // Group 1: Large objects (12+ bytes) - vectors and callback manager
std::vector<ESPBTDeviceListener *> listeners_; #ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
std::vector<ESPBTClient *> clients_; StaticVector<ESPBTDeviceListener *, ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT> listeners_;
#endif
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
StaticVector<ESPBTClient *, ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT> clients_;
#endif
CallbackManager<void(ScannerState)> scanner_state_callbacks_; CallbackManager<void(ScannerState)> scanner_state_callbacks_;
#ifdef USE_ESP32_BLE_DEVICE #ifdef USE_ESP32_BLE_DEVICE
/// Vector of addresses that have already been printed in print_bt_device_info /// Vector of addresses that have already been printed in print_bt_device_info

View File

@@ -68,7 +68,7 @@ static bool get_bitrate(canbus::CanSpeed bitrate, twai_timing_config_t *t_config
bool ESP32Can::setup_internal() { bool ESP32Can::setup_internal() {
static int next_twai_ctrl_num = 0; static int next_twai_ctrl_num = 0;
if (next_twai_ctrl_num >= SOC_TWAI_CONTROLLER_NUM) { if (static_cast<unsigned>(next_twai_ctrl_num) >= SOC_TWAI_CONTROLLER_NUM) {
ESP_LOGW(TAG, "Maximum number of esp32_can components created already"); ESP_LOGW(TAG, "Maximum number of esp32_can components created already");
this->mark_failed(); this->mark_failed();
return false; return false;

View File

@@ -80,7 +80,7 @@ void FingerprintGrowComponent::setup() {
delay(20); // This delay guarantees the sensor will in fact be powered power. delay(20); // This delay guarantees the sensor will in fact be powered power.
if (this->check_password_()) { if (this->check_password_()) {
if (this->new_password_ != -1) { if (this->new_password_ != std::numeric_limits<uint32_t>::max()) {
if (this->set_password_()) if (this->set_password_())
return; return;
} else { } else {

View File

@@ -6,6 +6,7 @@
#include "esphome/components/binary_sensor/binary_sensor.h" #include "esphome/components/binary_sensor/binary_sensor.h"
#include "esphome/components/uart/uart.h" #include "esphome/components/uart/uart.h"
#include <limits>
#include <vector> #include <vector>
namespace esphome { namespace esphome {
@@ -177,7 +178,7 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic
uint8_t address_[4] = {0xFF, 0xFF, 0xFF, 0xFF}; uint8_t address_[4] = {0xFF, 0xFF, 0xFF, 0xFF};
uint16_t capacity_ = 64; uint16_t capacity_ = 64;
uint32_t password_ = 0x0; uint32_t password_ = 0x0;
uint32_t new_password_ = -1; uint32_t new_password_ = std::numeric_limits<uint32_t>::max();
GPIOPin *sensing_pin_{nullptr}; GPIOPin *sensing_pin_{nullptr};
GPIOPin *sensor_power_pin_{nullptr}; GPIOPin *sensor_power_pin_{nullptr};
uint8_t enrollment_image_ = 0; uint8_t enrollment_image_ = 0;

View File

@@ -179,7 +179,7 @@ void Graph::draw(Display *buff, uint16_t x_offset, uint16_t y_offset, Color colo
if (b) { if (b) {
int16_t y = (int16_t) roundf((this->height_ - 1) * (1.0 - v)) - thick / 2 + y_offset; int16_t y = (int16_t) roundf((this->height_ - 1) * (1.0 - v)) - thick / 2 + y_offset;
auto draw_pixel_at = [&buff, c, y_offset, this](int16_t x, int16_t y) { auto draw_pixel_at = [&buff, c, y_offset, this](int16_t x, int16_t y) {
if (y >= y_offset && y < y_offset + this->height_) if (y >= y_offset && static_cast<uint32_t>(y) < y_offset + this->height_)
buff->draw_pixel_at(x, y, c); buff->draw_pixel_at(x, y, c);
}; };
if (!continuous || !has_prev || !prev_b || (abs(y - prev_y) <= thick)) { if (!continuous || !has_prev || !prev_b || (abs(y - prev_y) <= thick)) {

View File

@@ -116,7 +116,7 @@ void GraphicalDisplayMenu::draw_menu_internal_(display::Display *display, const
int number_items_fit_to_screen = 0; int number_items_fit_to_screen = 0;
const int max_item_index = this->displayed_item_->items_size() - 1; const int max_item_index = this->displayed_item_->items_size() - 1;
for (size_t i = 0; i <= max_item_index; i++) { for (size_t i = 0; max_item_index >= 0 && i <= static_cast<size_t>(max_item_index); i++) {
const auto *item = this->displayed_item_->get_item(i); const auto *item = this->displayed_item_->get_item(i);
const bool selected = i == this->cursor_index_; const bool selected = i == this->cursor_index_;
const display::Rect item_dimensions = this->measure_item(display, item, bounds, selected); const display::Rect item_dimensions = this->measure_item(display, item, bounds, selected);
@@ -174,7 +174,8 @@ void GraphicalDisplayMenu::draw_menu_internal_(display::Display *display, const
display->filled_rectangle(bounds->x, bounds->y, max_width, total_height, this->background_color_); display->filled_rectangle(bounds->x, bounds->y, max_width, total_height, this->background_color_);
auto y_offset = bounds->y; auto y_offset = bounds->y;
for (size_t i = first_item_index; i <= last_item_index; i++) { for (size_t i = static_cast<size_t>(first_item_index);
last_item_index >= 0 && i <= static_cast<size_t>(last_item_index); i++) {
const auto *item = this->displayed_item_->get_item(i); const auto *item = this->displayed_item_->get_item(i);
const bool selected = i == this->cursor_index_; const bool selected = i == this->cursor_index_;
display::Rect dimensions = menu_dimensions[i]; display::Rect dimensions = menu_dimensions[i];

View File

@@ -213,7 +213,7 @@ haier_protocol::HandlerError HonClimate::status_handler_(haier_protocol::FrameTy
this->real_control_packet_size_); this->real_control_packet_size_);
this->status_message_callback_.call((const char *) data, data_size); this->status_message_callback_.call((const char *) data, data_size);
} else { } else {
ESP_LOGW(TAG, "Status packet too small: %d (should be >= %d)", data_size, this->real_control_packet_size_); ESP_LOGW(TAG, "Status packet too small: %zu (should be >= %zu)", data_size, this->real_control_packet_size_);
} }
switch (this->protocol_phase_) { switch (this->protocol_phase_) {
case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST: case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST:
@@ -827,7 +827,7 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
size_t expected_size = size_t expected_size =
2 + this->status_message_header_size_ + this->real_control_packet_size_ + this->real_sensors_packet_size_; 2 + this->status_message_header_size_ + this->real_control_packet_size_ + this->real_sensors_packet_size_;
if (size < expected_size) { if (size < expected_size) {
ESP_LOGW(TAG, "Unexpected message size %d (expexted >= %d)", size, expected_size); ESP_LOGW(TAG, "Unexpected message size %u (expexted >= %zu)", size, expected_size);
return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE; return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE;
} }
uint16_t subtype = (((uint16_t) packet_buffer[0]) << 8) + packet_buffer[1]; uint16_t subtype = (((uint16_t) packet_buffer[0]) << 8) + packet_buffer[1];

View File

@@ -178,7 +178,7 @@ class HonClimate : public HaierClimateBase {
int extra_control_packet_bytes_{0}; int extra_control_packet_bytes_{0};
int extra_sensors_packet_bytes_{4}; int extra_sensors_packet_bytes_{4};
int status_message_header_size_{0}; int status_message_header_size_{0};
int real_control_packet_size_{sizeof(hon_protocol::HaierPacketControl)}; size_t real_control_packet_size_{sizeof(hon_protocol::HaierPacketControl)};
int real_sensors_packet_size_{sizeof(hon_protocol::HaierPacketSensors) + 4}; int real_sensors_packet_size_{sizeof(hon_protocol::HaierPacketSensors) + 4};
HonControlMethod control_method_; HonControlMethod control_method_;
std::queue<haier_protocol::HaierMessage> control_messages_queue_; std::queue<haier_protocol::HaierMessage> control_messages_queue_;

View File

@@ -5,6 +5,7 @@ from esphome.components.const import CONF_REQUEST_HEADERS
from esphome.config_helpers import filter_source_files_from_platform from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_CAPTURE_RESPONSE,
CONF_ESP8266_DISABLE_SSL_SUPPORT, CONF_ESP8266_DISABLE_SSL_SUPPORT,
CONF_ID, CONF_ID,
CONF_METHOD, CONF_METHOD,
@@ -57,7 +58,6 @@ CONF_HEADERS = "headers"
CONF_COLLECT_HEADERS = "collect_headers" CONF_COLLECT_HEADERS = "collect_headers"
CONF_BODY = "body" CONF_BODY = "body"
CONF_JSON = "json" CONF_JSON = "json"
CONF_CAPTURE_RESPONSE = "capture_response"
def validate_url(value): def validate_url(value):

View File

@@ -377,7 +377,7 @@ void I2SAudioSpeaker::speaker_task(void *params) {
this_speaker->current_stream_info_.get_bits_per_sample() <= 16) { this_speaker->current_stream_info_.get_bits_per_sample() <= 16) {
size_t len = bytes_read / sizeof(int16_t); size_t len = bytes_read / sizeof(int16_t);
int16_t *tmp_buf = (int16_t *) new_data; int16_t *tmp_buf = (int16_t *) new_data;
for (int i = 0; i < len; i += 2) { for (size_t i = 0; i < len; i += 2) {
int16_t tmp = tmp_buf[i]; int16_t tmp = tmp_buf[i];
tmp_buf[i] = tmp_buf[i + 1]; tmp_buf[i] = tmp_buf[i + 1];
tmp_buf[i + 1] = tmp; tmp_buf[i + 1] = tmp;

View File

@@ -325,7 +325,7 @@ void ILI9XXXDisplay::draw_pixels_at(int x_start, int y_start, int w, int h, cons
// we could deal here with a non-zero y_offset, but if x_offset is zero, y_offset probably will be so don't bother // we could deal here with a non-zero y_offset, but if x_offset is zero, y_offset probably will be so don't bother
this->write_array(ptr, w * h * 2); this->write_array(ptr, w * h * 2);
} else { } else {
for (size_t y = 0; y != h; y++) { for (size_t y = 0; y != static_cast<size_t>(h); y++) {
this->write_array(ptr + (y + y_offset) * stride + x_offset, w * 2); this->write_array(ptr + (y + y_offset) * stride + x_offset, w * 2);
} }
} }
@@ -349,7 +349,7 @@ void ILI9XXXDisplay::draw_pixels_at(int x_start, int y_start, int w, int h, cons
App.feed_wdt(); App.feed_wdt();
} }
// end of line? Skip to the next. // end of line? Skip to the next.
if (++pixel == w) { if (++pixel == static_cast<size_t>(w)) {
pixel = 0; pixel = 0;
ptr += (x_pad + x_offset) * 2; ptr += (x_pad + x_offset) * 2;
} }

View File

@@ -8,6 +8,13 @@ namespace json {
static const char *const TAG = "json"; static const char *const TAG = "json";
#ifdef USE_PSRAM
// Global allocator that outlives all JsonDocuments returned by parse_json()
// This prevents dangling pointer issues when JsonDocuments are returned from functions
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) - Must be mutable for ArduinoJson::Allocator
static SpiRamAllocator global_json_allocator;
#endif
std::string build_json(const json_build_t &f) { std::string build_json(const json_build_t &f) {
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
JsonBuilder builder; JsonBuilder builder;
@@ -19,18 +26,21 @@ std::string build_json(const json_build_t &f) {
bool parse_json(const std::string &data, const json_parse_t &f) { bool parse_json(const std::string &data, const json_parse_t &f) {
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
JsonDocument doc = parse_json(data); JsonDocument doc = parse_json(reinterpret_cast<const uint8_t *>(data.c_str()), data.size());
if (doc.overflowed() || doc.isNull()) if (doc.overflowed() || doc.isNull())
return false; return false;
return f(doc.as<JsonObject>()); return f(doc.as<JsonObject>());
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
} }
JsonDocument parse_json(const std::string &data) { JsonDocument parse_json(const uint8_t *data, size_t len) {
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
if (data == nullptr || len == 0) {
ESP_LOGE(TAG, "No data to parse");
return JsonObject(); // return unbound object
}
#ifdef USE_PSRAM #ifdef USE_PSRAM
auto doc_allocator = SpiRamAllocator(); JsonDocument json_document(&global_json_allocator);
JsonDocument json_document(&doc_allocator);
#else #else
JsonDocument json_document; JsonDocument json_document;
#endif #endif
@@ -38,7 +48,7 @@ JsonDocument parse_json(const std::string &data) {
ESP_LOGE(TAG, "Could not allocate memory for JSON document!"); ESP_LOGE(TAG, "Could not allocate memory for JSON document!");
return JsonObject(); // return unbound object return JsonObject(); // return unbound object
} }
DeserializationError err = deserializeJson(json_document, data); DeserializationError err = deserializeJson(json_document, data, len);
if (err == DeserializationError::Ok) { if (err == DeserializationError::Ok) {
return json_document; return json_document;

View File

@@ -50,8 +50,13 @@ std::string build_json(const json_build_t &f);
/// Parse a JSON string and run the provided json parse function if it's valid. /// Parse a JSON string and run the provided json parse function if it's valid.
bool parse_json(const std::string &data, const json_parse_t &f); bool parse_json(const std::string &data, const json_parse_t &f);
/// Parse a JSON string and return the root JsonDocument (or an unbound object on error) /// Parse a JSON string and return the root JsonDocument (or an unbound object on error)
JsonDocument parse_json(const std::string &data); JsonDocument parse_json(const uint8_t *data, size_t len);
/// Parse a JSON string and return the root JsonDocument (or an unbound object on error)
inline JsonDocument parse_json(const std::string &data) {
return parse_json(reinterpret_cast<const uint8_t *>(data.c_str()), data.size());
}
/// Builder class for creating JSON documents without lambdas /// Builder class for creating JSON documents without lambdas
class JsonBuilder { class JsonBuilder {

View File

@@ -22,7 +22,7 @@ void KamstrupKMPComponent::dump_config() {
LOG_SENSOR(" ", "Flow", this->flow_sensor_); LOG_SENSOR(" ", "Flow", this->flow_sensor_);
LOG_SENSOR(" ", "Volume", this->volume_sensor_); LOG_SENSOR(" ", "Volume", this->volume_sensor_);
for (int i = 0; i < this->custom_sensors_.size(); i++) { for (size_t i = 0; i < this->custom_sensors_.size(); i++) {
LOG_SENSOR(" ", "Custom Sensor", this->custom_sensors_[i]); LOG_SENSOR(" ", "Custom Sensor", this->custom_sensors_[i]);
ESP_LOGCONFIG(TAG, " Command: 0x%04X", this->custom_commands_[i]); ESP_LOGCONFIG(TAG, " Command: 0x%04X", this->custom_commands_[i]);
} }
@@ -268,7 +268,7 @@ void KamstrupKMPComponent::set_sensor_value_(uint16_t command, float value, uint
} }
// Custom sensors // Custom sensors
for (int i = 0; i < this->custom_commands_.size(); i++) { for (size_t i = 0; i < this->custom_commands_.size(); i++) {
if (command == this->custom_commands_[i]) { if (command == this->custom_commands_[i]) {
this->custom_sensors_[i]->publish_state(value); this->custom_sensors_[i]->publish_state(value);
} }

View File

@@ -13,8 +13,8 @@ class KeyCollector : public Component {
void loop() override; void loop() override;
void dump_config() override; void dump_config() override;
void set_provider(key_provider::KeyProvider *provider); void set_provider(key_provider::KeyProvider *provider);
void set_min_length(int min_length) { this->min_length_ = min_length; }; void set_min_length(uint32_t min_length) { this->min_length_ = min_length; };
void set_max_length(int max_length) { this->max_length_ = max_length; }; void set_max_length(uint32_t max_length) { this->max_length_ = max_length; };
void set_start_keys(std::string start_keys) { this->start_keys_ = std::move(start_keys); }; void set_start_keys(std::string start_keys) { this->start_keys_ = std::move(start_keys); };
void set_end_keys(std::string end_keys) { this->end_keys_ = std::move(end_keys); }; void set_end_keys(std::string end_keys) { this->end_keys_ = std::move(end_keys); };
void set_end_key_required(bool end_key_required) { this->end_key_required_ = end_key_required; }; void set_end_key_required(bool end_key_required) { this->end_key_required_ = end_key_required; };
@@ -33,8 +33,8 @@ class KeyCollector : public Component {
protected: protected:
void key_pressed_(uint8_t key); void key_pressed_(uint8_t key);
int min_length_{0}; uint32_t min_length_{0};
int max_length_{0}; uint32_t max_length_{0};
std::string start_keys_; std::string start_keys_;
std::string end_keys_; std::string end_keys_;
bool end_key_required_{false}; bool end_key_required_{false};

View File

@@ -10,11 +10,15 @@ namespace light {
static const char *const TAG = "light"; static const char *const TAG = "light";
// Helper functions to reduce code size for logging // Helper functions to reduce code size for logging
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_WARN static void clamp_and_log_if_invalid(const char *name, float &value, const LogString *param_name, float min = 0.0f,
static void log_validation_warning(const char *name, const LogString *param_name, float val, float min, float max) { float max = 1.0f) {
ESP_LOGW(TAG, "'%s': %s value %.2f is out of range [%.1f - %.1f]", name, LOG_STR_ARG(param_name), val, min, max); if (value < min || value > max) {
ESP_LOGW(TAG, "'%s': %s value %.2f is out of range [%.1f - %.1f]", name, LOG_STR_ARG(param_name), value, min, max);
value = clamp(value, min, max);
}
} }
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_WARN
static void log_feature_not_supported(const char *name, const LogString *feature) { static void log_feature_not_supported(const char *name, const LogString *feature) {
ESP_LOGW(TAG, "'%s': %s not supported", name, LOG_STR_ARG(feature)); ESP_LOGW(TAG, "'%s': %s not supported", name, LOG_STR_ARG(feature));
} }
@@ -27,7 +31,6 @@ static void log_invalid_parameter(const char *name, const LogString *message) {
ESP_LOGW(TAG, "'%s': %s", name, LOG_STR_ARG(message)); ESP_LOGW(TAG, "'%s': %s", name, LOG_STR_ARG(message));
} }
#else #else
#define log_validation_warning(name, param_name, val, min, max)
#define log_feature_not_supported(name, feature) #define log_feature_not_supported(name, feature)
#define log_color_mode_not_supported(name, feature) #define log_color_mode_not_supported(name, feature)
#define log_invalid_parameter(name, message) #define log_invalid_parameter(name, message)
@@ -44,7 +47,7 @@ static void log_invalid_parameter(const char *name, const LogString *message) {
} \ } \
LightCall &LightCall::set_##name(type name) { \ LightCall &LightCall::set_##name(type name) { \
this->name##_ = name; \ this->name##_ = name; \
this->set_flag_(flag, true); \ this->set_flag_(flag); \
return *this; \ return *this; \
} }
@@ -181,6 +184,16 @@ void LightCall::perform() {
} }
} }
void LightCall::log_and_clear_unsupported_(FieldFlags flag, const LogString *feature, bool use_color_mode_log) {
auto *name = this->parent_->get_name().c_str();
if (use_color_mode_log) {
log_color_mode_not_supported(name, feature);
} else {
log_feature_not_supported(name, feature);
}
this->clear_flag_(flag);
}
LightColorValues LightCall::validate_() { LightColorValues LightCall::validate_() {
auto *name = this->parent_->get_name().c_str(); auto *name = this->parent_->get_name().c_str();
auto traits = this->parent_->get_traits(); auto traits = this->parent_->get_traits();
@@ -188,141 +201,108 @@ LightColorValues LightCall::validate_() {
// Color mode check // Color mode check
if (this->has_color_mode() && !traits.supports_color_mode(this->color_mode_)) { if (this->has_color_mode() && !traits.supports_color_mode(this->color_mode_)) {
ESP_LOGW(TAG, "'%s' does not support color mode %s", name, LOG_STR_ARG(color_mode_to_human(this->color_mode_))); ESP_LOGW(TAG, "'%s' does not support color mode %s", name, LOG_STR_ARG(color_mode_to_human(this->color_mode_)));
this->set_flag_(FLAG_HAS_COLOR_MODE, false); this->clear_flag_(FLAG_HAS_COLOR_MODE);
} }
// Ensure there is always a color mode set // Ensure there is always a color mode set
if (!this->has_color_mode()) { if (!this->has_color_mode()) {
this->color_mode_ = this->compute_color_mode_(); this->color_mode_ = this->compute_color_mode_();
this->set_flag_(FLAG_HAS_COLOR_MODE, true); this->set_flag_(FLAG_HAS_COLOR_MODE);
} }
auto color_mode = this->color_mode_; auto color_mode = this->color_mode_;
// Transform calls that use non-native parameters for the current mode. // Transform calls that use non-native parameters for the current mode.
this->transform_parameters_(); this->transform_parameters_();
// Brightness exists check // Business logic adjustments before validation
if (this->has_brightness() && this->brightness_ > 0.0f && !(color_mode & ColorCapability::BRIGHTNESS)) {
log_feature_not_supported(name, LOG_STR("brightness"));
this->set_flag_(FLAG_HAS_BRIGHTNESS, false);
}
// Transition length possible check
if (this->has_transition_() && this->transition_length_ != 0 && !(color_mode & ColorCapability::BRIGHTNESS)) {
log_feature_not_supported(name, LOG_STR("transitions"));
this->set_flag_(FLAG_HAS_TRANSITION, false);
}
// Color brightness exists check
if (this->has_color_brightness() && this->color_brightness_ > 0.0f && !(color_mode & ColorCapability::RGB)) {
log_color_mode_not_supported(name, LOG_STR("RGB brightness"));
this->set_flag_(FLAG_HAS_COLOR_BRIGHTNESS, false);
}
// RGB exists check
if ((this->has_red() && this->red_ > 0.0f) || (this->has_green() && this->green_ > 0.0f) ||
(this->has_blue() && this->blue_ > 0.0f)) {
if (!(color_mode & ColorCapability::RGB)) {
log_color_mode_not_supported(name, LOG_STR("RGB color"));
this->set_flag_(FLAG_HAS_RED, false);
this->set_flag_(FLAG_HAS_GREEN, false);
this->set_flag_(FLAG_HAS_BLUE, false);
}
}
// White value exists check
if (this->has_white() && this->white_ > 0.0f &&
!(color_mode & ColorCapability::WHITE || color_mode & ColorCapability::COLD_WARM_WHITE)) {
log_color_mode_not_supported(name, LOG_STR("white value"));
this->set_flag_(FLAG_HAS_WHITE, false);
}
// Color temperature exists check
if (this->has_color_temperature() &&
!(color_mode & ColorCapability::COLOR_TEMPERATURE || color_mode & ColorCapability::COLD_WARM_WHITE)) {
log_color_mode_not_supported(name, LOG_STR("color temperature"));
this->set_flag_(FLAG_HAS_COLOR_TEMPERATURE, false);
}
// Cold/warm white value exists check
if ((this->has_cold_white() && this->cold_white_ > 0.0f) || (this->has_warm_white() && this->warm_white_ > 0.0f)) {
if (!(color_mode & ColorCapability::COLD_WARM_WHITE)) {
log_color_mode_not_supported(name, LOG_STR("cold/warm white value"));
this->set_flag_(FLAG_HAS_COLD_WHITE, false);
this->set_flag_(FLAG_HAS_WARM_WHITE, false);
}
}
#define VALIDATE_RANGE_(name_, upper_name, min, max) \
if (this->has_##name_()) { \
auto val = this->name_##_; \
if (val < (min) || val > (max)) { \
log_validation_warning(name, LOG_STR(upper_name), val, (min), (max)); \
this->name_##_ = clamp(val, (min), (max)); \
} \
}
#define VALIDATE_RANGE(name, upper_name) VALIDATE_RANGE_(name, upper_name, 0.0f, 1.0f)
// Range checks
VALIDATE_RANGE(brightness, "Brightness")
VALIDATE_RANGE(color_brightness, "Color brightness")
VALIDATE_RANGE(red, "Red")
VALIDATE_RANGE(green, "Green")
VALIDATE_RANGE(blue, "Blue")
VALIDATE_RANGE(white, "White")
VALIDATE_RANGE(cold_white, "Cold white")
VALIDATE_RANGE(warm_white, "Warm white")
VALIDATE_RANGE_(color_temperature, "Color temperature", traits.get_min_mireds(), traits.get_max_mireds())
// Flag whether an explicit turn off was requested, in which case we'll also stop the effect. // Flag whether an explicit turn off was requested, in which case we'll also stop the effect.
bool explicit_turn_off_request = this->has_state() && !this->state_; bool explicit_turn_off_request = this->has_state() && !this->state_;
// Turn off when brightness is set to zero, and reset brightness (so that it has nonzero brightness when turned on). // Turn off when brightness is set to zero, and reset brightness (so that it has nonzero brightness when turned on).
if (this->has_brightness() && this->brightness_ == 0.0f) { if (this->has_brightness() && this->brightness_ == 0.0f) {
this->state_ = false; this->state_ = false;
this->set_flag_(FLAG_HAS_STATE, true); this->set_flag_(FLAG_HAS_STATE);
this->brightness_ = 1.0f; this->brightness_ = 1.0f;
} }
// Set color brightness to 100% if currently zero and a color is set. // Set color brightness to 100% if currently zero and a color is set.
if (this->has_red() || this->has_green() || this->has_blue()) { if ((this->has_red() || this->has_green() || this->has_blue()) && !this->has_color_brightness() &&
if (!this->has_color_brightness() && this->parent_->remote_values.get_color_brightness() == 0.0f) { this->parent_->remote_values.get_color_brightness() == 0.0f) {
this->color_brightness_ = 1.0f; this->color_brightness_ = 1.0f;
this->set_flag_(FLAG_HAS_COLOR_BRIGHTNESS, true); this->set_flag_(FLAG_HAS_COLOR_BRIGHTNESS);
}
} }
// Create color values for the light with this call applied. // Capability validation
if (this->has_brightness() && this->brightness_ > 0.0f && !(color_mode & ColorCapability::BRIGHTNESS))
this->log_and_clear_unsupported_(FLAG_HAS_BRIGHTNESS, LOG_STR("brightness"), false);
// Transition length possible check
if (this->has_transition_() && this->transition_length_ != 0 && !(color_mode & ColorCapability::BRIGHTNESS))
this->log_and_clear_unsupported_(FLAG_HAS_TRANSITION, LOG_STR("transitions"), false);
if (this->has_color_brightness() && this->color_brightness_ > 0.0f && !(color_mode & ColorCapability::RGB))
this->log_and_clear_unsupported_(FLAG_HAS_COLOR_BRIGHTNESS, LOG_STR("RGB brightness"), true);
// RGB exists check
if (((this->has_red() && this->red_ > 0.0f) || (this->has_green() && this->green_ > 0.0f) ||
(this->has_blue() && this->blue_ > 0.0f)) &&
!(color_mode & ColorCapability::RGB)) {
log_color_mode_not_supported(name, LOG_STR("RGB color"));
this->clear_flag_(FLAG_HAS_RED);
this->clear_flag_(FLAG_HAS_GREEN);
this->clear_flag_(FLAG_HAS_BLUE);
}
// White value exists check
if (this->has_white() && this->white_ > 0.0f &&
!(color_mode & ColorCapability::WHITE || color_mode & ColorCapability::COLD_WARM_WHITE))
this->log_and_clear_unsupported_(FLAG_HAS_WHITE, LOG_STR("white value"), true);
// Color temperature exists check
if (this->has_color_temperature() &&
!(color_mode & ColorCapability::COLOR_TEMPERATURE || color_mode & ColorCapability::COLD_WARM_WHITE))
this->log_and_clear_unsupported_(FLAG_HAS_COLOR_TEMPERATURE, LOG_STR("color temperature"), true);
// Cold/warm white value exists check
if (((this->has_cold_white() && this->cold_white_ > 0.0f) || (this->has_warm_white() && this->warm_white_ > 0.0f)) &&
!(color_mode & ColorCapability::COLD_WARM_WHITE)) {
log_color_mode_not_supported(name, LOG_STR("cold/warm white value"));
this->clear_flag_(FLAG_HAS_COLD_WHITE);
this->clear_flag_(FLAG_HAS_WARM_WHITE);
}
// Create color values and validate+apply ranges in one step to eliminate duplicate checks
auto v = this->parent_->remote_values; auto v = this->parent_->remote_values;
if (this->has_color_mode()) if (this->has_color_mode())
v.set_color_mode(this->color_mode_); v.set_color_mode(this->color_mode_);
if (this->has_state()) if (this->has_state())
v.set_state(this->state_); v.set_state(this->state_);
if (this->has_brightness())
v.set_brightness(this->brightness_); #define VALIDATE_AND_APPLY(field, setter, name_str, ...) \
if (this->has_color_brightness()) if (this->has_##field()) { \
v.set_color_brightness(this->color_brightness_); clamp_and_log_if_invalid(name, this->field##_, LOG_STR(name_str), ##__VA_ARGS__); \
if (this->has_red()) v.setter(this->field##_); \
v.set_red(this->red_); }
if (this->has_green())
v.set_green(this->green_); VALIDATE_AND_APPLY(brightness, set_brightness, "Brightness")
if (this->has_blue()) VALIDATE_AND_APPLY(color_brightness, set_color_brightness, "Color brightness")
v.set_blue(this->blue_); VALIDATE_AND_APPLY(red, set_red, "Red")
if (this->has_white()) VALIDATE_AND_APPLY(green, set_green, "Green")
v.set_white(this->white_); VALIDATE_AND_APPLY(blue, set_blue, "Blue")
if (this->has_color_temperature()) VALIDATE_AND_APPLY(white, set_white, "White")
v.set_color_temperature(this->color_temperature_); VALIDATE_AND_APPLY(cold_white, set_cold_white, "Cold white")
if (this->has_cold_white()) VALIDATE_AND_APPLY(warm_white, set_warm_white, "Warm white")
v.set_cold_white(this->cold_white_); VALIDATE_AND_APPLY(color_temperature, set_color_temperature, "Color temperature", traits.get_min_mireds(),
if (this->has_warm_white()) traits.get_max_mireds())
v.set_warm_white(this->warm_white_);
#undef VALIDATE_AND_APPLY
v.normalize_color(); v.normalize_color();
// Flash length check // Flash length check
if (this->has_flash_() && this->flash_length_ == 0) { if (this->has_flash_() && this->flash_length_ == 0) {
log_invalid_parameter(name, LOG_STR("flash length must be greater than zero")); log_invalid_parameter(name, LOG_STR("flash length must be >0"));
this->set_flag_(FLAG_HAS_FLASH, false); this->clear_flag_(FLAG_HAS_FLASH);
} }
// validate transition length/flash length/effect not used at the same time // validate transition length/flash length/effect not used at the same time
@@ -330,42 +310,40 @@ LightColorValues LightCall::validate_() {
// If effect is already active, remove effect start // If effect is already active, remove effect start
if (this->has_effect_() && this->effect_ == this->parent_->active_effect_index_) { if (this->has_effect_() && this->effect_ == this->parent_->active_effect_index_) {
this->set_flag_(FLAG_HAS_EFFECT, false); this->clear_flag_(FLAG_HAS_EFFECT);
} }
// validate effect index // validate effect index
if (this->has_effect_() && this->effect_ > this->parent_->effects_.size()) { if (this->has_effect_() && this->effect_ > this->parent_->effects_.size()) {
ESP_LOGW(TAG, "'%s': invalid effect index %" PRIu32, name, this->effect_); ESP_LOGW(TAG, "'%s': invalid effect index %" PRIu32, name, this->effect_);
this->set_flag_(FLAG_HAS_EFFECT, false); this->clear_flag_(FLAG_HAS_EFFECT);
} }
if (this->has_effect_() && (this->has_transition_() || this->has_flash_())) { if (this->has_effect_() && (this->has_transition_() || this->has_flash_())) {
log_invalid_parameter(name, LOG_STR("effect cannot be used with transition/flash")); log_invalid_parameter(name, LOG_STR("effect cannot be used with transition/flash"));
this->set_flag_(FLAG_HAS_TRANSITION, false); this->clear_flag_(FLAG_HAS_TRANSITION);
this->set_flag_(FLAG_HAS_FLASH, false); this->clear_flag_(FLAG_HAS_FLASH);
} }
if (this->has_flash_() && this->has_transition_()) { if (this->has_flash_() && this->has_transition_()) {
log_invalid_parameter(name, LOG_STR("flash cannot be used with transition")); log_invalid_parameter(name, LOG_STR("flash cannot be used with transition"));
this->set_flag_(FLAG_HAS_TRANSITION, false); this->clear_flag_(FLAG_HAS_TRANSITION);
} }
if (!this->has_transition_() && !this->has_flash_() && (!this->has_effect_() || this->effect_ == 0) && if (!this->has_transition_() && !this->has_flash_() && (!this->has_effect_() || this->effect_ == 0) &&
supports_transition) { supports_transition) {
// nothing specified and light supports transitions, set default transition length // nothing specified and light supports transitions, set default transition length
this->transition_length_ = this->parent_->default_transition_length_; this->transition_length_ = this->parent_->default_transition_length_;
this->set_flag_(FLAG_HAS_TRANSITION, true); this->set_flag_(FLAG_HAS_TRANSITION);
} }
if (this->has_transition_() && this->transition_length_ == 0) { if (this->has_transition_() && this->transition_length_ == 0) {
// 0 transition is interpreted as no transition (instant change) // 0 transition is interpreted as no transition (instant change)
this->set_flag_(FLAG_HAS_TRANSITION, false); this->clear_flag_(FLAG_HAS_TRANSITION);
} }
if (this->has_transition_() && !supports_transition) { if (this->has_transition_() && !supports_transition)
log_feature_not_supported(name, LOG_STR("transitions")); this->log_and_clear_unsupported_(FLAG_HAS_TRANSITION, LOG_STR("transitions"), false);
this->set_flag_(FLAG_HAS_TRANSITION, false);
}
// If not a flash and turning the light off, then disable the light // If not a flash and turning the light off, then disable the light
// Do not use light color values directly, so that effects can set 0% brightness // Do not use light color values directly, so that effects can set 0% brightness
@@ -374,17 +352,17 @@ LightColorValues LightCall::validate_() {
if (!this->has_flash_() && !target_state) { if (!this->has_flash_() && !target_state) {
if (this->has_effect_()) { if (this->has_effect_()) {
log_invalid_parameter(name, LOG_STR("cannot start effect when turning off")); log_invalid_parameter(name, LOG_STR("cannot start effect when turning off"));
this->set_flag_(FLAG_HAS_EFFECT, false); this->clear_flag_(FLAG_HAS_EFFECT);
} else if (this->parent_->active_effect_index_ != 0 && explicit_turn_off_request) { } else if (this->parent_->active_effect_index_ != 0 && explicit_turn_off_request) {
// Auto turn off effect // Auto turn off effect
this->effect_ = 0; this->effect_ = 0;
this->set_flag_(FLAG_HAS_EFFECT, true); this->set_flag_(FLAG_HAS_EFFECT);
} }
} }
// Disable saving for flashes // Disable saving for flashes
if (this->has_flash_()) if (this->has_flash_())
this->set_flag_(FLAG_SAVE, false); this->clear_flag_(FLAG_SAVE);
return v; return v;
} }
@@ -418,12 +396,12 @@ void LightCall::transform_parameters_() {
const float gamma = this->parent_->get_gamma_correct(); const float gamma = this->parent_->get_gamma_correct();
this->cold_white_ = gamma_uncorrect(cw_fraction / max_cw_ww, gamma); this->cold_white_ = gamma_uncorrect(cw_fraction / max_cw_ww, gamma);
this->warm_white_ = gamma_uncorrect(ww_fraction / max_cw_ww, gamma); this->warm_white_ = gamma_uncorrect(ww_fraction / max_cw_ww, gamma);
this->set_flag_(FLAG_HAS_COLD_WHITE, true); this->set_flag_(FLAG_HAS_COLD_WHITE);
this->set_flag_(FLAG_HAS_WARM_WHITE, true); this->set_flag_(FLAG_HAS_WARM_WHITE);
} }
if (this->has_white()) { if (this->has_white()) {
this->brightness_ = this->white_; this->brightness_ = this->white_;
this->set_flag_(FLAG_HAS_BRIGHTNESS, true); this->set_flag_(FLAG_HAS_BRIGHTNESS);
} }
} }
} }
@@ -630,7 +608,7 @@ LightCall &LightCall::set_effect(optional<std::string> effect) {
} }
LightCall &LightCall::set_effect(uint32_t effect_number) { LightCall &LightCall::set_effect(uint32_t effect_number) {
this->effect_ = effect_number; this->effect_ = effect_number;
this->set_flag_(FLAG_HAS_EFFECT, true); this->set_flag_(FLAG_HAS_EFFECT);
return *this; return *this;
} }
LightCall &LightCall::set_effect(optional<uint32_t> effect_number) { LightCall &LightCall::set_effect(optional<uint32_t> effect_number) {

View File

@@ -4,6 +4,10 @@
#include <set> #include <set>
namespace esphome { namespace esphome {
// Forward declaration
struct LogString;
namespace light { namespace light {
class LightState; class LightState;
@@ -207,14 +211,14 @@ class LightCall {
FLAG_SAVE = 1 << 15, FLAG_SAVE = 1 << 15,
}; };
bool has_transition_() { return (this->flags_ & FLAG_HAS_TRANSITION) != 0; } inline bool has_transition_() { return (this->flags_ & FLAG_HAS_TRANSITION) != 0; }
bool has_flash_() { return (this->flags_ & FLAG_HAS_FLASH) != 0; } inline bool has_flash_() { return (this->flags_ & FLAG_HAS_FLASH) != 0; }
bool has_effect_() { return (this->flags_ & FLAG_HAS_EFFECT) != 0; } inline bool has_effect_() { return (this->flags_ & FLAG_HAS_EFFECT) != 0; }
bool get_publish_() { return (this->flags_ & FLAG_PUBLISH) != 0; } inline bool get_publish_() { return (this->flags_ & FLAG_PUBLISH) != 0; }
bool get_save_() { return (this->flags_ & FLAG_SAVE) != 0; } inline bool get_save_() { return (this->flags_ & FLAG_SAVE) != 0; }
// Helper to set flag // Helper to set flag - defaults to true for common case
void set_flag_(FieldFlags flag, bool value) { void set_flag_(FieldFlags flag, bool value = true) {
if (value) { if (value) {
this->flags_ |= flag; this->flags_ |= flag;
} else { } else {
@@ -222,6 +226,12 @@ class LightCall {
} }
} }
// Helper to clear flag - reduces code size for common case
void clear_flag_(FieldFlags flag) { this->flags_ &= ~flag; }
// Helper to log unsupported feature and clear flag - reduces code duplication
void log_and_clear_unsupported_(FieldFlags flag, const LogString *feature, bool use_color_mode_log);
LightState *parent_; LightState *parent_;
// Light state values - use flags_ to check if a value has been set. // Light state values - use flags_ to check if a value has been set.

View File

View File

@@ -0,0 +1,39 @@
#include "lm75b.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
namespace esphome {
namespace lm75b {
static const char *const TAG = "lm75b";
void LM75BComponent::dump_config() {
ESP_LOGCONFIG(TAG, "LM75B:");
LOG_I2C_DEVICE(this);
if (this->is_failed()) {
ESP_LOGE(TAG, "Setting up LM75B failed!");
}
LOG_UPDATE_INTERVAL(this);
LOG_SENSOR(" ", "Temperature", this);
}
void LM75BComponent::update() {
// Create a temporary buffer
uint8_t buff[2];
if (this->read_register(LM75B_REG_TEMPERATURE, buff, 2) != i2c::ERROR_OK) {
this->status_set_warning();
return;
}
// Obtain combined 16-bit value
int16_t raw_temperature = (buff[0] << 8) | buff[1];
// Read the 11-bit raw temperature value
raw_temperature >>= 5;
// Publish the temperature in °C
this->publish_state(raw_temperature * 0.125);
if (this->status_has_warning()) {
this->status_clear_warning();
}
}
} // namespace lm75b
} // namespace esphome

View File

@@ -0,0 +1,19 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome {
namespace lm75b {
static const uint8_t LM75B_REG_TEMPERATURE = 0x00;
class LM75BComponent : public PollingComponent, public i2c::I2CDevice, public sensor::Sensor {
public:
void dump_config() override;
void update() override;
};
} // namespace lm75b
} // namespace esphome

View File

@@ -0,0 +1,34 @@
import esphome.codegen as cg
from esphome.components import i2c, sensor
import esphome.config_validation as cv
from esphome.const import (
DEVICE_CLASS_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
)
CODEOWNERS = ["@beormund"]
DEPENDENCIES = ["i2c"]
lm75b_ns = cg.esphome_ns.namespace("lm75b")
LM75BComponent = lm75b_ns.class_(
"LM75BComponent", cg.PollingComponent, i2c.I2CDevice, sensor.Sensor
)
CONFIG_SCHEMA = (
sensor.sensor_schema(
LM75BComponent,
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=3,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x48))
)
async def to_code(config):
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)

View File

@@ -2,6 +2,7 @@
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include <limits>
using esphome::i2c::ErrorCode; using esphome::i2c::ErrorCode;
@@ -28,30 +29,30 @@ bool operator!=(const GainTimePair &lhs, const GainTimePair &rhs) {
template<typename T, size_t size> T get_next(const T (&array)[size], const T val) { template<typename T, size_t size> T get_next(const T (&array)[size], const T val) {
size_t i = 0; size_t i = 0;
size_t idx = -1; size_t idx = std::numeric_limits<size_t>::max();
while (idx == -1 && i < size) { while (idx == std::numeric_limits<size_t>::max() && i < size) {
if (array[i] == val) { if (array[i] == val) {
idx = i; idx = i;
break; break;
} }
i++; i++;
} }
if (idx == -1 || i + 1 >= size) if (idx == std::numeric_limits<size_t>::max() || i + 1 >= size)
return val; return val;
return array[i + 1]; return array[i + 1];
} }
template<typename T, size_t size> T get_prev(const T (&array)[size], const T val) { template<typename T, size_t size> T get_prev(const T (&array)[size], const T val) {
size_t i = size - 1; size_t i = size - 1;
size_t idx = -1; size_t idx = std::numeric_limits<size_t>::max();
while (idx == -1 && i > 0) { while (idx == std::numeric_limits<size_t>::max() && i > 0) {
if (array[i] == val) { if (array[i] == val) {
idx = i; idx = i;
break; break;
} }
i--; i--;
} }
if (idx == -1 || i == 0) if (idx == std::numeric_limits<size_t>::max() || i == 0)
return val; return val;
return array[i - 1]; return array[i - 1];
} }

View File

@@ -2,6 +2,7 @@
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include <limits>
using esphome::i2c::ErrorCode; using esphome::i2c::ErrorCode;
@@ -14,30 +15,30 @@ static const uint8_t MAX_TRIES = 5;
template<typename T, size_t size> T get_next(const T (&array)[size], const T val) { template<typename T, size_t size> T get_next(const T (&array)[size], const T val) {
size_t i = 0; size_t i = 0;
size_t idx = -1; size_t idx = std::numeric_limits<size_t>::max();
while (idx == -1 && i < size) { while (idx == std::numeric_limits<size_t>::max() && i < size) {
if (array[i] == val) { if (array[i] == val) {
idx = i; idx = i;
break; break;
} }
i++; i++;
} }
if (idx == -1 || i + 1 >= size) if (idx == std::numeric_limits<size_t>::max() || i + 1 >= size)
return val; return val;
return array[i + 1]; return array[i + 1];
} }
template<typename T, size_t size> T get_prev(const T (&array)[size], const T val) { template<typename T, size_t size> T get_prev(const T (&array)[size], const T val) {
size_t i = size - 1; size_t i = size - 1;
size_t idx = -1; size_t idx = std::numeric_limits<size_t>::max();
while (idx == -1 && i > 0) { while (idx == std::numeric_limits<size_t>::max() && i > 0) {
if (array[i] == val) { if (array[i] == val) {
idx = i; idx = i;
break; break;
} }
i--; i--;
} }
if (idx == -1 || i == 0) if (idx == std::numeric_limits<size_t>::max() || i == 0)
return val; return val;
return array[i - 1]; return array[i - 1];
} }

View File

@@ -29,9 +29,9 @@ class MatrixKeypad : public key_provider::KeyProvider, public Component {
void set_columns(std::vector<GPIOPin *> pins) { columns_ = std::move(pins); }; void set_columns(std::vector<GPIOPin *> pins) { columns_ = std::move(pins); };
void set_rows(std::vector<GPIOPin *> pins) { rows_ = std::move(pins); }; void set_rows(std::vector<GPIOPin *> pins) { rows_ = std::move(pins); };
void set_keys(std::string keys) { keys_ = std::move(keys); }; void set_keys(std::string keys) { keys_ = std::move(keys); };
void set_debounce_time(int debounce_time) { debounce_time_ = debounce_time; }; void set_debounce_time(uint32_t debounce_time) { debounce_time_ = debounce_time; };
void set_has_diodes(int has_diodes) { has_diodes_ = has_diodes; }; void set_has_diodes(bool has_diodes) { has_diodes_ = has_diodes; };
void set_has_pulldowns(int has_pulldowns) { has_pulldowns_ = has_pulldowns; }; void set_has_pulldowns(bool has_pulldowns) { has_pulldowns_ = has_pulldowns; };
void register_listener(MatrixKeypadListener *listener); void register_listener(MatrixKeypadListener *listener);
void register_key_trigger(MatrixKeyTrigger *trig); void register_key_trigger(MatrixKeyTrigger *trig);
@@ -40,7 +40,7 @@ class MatrixKeypad : public key_provider::KeyProvider, public Component {
std::vector<GPIOPin *> rows_; std::vector<GPIOPin *> rows_;
std::vector<GPIOPin *> columns_; std::vector<GPIOPin *> columns_;
std::string keys_; std::string keys_;
int debounce_time_ = 0; uint32_t debounce_time_ = 0;
bool has_diodes_{false}; bool has_diodes_{false};
bool has_pulldowns_{false}; bool has_pulldowns_{false};
int pressed_key_ = -1; int pressed_key_ = -1;

View File

@@ -90,7 +90,7 @@ void MAX7219Component::loop() {
} }
if (this->scroll_mode_ == ScrollMode::STOP) { if (this->scroll_mode_ == ScrollMode::STOP) {
if (this->stepsleft_ + get_width_internal() == first_line_size + 1) { if (static_cast<size_t>(this->stepsleft_ + get_width_internal()) == first_line_size + 1) {
if (millis_since_last_scroll < this->scroll_dwell_) { if (millis_since_last_scroll < this->scroll_dwell_) {
ESP_LOGVV(TAG, "Dwell time at end of string in case of stop at end. Step %d, since last scroll %d, dwell %d.", ESP_LOGVV(TAG, "Dwell time at end of string in case of stop at end. Step %d, since last scroll %d, dwell %d.",
this->stepsleft_, millis_since_last_scroll, this->scroll_dwell_); this->stepsleft_, millis_since_last_scroll, this->scroll_dwell_);

View File

@@ -21,11 +21,11 @@ template<uint8_t N> class MCP23XXXBase : public Component, public gpio_expander:
protected: protected:
// read a given register // read a given register
virtual bool read_reg(uint8_t reg, uint8_t *value); virtual bool read_reg(uint8_t reg, uint8_t *value) = 0;
// write a value to a given register // write a value to a given register
virtual bool write_reg(uint8_t reg, uint8_t value); virtual bool write_reg(uint8_t reg, uint8_t value) = 0;
// update registers with given pin value. // update registers with given pin value.
virtual void update_reg(uint8_t pin, bool pin_value, uint8_t reg_a); virtual void update_reg(uint8_t pin, bool pin_value, uint8_t reg_a) = 0;
bool open_drain_ints_; bool open_drain_ints_;
}; };

View File

@@ -20,6 +20,23 @@ bool MCP2515::setup_internal() {
return false; return false;
if (this->set_bitrate_(this->bit_rate_, this->mcp_clock_) != canbus::ERROR_OK) if (this->set_bitrate_(this->bit_rate_, this->mcp_clock_) != canbus::ERROR_OK)
return false; return false;
// setup hardware filter RXF0 accepting all standard CAN IDs
if (this->set_filter_(RXF::RXF0, false, 0) != canbus::ERROR_OK) {
return false;
}
if (this->set_filter_mask_(MASK::MASK0, false, 0) != canbus::ERROR_OK) {
return false;
}
// setup hardware filter RXF1 accepting all extended CAN IDs
if (this->set_filter_(RXF::RXF1, true, 0) != canbus::ERROR_OK) {
return false;
}
if (this->set_filter_mask_(MASK::MASK1, true, 0) != canbus::ERROR_OK) {
return false;
}
if (this->set_mode_(this->mcp_mode_) != canbus::ERROR_OK) if (this->set_mode_(this->mcp_mode_) != canbus::ERROR_OK)
return false; return false;
uint8_t err_flags = this->get_error_flags_(); uint8_t err_flags = this->get_error_flags_();

View File

@@ -11,7 +11,7 @@ from esphome.const import (
CONF_SERVICES, CONF_SERVICES,
PlatformFramework, PlatformFramework,
) )
from esphome.core import CORE, coroutine_with_priority from esphome.core import CORE, Lambda, coroutine_with_priority
from esphome.coroutine import CoroPriority from esphome.coroutine import CoroPriority
CODEOWNERS = ["@esphome/core"] CODEOWNERS = ["@esphome/core"]
@@ -58,21 +58,68 @@ CONFIG_SCHEMA = cv.All(
) )
def mdns_txt_record(key: str, value: str): def mdns_txt_record(key: str, value: str) -> cg.RawExpression:
return cg.StructInitializer( """Create a mDNS TXT record.
MDNSTXTRecord,
("key", key), Public API for external components. Do not remove.
("value", value),
Args:
key: The TXT record key
value: The TXT record value (static string only)
Returns:
A RawExpression representing a MDNSTXTRecord struct
"""
return cg.RawExpression(
f"{{MDNS_STR({cg.safe_exp(key)}), MDNS_STR({cg.safe_exp(value)})}}"
) )
async def _mdns_txt_record_templated(
mdns_comp: cg.Pvariable, key: str, value: Lambda | str
) -> cg.RawExpression:
"""Create a mDNS TXT record with support for templated values.
Internal helper function.
Args:
mdns_comp: The MDNSComponent instance (from cg.get_variable())
key: The TXT record key
value: The TXT record value (can be a static string or a lambda template)
Returns:
A RawExpression representing a MDNSTXTRecord struct
"""
if not cg.is_template(value):
# It's a static string - use directly in flash, no need to store in vector
return mdns_txt_record(key, value)
# It's a lambda - evaluate and store using helper
templated_value = await cg.templatable(value, [], cg.std_string)
safe_key = cg.safe_exp(key)
dynamic_call = f"{mdns_comp}->add_dynamic_txt_value(({templated_value})())"
return cg.RawExpression(f"{{MDNS_STR({safe_key}), MDNS_STR({dynamic_call})}}")
def mdns_service( def mdns_service(
service: str, proto: str, port: int, txt_records: list[dict[str, str]] service: str, proto: str, port: int, txt_records: list[cg.RawExpression]
): ) -> cg.StructInitializer:
"""Create a mDNS service.
Public API for external components. Do not remove.
Args:
service: Service name (e.g., "_http")
proto: Protocol (e.g., "_tcp" or "_udp")
port: Port number
txt_records: List of MDNSTXTRecord expressions
Returns:
A StructInitializer representing a MDNSService struct
"""
return cg.StructInitializer( return cg.StructInitializer(
MDNSService, MDNSService,
("service_type", service), ("service_type", cg.RawExpression(f"MDNS_STR({cg.safe_exp(service)})")),
("proto", proto), ("proto", cg.RawExpression(f"MDNS_STR({cg.safe_exp(proto)})")),
("port", port), ("port", port),
("txt_records", txt_records), ("txt_records", txt_records),
) )
@@ -107,23 +154,37 @@ async def to_code(config):
# Ensure at least 1 service (fallback service) # Ensure at least 1 service (fallback service)
cg.add_define("MDNS_SERVICE_COUNT", max(1, service_count)) cg.add_define("MDNS_SERVICE_COUNT", max(1, service_count))
# Calculate compile-time dynamic TXT value count
# Dynamic values are those that cannot be stored in flash at compile time
dynamic_txt_count = 0
if "api" in CORE.config:
# Always: get_mac_address()
dynamic_txt_count += 1
# User-provided templatable TXT values (only lambdas, not static strings)
dynamic_txt_count += sum(
1
for service in config[CONF_SERVICES]
for txt_value in service[CONF_TXT].values()
if cg.is_template(txt_value)
)
# Ensure at least 1 to avoid zero-size array
cg.add_define("MDNS_DYNAMIC_TXT_COUNT", max(1, dynamic_txt_count))
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
for service in config[CONF_SERVICES]: for service in config[CONF_SERVICES]:
txt = [ txt_records = [
cg.StructInitializer( await _mdns_txt_record_templated(var, txt_key, txt_value)
MDNSTXTRecord,
("key", txt_key),
("value", await cg.templatable(txt_value, [], cg.std_string)),
)
for txt_key, txt_value in service[CONF_TXT].items() for txt_key, txt_value in service[CONF_TXT].items()
] ]
exp = mdns_service( exp = mdns_service(
service[CONF_SERVICE], service[CONF_SERVICE],
service[CONF_PROTOCOL], service[CONF_PROTOCOL],
await cg.templatable(service[CONF_PORT], [], cg.uint16), await cg.templatable(service[CONF_PORT], [], cg.uint16),
txt, txt_records,
) )
cg.add(var.add_extra_service(exp)) cg.add(var.add_extra_service(exp))

View File

@@ -9,24 +9,9 @@
#include <pgmspace.h> #include <pgmspace.h>
// Macro to define strings in PROGMEM on ESP8266, regular memory on other platforms // Macro to define strings in PROGMEM on ESP8266, regular memory on other platforms
#define MDNS_STATIC_CONST_CHAR(name, value) static const char name[] PROGMEM = value #define MDNS_STATIC_CONST_CHAR(name, value) static const char name[] PROGMEM = value
// Helper to get string from PROGMEM - returns a temporary std::string
// Only define this function if we have services that will use it
#if defined(USE_API) || defined(USE_PROMETHEUS) || defined(USE_WEBSERVER) || defined(USE_MDNS_EXTRA_SERVICES)
static std::string mdns_string_p(const char *src) {
char buf[64];
strncpy_P(buf, src, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
return std::string(buf);
}
#define MDNS_STR(name) mdns_string_p(name)
#else
// If no services are configured, we still need the fallback service but it uses string literals
#define MDNS_STR(name) std::string(name)
#endif
#else #else
// On non-ESP8266 platforms, use regular const char* // On non-ESP8266 platforms, use regular const char*
#define MDNS_STATIC_CONST_CHAR(name, value) static constexpr const char *name = value #define MDNS_STATIC_CONST_CHAR(name, value) static constexpr const char name[] = value
#define MDNS_STR(name) name
#endif #endif
#ifdef USE_API #ifdef USE_API
@@ -46,30 +31,10 @@ static const char *const TAG = "mdns";
#endif #endif
// Define all constant strings using the macro // Define all constant strings using the macro
MDNS_STATIC_CONST_CHAR(SERVICE_ESPHOMELIB, "_esphomelib");
MDNS_STATIC_CONST_CHAR(SERVICE_TCP, "_tcp"); MDNS_STATIC_CONST_CHAR(SERVICE_TCP, "_tcp");
MDNS_STATIC_CONST_CHAR(SERVICE_PROMETHEUS, "_prometheus-http");
MDNS_STATIC_CONST_CHAR(SERVICE_HTTP, "_http");
MDNS_STATIC_CONST_CHAR(TXT_FRIENDLY_NAME, "friendly_name"); // Wrap build-time defines into flash storage
MDNS_STATIC_CONST_CHAR(TXT_VERSION, "version"); MDNS_STATIC_CONST_CHAR(VALUE_VERSION, ESPHOME_VERSION);
MDNS_STATIC_CONST_CHAR(TXT_MAC, "mac");
MDNS_STATIC_CONST_CHAR(TXT_PLATFORM, "platform");
MDNS_STATIC_CONST_CHAR(TXT_BOARD, "board");
MDNS_STATIC_CONST_CHAR(TXT_NETWORK, "network");
MDNS_STATIC_CONST_CHAR(TXT_API_ENCRYPTION, "api_encryption");
MDNS_STATIC_CONST_CHAR(TXT_API_ENCRYPTION_SUPPORTED, "api_encryption_supported");
MDNS_STATIC_CONST_CHAR(TXT_PROJECT_NAME, "project_name");
MDNS_STATIC_CONST_CHAR(TXT_PROJECT_VERSION, "project_version");
MDNS_STATIC_CONST_CHAR(TXT_PACKAGE_IMPORT_URL, "package_import_url");
MDNS_STATIC_CONST_CHAR(PLATFORM_ESP8266, "ESP8266");
MDNS_STATIC_CONST_CHAR(PLATFORM_ESP32, "ESP32");
MDNS_STATIC_CONST_CHAR(PLATFORM_RP2040, "RP2040");
MDNS_STATIC_CONST_CHAR(NETWORK_WIFI, "wifi");
MDNS_STATIC_CONST_CHAR(NETWORK_ETHERNET, "ethernet");
MDNS_STATIC_CONST_CHAR(NETWORK_THREAD, "thread");
void MDNSComponent::compile_records_() { void MDNSComponent::compile_records_() {
this->hostname_ = App.get_name(); this->hostname_ = App.get_name();
@@ -78,6 +43,15 @@ void MDNSComponent::compile_records_() {
// in mdns/__init__.py. If you add a new service here, update both locations. // in mdns/__init__.py. If you add a new service here, update both locations.
#ifdef USE_API #ifdef USE_API
MDNS_STATIC_CONST_CHAR(SERVICE_ESPHOMELIB, "_esphomelib");
MDNS_STATIC_CONST_CHAR(TXT_FRIENDLY_NAME, "friendly_name");
MDNS_STATIC_CONST_CHAR(TXT_VERSION, "version");
MDNS_STATIC_CONST_CHAR(TXT_MAC, "mac");
MDNS_STATIC_CONST_CHAR(TXT_PLATFORM, "platform");
MDNS_STATIC_CONST_CHAR(TXT_BOARD, "board");
MDNS_STATIC_CONST_CHAR(TXT_NETWORK, "network");
MDNS_STATIC_CONST_CHAR(VALUE_BOARD, ESPHOME_BOARD);
if (api::global_api_server != nullptr) { if (api::global_api_server != nullptr) {
auto &service = this->services_.emplace_next(); auto &service = this->services_.emplace_next();
service.service_type = MDNS_STR(SERVICE_ESPHOMELIB); service.service_type = MDNS_STR(SERVICE_ESPHOMELIB);
@@ -112,52 +86,66 @@ void MDNSComponent::compile_records_() {
txt_records.reserve(txt_count); txt_records.reserve(txt_count);
if (!friendly_name_empty) { if (!friendly_name_empty) {
txt_records.push_back({MDNS_STR(TXT_FRIENDLY_NAME), friendly_name}); txt_records.push_back({MDNS_STR(TXT_FRIENDLY_NAME), MDNS_STR(friendly_name.c_str())});
} }
txt_records.push_back({MDNS_STR(TXT_VERSION), ESPHOME_VERSION}); txt_records.push_back({MDNS_STR(TXT_VERSION), MDNS_STR(VALUE_VERSION)});
txt_records.push_back({MDNS_STR(TXT_MAC), get_mac_address()}); txt_records.push_back({MDNS_STR(TXT_MAC), MDNS_STR(this->add_dynamic_txt_value(get_mac_address()))});
#ifdef USE_ESP8266 #ifdef USE_ESP8266
MDNS_STATIC_CONST_CHAR(PLATFORM_ESP8266, "ESP8266");
txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(PLATFORM_ESP8266)}); txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(PLATFORM_ESP8266)});
#elif defined(USE_ESP32) #elif defined(USE_ESP32)
MDNS_STATIC_CONST_CHAR(PLATFORM_ESP32, "ESP32");
txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(PLATFORM_ESP32)}); txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(PLATFORM_ESP32)});
#elif defined(USE_RP2040) #elif defined(USE_RP2040)
MDNS_STATIC_CONST_CHAR(PLATFORM_RP2040, "RP2040");
txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(PLATFORM_RP2040)}); txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(PLATFORM_RP2040)});
#elif defined(USE_LIBRETINY) #elif defined(USE_LIBRETINY)
txt_records.emplace_back(MDNSTXTRecord{"platform", lt_cpu_get_model_name()}); txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(lt_cpu_get_model_name())});
#endif #endif
txt_records.push_back({MDNS_STR(TXT_BOARD), ESPHOME_BOARD}); txt_records.push_back({MDNS_STR(TXT_BOARD), MDNS_STR(VALUE_BOARD)});
#if defined(USE_WIFI) #if defined(USE_WIFI)
MDNS_STATIC_CONST_CHAR(NETWORK_WIFI, "wifi");
txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_WIFI)}); txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_WIFI)});
#elif defined(USE_ETHERNET) #elif defined(USE_ETHERNET)
MDNS_STATIC_CONST_CHAR(NETWORK_ETHERNET, "ethernet");
txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_ETHERNET)}); txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_ETHERNET)});
#elif defined(USE_OPENTHREAD) #elif defined(USE_OPENTHREAD)
MDNS_STATIC_CONST_CHAR(NETWORK_THREAD, "thread");
txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_THREAD)}); txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_THREAD)});
#endif #endif
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
MDNS_STATIC_CONST_CHAR(TXT_API_ENCRYPTION, "api_encryption");
MDNS_STATIC_CONST_CHAR(TXT_API_ENCRYPTION_SUPPORTED, "api_encryption_supported");
MDNS_STATIC_CONST_CHAR(NOISE_ENCRYPTION, "Noise_NNpsk0_25519_ChaChaPoly_SHA256"); MDNS_STATIC_CONST_CHAR(NOISE_ENCRYPTION, "Noise_NNpsk0_25519_ChaChaPoly_SHA256");
if (api::global_api_server->get_noise_ctx()->has_psk()) { bool has_psk = api::global_api_server->get_noise_ctx()->has_psk();
txt_records.push_back({MDNS_STR(TXT_API_ENCRYPTION), MDNS_STR(NOISE_ENCRYPTION)}); const char *encryption_key = has_psk ? TXT_API_ENCRYPTION : TXT_API_ENCRYPTION_SUPPORTED;
} else { txt_records.push_back({MDNS_STR(encryption_key), MDNS_STR(NOISE_ENCRYPTION)});
txt_records.push_back({MDNS_STR(TXT_API_ENCRYPTION_SUPPORTED), MDNS_STR(NOISE_ENCRYPTION)});
}
#endif #endif
#ifdef ESPHOME_PROJECT_NAME #ifdef ESPHOME_PROJECT_NAME
txt_records.push_back({MDNS_STR(TXT_PROJECT_NAME), ESPHOME_PROJECT_NAME}); MDNS_STATIC_CONST_CHAR(TXT_PROJECT_NAME, "project_name");
txt_records.push_back({MDNS_STR(TXT_PROJECT_VERSION), ESPHOME_PROJECT_VERSION}); MDNS_STATIC_CONST_CHAR(TXT_PROJECT_VERSION, "project_version");
MDNS_STATIC_CONST_CHAR(VALUE_PROJECT_NAME, ESPHOME_PROJECT_NAME);
MDNS_STATIC_CONST_CHAR(VALUE_PROJECT_VERSION, ESPHOME_PROJECT_VERSION);
txt_records.push_back({MDNS_STR(TXT_PROJECT_NAME), MDNS_STR(VALUE_PROJECT_NAME)});
txt_records.push_back({MDNS_STR(TXT_PROJECT_VERSION), MDNS_STR(VALUE_PROJECT_VERSION)});
#endif // ESPHOME_PROJECT_NAME #endif // ESPHOME_PROJECT_NAME
#ifdef USE_DASHBOARD_IMPORT #ifdef USE_DASHBOARD_IMPORT
txt_records.push_back({MDNS_STR(TXT_PACKAGE_IMPORT_URL), dashboard_import::get_package_import_url()}); MDNS_STATIC_CONST_CHAR(TXT_PACKAGE_IMPORT_URL, "package_import_url");
txt_records.push_back(
{MDNS_STR(TXT_PACKAGE_IMPORT_URL), MDNS_STR(dashboard_import::get_package_import_url().c_str())});
#endif #endif
} }
#endif // USE_API #endif // USE_API
#ifdef USE_PROMETHEUS #ifdef USE_PROMETHEUS
MDNS_STATIC_CONST_CHAR(SERVICE_PROMETHEUS, "_prometheus-http");
auto &prom_service = this->services_.emplace_next(); auto &prom_service = this->services_.emplace_next();
prom_service.service_type = MDNS_STR(SERVICE_PROMETHEUS); prom_service.service_type = MDNS_STR(SERVICE_PROMETHEUS);
prom_service.proto = MDNS_STR(SERVICE_TCP); prom_service.proto = MDNS_STR(SERVICE_TCP);
@@ -165,6 +153,8 @@ void MDNSComponent::compile_records_() {
#endif #endif
#ifdef USE_WEBSERVER #ifdef USE_WEBSERVER
MDNS_STATIC_CONST_CHAR(SERVICE_HTTP, "_http");
auto &web_service = this->services_.emplace_next(); auto &web_service = this->services_.emplace_next();
web_service.service_type = MDNS_STR(SERVICE_HTTP); web_service.service_type = MDNS_STR(SERVICE_HTTP);
web_service.proto = MDNS_STR(SERVICE_TCP); web_service.proto = MDNS_STR(SERVICE_TCP);
@@ -172,13 +162,16 @@ void MDNSComponent::compile_records_() {
#endif #endif
#if !defined(USE_API) && !defined(USE_PROMETHEUS) && !defined(USE_WEBSERVER) && !defined(USE_MDNS_EXTRA_SERVICES) #if !defined(USE_API) && !defined(USE_PROMETHEUS) && !defined(USE_WEBSERVER) && !defined(USE_MDNS_EXTRA_SERVICES)
MDNS_STATIC_CONST_CHAR(SERVICE_HTTP, "_http");
MDNS_STATIC_CONST_CHAR(TXT_VERSION, "version");
// Publish "http" service if not using native API or any other services // Publish "http" service if not using native API or any other services
// This is just to have *some* mDNS service so that .local resolution works // This is just to have *some* mDNS service so that .local resolution works
auto &fallback_service = this->services_.emplace_next(); auto &fallback_service = this->services_.emplace_next();
fallback_service.service_type = "_http"; fallback_service.service_type = MDNS_STR(SERVICE_HTTP);
fallback_service.proto = "_tcp"; fallback_service.proto = MDNS_STR(SERVICE_TCP);
fallback_service.port = USE_WEBSERVER_PORT; fallback_service.port = USE_WEBSERVER_PORT;
fallback_service.txt_records.emplace_back(MDNSTXTRecord{"version", ESPHOME_VERSION}); fallback_service.txt_records.push_back({MDNS_STR(TXT_VERSION), MDNS_STR(VALUE_VERSION)});
#endif #endif
} }
@@ -190,11 +183,10 @@ void MDNSComponent::dump_config() {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
ESP_LOGV(TAG, " Services:"); ESP_LOGV(TAG, " Services:");
for (const auto &service : this->services_) { for (const auto &service : this->services_) {
ESP_LOGV(TAG, " - %s, %s, %d", service.service_type.c_str(), service.proto.c_str(), ESP_LOGV(TAG, " - %s, %s, %d", MDNS_STR_ARG(service.service_type), MDNS_STR_ARG(service.proto),
const_cast<TemplatableValue<uint16_t> &>(service.port).value()); const_cast<TemplatableValue<uint16_t> &>(service.port).value());
for (const auto &record : service.txt_records) { for (const auto &record : service.txt_records) {
ESP_LOGV(TAG, " TXT: %s = %s", record.key.c_str(), ESP_LOGV(TAG, " TXT: %s = %s", MDNS_STR_ARG(record.key), MDNS_STR_ARG(record.value));
const_cast<TemplatableValue<std::string> &>(record.value).value().c_str());
} }
} }
#endif #endif

View File

@@ -9,21 +9,34 @@
namespace esphome { namespace esphome {
namespace mdns { namespace mdns {
// Helper struct that identifies strings that may be stored in flash storage (similar to LogString)
struct MDNSString;
// Macro to cast string literals to MDNSString* (works on all platforms)
#define MDNS_STR(name) (reinterpret_cast<const esphome::mdns::MDNSString *>(name))
#ifdef USE_ESP8266
#include <pgmspace.h>
#define MDNS_STR_ARG(s) ((PGM_P) (s))
#else
#define MDNS_STR_ARG(s) (reinterpret_cast<const char *>(s))
#endif
// Service count is calculated at compile time by Python codegen // Service count is calculated at compile time by Python codegen
// MDNS_SERVICE_COUNT will always be defined // MDNS_SERVICE_COUNT will always be defined
struct MDNSTXTRecord { struct MDNSTXTRecord {
std::string key; const MDNSString *key;
TemplatableValue<std::string> value; const MDNSString *value;
}; };
struct MDNSService { struct MDNSService {
// service name _including_ underscore character prefix // service name _including_ underscore character prefix
// as defined in RFC6763 Section 7 // as defined in RFC6763 Section 7
std::string service_type; const MDNSString *service_type;
// second label indicating protocol _including_ underscore character prefix // second label indicating protocol _including_ underscore character prefix
// as defined in RFC6763 Section 7, like "_tcp" or "_udp" // as defined in RFC6763 Section 7, like "_tcp" or "_udp"
std::string proto; const MDNSString *proto;
TemplatableValue<uint16_t> port; TemplatableValue<uint16_t> port;
std::vector<MDNSTXTRecord> txt_records; std::vector<MDNSTXTRecord> txt_records;
}; };
@@ -46,6 +59,17 @@ class MDNSComponent : public Component {
void on_shutdown() override; void on_shutdown() override;
/// Add a dynamic TXT value and return pointer to it for use in MDNSTXTRecord
const char *add_dynamic_txt_value(const std::string &value) {
this->dynamic_txt_values_.push_back(value);
return this->dynamic_txt_values_[this->dynamic_txt_values_.size() - 1].c_str();
}
/// Storage for runtime-generated TXT values (MAC address, user lambdas)
/// Pre-sized at compile time via MDNS_DYNAMIC_TXT_COUNT to avoid heap allocations.
/// Static/compile-time values (version, board, etc.) are stored directly in flash and don't use this.
StaticVector<std::string, MDNS_DYNAMIC_TXT_COUNT> dynamic_txt_values_;
protected: protected:
StaticVector<MDNSService, MDNS_SERVICE_COUNT> services_{}; StaticVector<MDNSService, MDNS_SERVICE_COUNT> services_{};
std::string hostname_; std::string hostname_;

View File

@@ -2,7 +2,6 @@
#if defined(USE_ESP32) && defined(USE_MDNS) #if defined(USE_ESP32) && defined(USE_MDNS)
#include <mdns.h> #include <mdns.h>
#include <cstring>
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "mdns_component.h" #include "mdns_component.h"
@@ -29,23 +28,18 @@ void MDNSComponent::setup() {
std::vector<mdns_txt_item_t> txt_records; std::vector<mdns_txt_item_t> txt_records;
for (const auto &record : service.txt_records) { for (const auto &record : service.txt_records) {
mdns_txt_item_t it{}; mdns_txt_item_t it{};
// dup strings to ensure the pointer is valid even after the record loop // key and value are either compile-time string literals in flash or pointers to dynamic_txt_values_
it.key = strdup(record.key.c_str()); // Both remain valid for the lifetime of this function, and ESP-IDF makes internal copies
it.value = strdup(const_cast<TemplatableValue<std::string> &>(record.value).value().c_str()); it.key = MDNS_STR_ARG(record.key);
it.value = MDNS_STR_ARG(record.value);
txt_records.push_back(it); txt_records.push_back(it);
} }
uint16_t port = const_cast<TemplatableValue<uint16_t> &>(service.port).value(); uint16_t port = const_cast<TemplatableValue<uint16_t> &>(service.port).value();
err = mdns_service_add(nullptr, service.service_type.c_str(), service.proto.c_str(), port, txt_records.data(), err = mdns_service_add(nullptr, MDNS_STR_ARG(service.service_type), MDNS_STR_ARG(service.proto), port,
txt_records.size()); txt_records.data(), txt_records.size());
// free records
for (const auto &it : txt_records) {
delete it.key; // NOLINT(cppcoreguidelines-owning-memory)
delete it.value; // NOLINT(cppcoreguidelines-owning-memory)
}
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGW(TAG, "Failed to register service %s: %s", service.service_type.c_str(), esp_err_to_name(err)); ESP_LOGW(TAG, "Failed to register service %s: %s", MDNS_STR_ARG(service.service_type), esp_err_to_name(err));
} }
} }
} }

View File

@@ -21,19 +21,19 @@ void MDNSComponent::setup() {
// part of the wire protocol to have an underscore, and for example ESP-IDF // part of the wire protocol to have an underscore, and for example ESP-IDF
// expects the underscore to be there, the ESP8266 implementation always adds // expects the underscore to be there, the ESP8266 implementation always adds
// the underscore itself. // the underscore itself.
auto *proto = service.proto.c_str(); auto *proto = MDNS_STR_ARG(service.proto);
while (*proto == '_') { while (progmem_read_byte((const uint8_t *) proto) == '_') {
proto++; proto++;
} }
auto *service_type = service.service_type.c_str(); auto *service_type = MDNS_STR_ARG(service.service_type);
while (*service_type == '_') { while (progmem_read_byte((const uint8_t *) service_type) == '_') {
service_type++; service_type++;
} }
uint16_t port = const_cast<TemplatableValue<uint16_t> &>(service.port).value(); uint16_t port = const_cast<TemplatableValue<uint16_t> &>(service.port).value();
MDNS.addService(service_type, proto, port); MDNS.addService(FPSTR(service_type), FPSTR(proto), port);
for (const auto &record : service.txt_records) { for (const auto &record : service.txt_records) {
MDNS.addServiceTxt(service_type, proto, record.key.c_str(), MDNS.addServiceTxt(FPSTR(service_type), FPSTR(proto), FPSTR(MDNS_STR_ARG(record.key)),
const_cast<TemplatableValue<std::string> &>(record.value).value().c_str()); FPSTR(MDNS_STR_ARG(record.value)));
} }
} }
} }

View File

@@ -21,19 +21,18 @@ void MDNSComponent::setup() {
// part of the wire protocol to have an underscore, and for example ESP-IDF // part of the wire protocol to have an underscore, and for example ESP-IDF
// expects the underscore to be there, the ESP8266 implementation always adds // expects the underscore to be there, the ESP8266 implementation always adds
// the underscore itself. // the underscore itself.
auto *proto = service.proto.c_str(); auto *proto = MDNS_STR_ARG(service.proto);
while (*proto == '_') { while (*proto == '_') {
proto++; proto++;
} }
auto *service_type = service.service_type.c_str(); auto *service_type = MDNS_STR_ARG(service.service_type);
while (*service_type == '_') { while (*service_type == '_') {
service_type++; service_type++;
} }
uint16_t port_ = const_cast<TemplatableValue<uint16_t> &>(service.port).value(); uint16_t port_ = const_cast<TemplatableValue<uint16_t> &>(service.port).value();
MDNS.addService(service_type, proto, port_); MDNS.addService(service_type, proto, port_);
for (const auto &record : service.txt_records) { for (const auto &record : service.txt_records) {
MDNS.addServiceTxt(service_type, proto, record.key.c_str(), MDNS.addServiceTxt(service_type, proto, MDNS_STR_ARG(record.key), MDNS_STR_ARG(record.value));
const_cast<TemplatableValue<std::string> &>(record.value).value().c_str());
} }
} }
} }

View File

@@ -21,19 +21,18 @@ void MDNSComponent::setup() {
// part of the wire protocol to have an underscore, and for example ESP-IDF // part of the wire protocol to have an underscore, and for example ESP-IDF
// expects the underscore to be there, the ESP8266 implementation always adds // expects the underscore to be there, the ESP8266 implementation always adds
// the underscore itself. // the underscore itself.
auto *proto = service.proto.c_str(); auto *proto = MDNS_STR_ARG(service.proto);
while (*proto == '_') { while (*proto == '_') {
proto++; proto++;
} }
auto *service_type = service.service_type.c_str(); auto *service_type = MDNS_STR_ARG(service.service_type);
while (*service_type == '_') { while (*service_type == '_') {
service_type++; service_type++;
} }
uint16_t port = const_cast<TemplatableValue<uint16_t> &>(service.port).value(); uint16_t port = const_cast<TemplatableValue<uint16_t> &>(service.port).value();
MDNS.addService(service_type, proto, port); MDNS.addService(service_type, proto, port);
for (const auto &record : service.txt_records) { for (const auto &record : service.txt_records) {
MDNS.addServiceTxt(service_type, proto, record.key.c_str(), MDNS.addServiceTxt(service_type, proto, MDNS_STR_ARG(record.key), MDNS_STR_ARG(record.value));
const_cast<TemplatableValue<std::string> &>(record.value).value().c_str());
} }
} }
} }

View File

@@ -340,7 +340,7 @@ class MipiSpi : public display::Display,
this->write_cmd_addr_data(0, 0, 0, 0, ptr, w * h, 8); this->write_cmd_addr_data(0, 0, 0, 0, ptr, w * h, 8);
} }
} else { } else {
for (size_t y = 0; y != h; y++) { for (size_t y = 0; y != static_cast<size_t>(h); y++) {
if constexpr (BUS_TYPE == BUS_TYPE_SINGLE || BUS_TYPE == BUS_TYPE_SINGLE_16) { if constexpr (BUS_TYPE == BUS_TYPE_SINGLE || BUS_TYPE == BUS_TYPE_SINGLE_16) {
this->write_array(ptr, w); this->write_array(ptr, w);
} else if constexpr (BUS_TYPE == BUS_TYPE_QUAD) { } else if constexpr (BUS_TYPE == BUS_TYPE_QUAD) {
@@ -372,8 +372,8 @@ class MipiSpi : public display::Display,
uint8_t dbuffer[DISPLAYPIXEL * 48]; uint8_t dbuffer[DISPLAYPIXEL * 48];
uint8_t *dptr = dbuffer; uint8_t *dptr = dbuffer;
auto stride = x_offset + w + x_pad; // stride in pixels auto stride = x_offset + w + x_pad; // stride in pixels
for (size_t y = 0; y != h; y++) { for (size_t y = 0; y != static_cast<size_t>(h); y++) {
for (size_t x = 0; x != w; x++) { for (size_t x = 0; x != static_cast<size_t>(w); x++) {
auto color_val = ptr[y * stride + x]; auto color_val = ptr[y * stride + x];
if constexpr (DISPLAYPIXEL == PIXEL_MODE_18 && BUFFERPIXEL == PIXEL_MODE_16) { if constexpr (DISPLAYPIXEL == PIXEL_MODE_18 && BUFFERPIXEL == PIXEL_MODE_16) {
// 16 to 18 bit conversion // 16 to 18 bit conversion

View File

@@ -572,7 +572,7 @@ void MixerSpeaker::audio_mixer_task(void *params) {
} }
} else { } else {
// Determine how many frames to mix // Determine how many frames to mix
for (int i = 0; i < transfer_buffers_with_data.size(); ++i) { for (size_t i = 0; i < transfer_buffers_with_data.size(); ++i) {
const uint32_t frames_available_in_buffer = const uint32_t frames_available_in_buffer =
speakers_with_data[i]->get_audio_stream_info().bytes_to_frames(transfer_buffers_with_data[i]->available()); speakers_with_data[i]->get_audio_stream_info().bytes_to_frames(transfer_buffers_with_data[i]->available());
frames_to_mix = std::min(frames_to_mix, frames_available_in_buffer); frames_to_mix = std::min(frames_to_mix, frames_available_in_buffer);
@@ -581,7 +581,7 @@ void MixerSpeaker::audio_mixer_task(void *params) {
audio::AudioStreamInfo primary_stream_info = speakers_with_data[0]->get_audio_stream_info(); audio::AudioStreamInfo primary_stream_info = speakers_with_data[0]->get_audio_stream_info();
// Mix two streams together // Mix two streams together
for (int i = 1; i < transfer_buffers_with_data.size(); ++i) { for (size_t i = 1; i < transfer_buffers_with_data.size(); ++i) {
mix_audio_samples(primary_buffer, primary_stream_info, mix_audio_samples(primary_buffer, primary_stream_info,
reinterpret_cast<int16_t *>(transfer_buffers_with_data[i]->get_buffer_start()), reinterpret_cast<int16_t *>(transfer_buffers_with_data[i]->get_buffer_start()),
speakers_with_data[i]->get_audio_stream_info(), speakers_with_data[i]->get_audio_stream_info(),
@@ -596,7 +596,7 @@ void MixerSpeaker::audio_mixer_task(void *params) {
} }
// Update source transfer buffer lengths and add new audio durations to the source speaker pending playbacks // Update source transfer buffer lengths and add new audio durations to the source speaker pending playbacks
for (int i = 0; i < transfer_buffers_with_data.size(); ++i) { for (size_t i = 0; i < transfer_buffers_with_data.size(); ++i) {
transfer_buffers_with_data[i]->decrease_buffer_length( transfer_buffers_with_data[i]->decrease_buffer_length(
speakers_with_data[i]->get_audio_stream_info().frames_to_bytes(frames_to_mix)); speakers_with_data[i]->get_audio_stream_info().frames_to_bytes(frames_to_mix));
speakers_with_data[i]->pending_playback_frames_ += frames_to_mix; speakers_with_data[i]->pending_playback_frames_ += frames_to_mix;

View File

@@ -66,7 +66,10 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) {
uint8_t data_offset = 3; uint8_t data_offset = 3;
// Per https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf Ch 5 User-Defined function codes // Per https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf Ch 5 User-Defined function codes
if (((function_code >= 65) && (function_code <= 72)) || ((function_code >= 100) && (function_code <= 110))) { if (((function_code >= FUNCTION_CODE_USER_DEFINED_SPACE_1_INIT) &&
(function_code <= FUNCTION_CODE_USER_DEFINED_SPACE_1_END)) ||
((function_code >= FUNCTION_CODE_USER_DEFINED_SPACE_2_INIT) &&
(function_code <= FUNCTION_CODE_USER_DEFINED_SPACE_2_END))) {
// Handle user-defined function, since we don't know how big this ought to be, // Handle user-defined function, since we don't know how big this ought to be,
// ideally we should delegate the entire length detection to whatever handler is // ideally we should delegate the entire length detection to whatever handler is
// installed, but wait, there is the CRC, and if we get a hit there is a good // installed, but wait, there is the CRC, and if we get a hit there is a good
@@ -91,10 +94,14 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) {
} else { } else {
// data starts at 2 and length is 4 for read registers commands // data starts at 2 and length is 4 for read registers commands
if (this->role == ModbusRole::SERVER) { if (this->role == ModbusRole::SERVER) {
if (function_code == 0x1 || function_code == 0x3 || function_code == 0x4 || function_code == 0x6) { if (function_code == ModbusFunctionCode::READ_COILS ||
function_code == ModbusFunctionCode::READ_DISCRETE_INPUTS ||
function_code == ModbusFunctionCode::READ_HOLDING_REGISTERS ||
function_code == ModbusFunctionCode::READ_INPUT_REGISTERS ||
function_code == ModbusFunctionCode::WRITE_SINGLE_REGISTER) {
data_offset = 2; data_offset = 2;
data_len = 4; data_len = 4;
} else if (function_code == 0x10) { } else if (function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) {
if (at < 6) { if (at < 6) {
return true; return true;
} }
@@ -104,7 +111,10 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) {
} }
} else { } else {
// the response for write command mirrors the requests and data starts at offset 2 instead of 3 for read commands // the response for write command mirrors the requests and data starts at offset 2 instead of 3 for read commands
if (function_code == 0x5 || function_code == 0x06 || function_code == 0xF || function_code == 0x10) { if (function_code == ModbusFunctionCode::WRITE_SINGLE_COIL ||
function_code == ModbusFunctionCode::WRITE_SINGLE_REGISTER ||
function_code == ModbusFunctionCode::WRITE_MULTIPLE_COILS ||
function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) {
data_offset = 2; data_offset = 2;
data_len = 4; data_len = 4;
} }
@@ -112,7 +122,7 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) {
// Error ( msb indicates error ) // Error ( msb indicates error )
// response format: Byte[0] = device address, Byte[1] function code | 0x80 , Byte[2] exception code, Byte[3-4] crc // response format: Byte[0] = device address, Byte[1] function code | 0x80 , Byte[2] exception code, Byte[3-4] crc
if ((function_code & 0x80) == 0x80) { if ((function_code & FUNCTION_CODE_EXCEPTION_MASK) == FUNCTION_CODE_EXCEPTION_MASK) {
data_offset = 2; data_offset = 2;
data_len = 1; data_len = 1;
} }
@@ -143,10 +153,10 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) {
if (device->address_ == address) { if (device->address_ == address) {
found = true; found = true;
// Is it an error response? // Is it an error response?
if ((function_code & 0x80) == 0x80) { if ((function_code & FUNCTION_CODE_EXCEPTION_MASK) == FUNCTION_CODE_EXCEPTION_MASK) {
ESP_LOGD(TAG, "Modbus error function code: 0x%X exception: %d", function_code, raw[2]); ESP_LOGD(TAG, "Modbus error function code: 0x%X exception: %d", function_code, raw[2]);
if (waiting_for_response != 0) { if (waiting_for_response != 0) {
device->on_modbus_error(function_code & 0x7F, raw[2]); device->on_modbus_error(function_code & FUNCTION_CODE_MASK, raw[2]);
} else { } else {
// Ignore modbus exception not related to a pending command // Ignore modbus exception not related to a pending command
ESP_LOGD(TAG, "Ignoring Modbus error - not expecting a response"); ESP_LOGD(TAG, "Ignoring Modbus error - not expecting a response");
@@ -154,12 +164,14 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) {
continue; continue;
} }
if (this->role == ModbusRole::SERVER) { if (this->role == ModbusRole::SERVER) {
if (function_code == 0x3 || function_code == 0x4) { if (function_code == ModbusFunctionCode::READ_HOLDING_REGISTERS ||
function_code == ModbusFunctionCode::READ_INPUT_REGISTERS) {
device->on_modbus_read_registers(function_code, uint16_t(data[1]) | (uint16_t(data[0]) << 8), device->on_modbus_read_registers(function_code, uint16_t(data[1]) | (uint16_t(data[0]) << 8),
uint16_t(data[3]) | (uint16_t(data[2]) << 8)); uint16_t(data[3]) | (uint16_t(data[2]) << 8));
continue; continue;
} }
if (function_code == 0x6 || function_code == 0x10) { if (function_code == ModbusFunctionCode::WRITE_SINGLE_REGISTER ||
function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) {
device->on_modbus_write_registers(function_code, data); device->on_modbus_write_registers(function_code, data);
continue; continue;
} }
@@ -199,7 +211,7 @@ void Modbus::send(uint8_t address, uint8_t function_code, uint16_t start_address
// Only check max number of registers for standard function codes // Only check max number of registers for standard function codes
// Some devices use non standard codes like 0x43 // Some devices use non standard codes like 0x43
if (number_of_entities > MAX_VALUES && function_code <= 0x10) { if (number_of_entities > MAX_VALUES && function_code <= ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) {
ESP_LOGE(TAG, "send too many values %d max=%zu", number_of_entities, MAX_VALUES); ESP_LOGE(TAG, "send too many values %d max=%zu", number_of_entities, MAX_VALUES);
return; return;
} }
@@ -210,15 +222,17 @@ void Modbus::send(uint8_t address, uint8_t function_code, uint16_t start_address
if (this->role == ModbusRole::CLIENT) { if (this->role == ModbusRole::CLIENT) {
data.push_back(start_address >> 8); data.push_back(start_address >> 8);
data.push_back(start_address >> 0); data.push_back(start_address >> 0);
if (function_code != 0x5 && function_code != 0x6) { if (function_code != ModbusFunctionCode::WRITE_SINGLE_COIL &&
function_code != ModbusFunctionCode::WRITE_SINGLE_REGISTER) {
data.push_back(number_of_entities >> 8); data.push_back(number_of_entities >> 8);
data.push_back(number_of_entities >> 0); data.push_back(number_of_entities >> 0);
} }
} }
if (payload != nullptr) { if (payload != nullptr) {
if (this->role == ModbusRole::SERVER || function_code == 0xF || function_code == 0x10) { // Write multiple if (this->role == ModbusRole::SERVER || function_code == ModbusFunctionCode::WRITE_MULTIPLE_COILS ||
data.push_back(payload_len); // Byte count is required for write function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) { // Write multiple
data.push_back(payload_len); // Byte count is required for write
} else { } else {
payload_len = 2; // Write single register or coil payload_len = 2; // Write single register or coil
} }

View File

@@ -3,6 +3,8 @@
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/components/uart/uart.h" #include "esphome/components/uart/uart.h"
#include "esphome/components/modbus/modbus_definitions.h"
#include <vector> #include <vector>
namespace esphome { namespace esphome {
@@ -65,12 +67,12 @@ class ModbusDevice {
this->parent_->send(this->address_, function, start_address, number_of_entities, payload_len, payload); this->parent_->send(this->address_, function, start_address, number_of_entities, payload_len, payload);
} }
void send_raw(const std::vector<uint8_t> &payload) { this->parent_->send_raw(payload); } void send_raw(const std::vector<uint8_t> &payload) { this->parent_->send_raw(payload); }
void send_error(uint8_t function_code, uint8_t exception_code) { void send_error(uint8_t function_code, ModbusExceptionCode exception_code) {
std::vector<uint8_t> error_response; std::vector<uint8_t> error_response;
error_response.reserve(3); error_response.reserve(3);
error_response.push_back(this->address_); error_response.push_back(this->address_);
error_response.push_back(function_code | 0x80); error_response.push_back(function_code | FUNCTION_CODE_EXCEPTION_MASK);
error_response.push_back(exception_code); error_response.push_back(static_cast<uint8_t>(exception_code));
this->send_raw(error_response); this->send_raw(error_response);
} }
// If more than one device is connected block sending a new command before a response is received // If more than one device is connected block sending a new command before a response is received

View File

@@ -0,0 +1,86 @@
#pragma once
#include "esphome/core/component.h"
namespace esphome {
namespace modbus {
/// Modbus definitions from specs:
/// https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf
// 5 Function Code Categories
const uint8_t FUNCTION_CODE_USER_DEFINED_SPACE_1_INIT = 65; // 0x41
const uint8_t FUNCTION_CODE_USER_DEFINED_SPACE_1_END = 72; // 0x48
const uint8_t FUNCTION_CODE_USER_DEFINED_SPACE_2_INIT = 100; // 0x64
const uint8_t FUNCTION_CODE_USER_DEFINED_SPACE_2_END = 110; // 0x6E
enum class ModbusFunctionCode : uint8_t {
CUSTOM = 0x00,
READ_COILS = 0x01,
READ_DISCRETE_INPUTS = 0x02,
READ_HOLDING_REGISTERS = 0x03,
READ_INPUT_REGISTERS = 0x04,
WRITE_SINGLE_COIL = 0x05,
WRITE_SINGLE_REGISTER = 0x06,
READ_EXCEPTION_STATUS = 0x07, // not implemented
DIAGNOSTICS = 0x08, // not implemented
GET_COMM_EVENT_COUNTER = 0x0B, // not implemented
GET_COMM_EVENT_LOG = 0x0C, // not implemented
WRITE_MULTIPLE_COILS = 0x0F,
WRITE_MULTIPLE_REGISTERS = 0x10,
REPORT_SERVER_ID = 0x11, // not implemented
READ_FILE_RECORD = 0x14, // not implemented
WRITE_FILE_RECORD = 0x15, // not implemented
MASK_WRITE_REGISTER = 0x16, // not implemented
READ_WRITE_MULTIPLE_REGISTERS = 0x17, // not implemented
READ_FIFO_QUEUE = 0x18, // not implemented
};
/*Allow comparison operators between ModbusFunctionCode and uint8_t*/
inline bool operator==(ModbusFunctionCode lhs, uint8_t rhs) { return static_cast<uint8_t>(lhs) == rhs; }
inline bool operator==(uint8_t lhs, ModbusFunctionCode rhs) { return lhs == static_cast<uint8_t>(rhs); }
inline bool operator!=(ModbusFunctionCode lhs, uint8_t rhs) { return !(static_cast<uint8_t>(lhs) == rhs); }
inline bool operator!=(uint8_t lhs, ModbusFunctionCode rhs) { return !(lhs == static_cast<uint8_t>(rhs)); }
inline bool operator<(ModbusFunctionCode lhs, uint8_t rhs) { return static_cast<uint8_t>(lhs) < rhs; }
inline bool operator<(uint8_t lhs, ModbusFunctionCode rhs) { return lhs < static_cast<uint8_t>(rhs); }
inline bool operator<=(ModbusFunctionCode lhs, uint8_t rhs) { return static_cast<uint8_t>(lhs) <= rhs; }
inline bool operator<=(uint8_t lhs, ModbusFunctionCode rhs) { return lhs <= static_cast<uint8_t>(rhs); }
inline bool operator>(ModbusFunctionCode lhs, uint8_t rhs) { return static_cast<uint8_t>(lhs) > rhs; }
inline bool operator>(uint8_t lhs, ModbusFunctionCode rhs) { return lhs > static_cast<uint8_t>(rhs); }
inline bool operator>=(ModbusFunctionCode lhs, uint8_t rhs) { return static_cast<uint8_t>(lhs) >= rhs; }
inline bool operator>=(uint8_t lhs, ModbusFunctionCode rhs) { return lhs >= static_cast<uint8_t>(rhs); }
// 4.3 MODBUS Data model
enum class ModbusRegisterType : uint8_t {
CUSTOM = 0x00,
COIL = 0x01,
DISCRETE_INPUT = 0x02,
HOLDING = 0x03,
READ = 0x04,
};
// 7 MODBUS Exception Responses:
const uint8_t FUNCTION_CODE_MASK = 0x7F;
const uint8_t FUNCTION_CODE_EXCEPTION_MASK = 0x80;
enum class ModbusExceptionCode : uint8_t {
ILLEGAL_FUNCTION = 0x01,
ILLEGAL_DATA_ADDRESS = 0x02,
ILLEGAL_DATA_VALUE = 0x03,
SERVICE_DEVICE_FAILURE = 0x04,
ACKNOWLEDGE = 0x05,
SERVER_DEVICE_BUSY = 0x06,
MEMORY_PARITY_ERROR = 0x08,
GATEWAY_PATH_UNAVAILABLE = 0x0A,
GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND = 0x0B,
};
// 6.12 16 (0x10) Write Multiple registers:
const uint8_t MAX_NUM_OF_REGISTERS_TO_WRITE = 123; // 0x7B
// 6.3 03 (0x03) Read Holding Registers
// 6.4 04 (0x04) Read Input Registers
const uint8_t MAX_NUM_OF_REGISTERS_TO_READ = 125; // 0x7D
/// End of Modbus definitions
} // namespace modbus
} // namespace esphome

View File

@@ -20,6 +20,7 @@ from .const import (
CONF_BYTE_OFFSET, CONF_BYTE_OFFSET,
CONF_COMMAND_THROTTLE, CONF_COMMAND_THROTTLE,
CONF_CUSTOM_COMMAND, CONF_CUSTOM_COMMAND,
CONF_ENABLED,
CONF_FORCE_NEW_RANGE, CONF_FORCE_NEW_RANGE,
CONF_MAX_CMD_RETRIES, CONF_MAX_CMD_RETRIES,
CONF_MODBUS_CONTROLLER_ID, CONF_MODBUS_CONTROLLER_ID,
@@ -28,8 +29,11 @@ from .const import (
CONF_ON_OFFLINE, CONF_ON_OFFLINE,
CONF_ON_ONLINE, CONF_ON_ONLINE,
CONF_REGISTER_COUNT, CONF_REGISTER_COUNT,
CONF_REGISTER_LAST_ADDRESS,
CONF_REGISTER_TYPE, CONF_REGISTER_TYPE,
CONF_REGISTER_VALUE,
CONF_RESPONSE_SIZE, CONF_RESPONSE_SIZE,
CONF_SERVER_COURTESY_RESPONSE,
CONF_SKIP_UPDATES, CONF_SKIP_UPDATES,
CONF_VALUE_TYPE, CONF_VALUE_TYPE,
) )
@@ -49,6 +53,7 @@ ModbusController = modbus_controller_ns.class_(
) )
SensorItem = modbus_controller_ns.struct("SensorItem") SensorItem = modbus_controller_ns.struct("SensorItem")
ServerCourtesyResponse = modbus_controller_ns.struct("ServerCourtesyResponse")
ServerRegister = modbus_controller_ns.struct("ServerRegister") ServerRegister = modbus_controller_ns.struct("ServerRegister")
ModbusFunctionCode_ns = modbus_controller_ns.namespace("ModbusFunctionCode") ModbusFunctionCode_ns = modbus_controller_ns.namespace("ModbusFunctionCode")
@@ -143,6 +148,14 @@ ModbusOfflineTrigger = modbus_controller_ns.class_(
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SERVER_COURTESY_RESPONSE_SCHEMA = cv.Schema(
{
cv.Optional(CONF_ENABLED, default=False): cv.boolean,
cv.Optional(CONF_REGISTER_LAST_ADDRESS, default=0xFFFF): cv.hex_uint16_t,
cv.Optional(CONF_REGISTER_VALUE, default=0): cv.hex_uint16_t,
}
)
ModbusServerRegisterSchema = cv.Schema( ModbusServerRegisterSchema = cv.Schema(
{ {
cv.GenerateID(): cv.declare_id(ServerRegister), cv.GenerateID(): cv.declare_id(ServerRegister),
@@ -162,6 +175,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional( cv.Optional(
CONF_COMMAND_THROTTLE, default="0ms" CONF_COMMAND_THROTTLE, default="0ms"
): cv.positive_time_period_milliseconds, ): cv.positive_time_period_milliseconds,
cv.Optional(CONF_SERVER_COURTESY_RESPONSE): SERVER_COURTESY_RESPONSE_SCHEMA,
cv.Optional(CONF_MAX_CMD_RETRIES, default=4): cv.positive_int, cv.Optional(CONF_MAX_CMD_RETRIES, default=4): cv.positive_int,
cv.Optional(CONF_OFFLINE_SKIP_UPDATES, default=0): cv.positive_int, cv.Optional(CONF_OFFLINE_SKIP_UPDATES, default=0): cv.positive_int,
cv.Optional( cv.Optional(
@@ -232,7 +246,7 @@ def validate_modbus_register(config):
def _final_validate(config): def _final_validate(config):
if CONF_SERVER_REGISTERS in config: if CONF_SERVER_COURTESY_RESPONSE in config or CONF_SERVER_REGISTERS in config:
return modbus.final_validate_modbus_device("modbus_controller", role="server")( return modbus.final_validate_modbus_device("modbus_controller", role="server")(
config config
) )
@@ -299,6 +313,20 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
cg.add(var.set_allow_duplicate_commands(config[CONF_ALLOW_DUPLICATE_COMMANDS])) cg.add(var.set_allow_duplicate_commands(config[CONF_ALLOW_DUPLICATE_COMMANDS]))
cg.add(var.set_command_throttle(config[CONF_COMMAND_THROTTLE])) cg.add(var.set_command_throttle(config[CONF_COMMAND_THROTTLE]))
if server_courtesy_response := config.get(CONF_SERVER_COURTESY_RESPONSE):
cg.add(
var.set_server_courtesy_response(
cg.StructInitializer(
ServerCourtesyResponse,
("enabled", server_courtesy_response[CONF_ENABLED]),
(
"register_last_address",
server_courtesy_response[CONF_REGISTER_LAST_ADDRESS],
),
("register_value", server_courtesy_response[CONF_REGISTER_VALUE]),
)
)
)
cg.add(var.set_max_cmd_retries(config[CONF_MAX_CMD_RETRIES])) cg.add(var.set_max_cmd_retries(config[CONF_MAX_CMD_RETRIES]))
cg.add(var.set_offline_skip_updates(config[CONF_OFFLINE_SKIP_UPDATES])) cg.add(var.set_offline_skip_updates(config[CONF_OFFLINE_SKIP_UPDATES]))
if CONF_SERVER_REGISTERS in config: if CONF_SERVER_REGISTERS in config:

View File

@@ -2,6 +2,7 @@ CONF_ALLOW_DUPLICATE_COMMANDS = "allow_duplicate_commands"
CONF_BITMASK = "bitmask" CONF_BITMASK = "bitmask"
CONF_BYTE_OFFSET = "byte_offset" CONF_BYTE_OFFSET = "byte_offset"
CONF_COMMAND_THROTTLE = "command_throttle" CONF_COMMAND_THROTTLE = "command_throttle"
CONF_ENABLED = "enabled"
CONF_OFFLINE_SKIP_UPDATES = "offline_skip_updates" CONF_OFFLINE_SKIP_UPDATES = "offline_skip_updates"
CONF_CUSTOM_COMMAND = "custom_command" CONF_CUSTOM_COMMAND = "custom_command"
CONF_FORCE_NEW_RANGE = "force_new_range" CONF_FORCE_NEW_RANGE = "force_new_range"
@@ -13,8 +14,11 @@ CONF_ON_ONLINE = "on_online"
CONF_ON_OFFLINE = "on_offline" CONF_ON_OFFLINE = "on_offline"
CONF_RAW_ENCODE = "raw_encode" CONF_RAW_ENCODE = "raw_encode"
CONF_REGISTER_COUNT = "register_count" CONF_REGISTER_COUNT = "register_count"
CONF_REGISTER_LAST_ADDRESS = "register_last_address"
CONF_REGISTER_TYPE = "register_type" CONF_REGISTER_TYPE = "register_type"
CONF_REGISTER_VALUE = "register_value"
CONF_RESPONSE_SIZE = "response_size" CONF_RESPONSE_SIZE = "response_size"
CONF_SERVER_COURTESY_RESPONSE = "server_courtesy_response"
CONF_SKIP_UPDATES = "skip_updates" CONF_SKIP_UPDATES = "skip_updates"
CONF_USE_WRITE_MULTIPLE = "use_write_multiple" CONF_USE_WRITE_MULTIPLE = "use_write_multiple"
CONF_VALUE_TYPE = "value_type" CONF_VALUE_TYPE = "value_type"

View File

@@ -112,6 +112,12 @@ void ModbusController::on_modbus_read_registers(uint8_t function_code, uint16_t
"0x%X.", "0x%X.",
this->address_, function_code, start_address, number_of_registers); this->address_, function_code, start_address, number_of_registers);
if (number_of_registers == 0 || number_of_registers > modbus::MAX_NUM_OF_REGISTERS_TO_READ) {
ESP_LOGW(TAG, "Invalid number of registers %d. Sending exception response.", number_of_registers);
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
return;
}
std::vector<uint16_t> sixteen_bit_response; std::vector<uint16_t> sixteen_bit_response;
for (uint16_t current_address = start_address; current_address < start_address + number_of_registers;) { for (uint16_t current_address = start_address; current_address < start_address + number_of_registers;) {
bool found = false; bool found = false;
@@ -136,9 +142,21 @@ void ModbusController::on_modbus_read_registers(uint8_t function_code, uint16_t
} }
if (!found) { if (!found) {
ESP_LOGW(TAG, "Could not match any register to address %02X. Sending exception response.", current_address); if (this->server_courtesy_response_.enabled &&
send_error(function_code, 0x02); (current_address <= this->server_courtesy_response_.register_last_address)) {
return; ESP_LOGD(TAG,
"Could not match any register to address 0x%02X, but default allowed. "
"Returning default value: %d.",
current_address, this->server_courtesy_response_.register_value);
sixteen_bit_response.push_back(this->server_courtesy_response_.register_value);
current_address += 1; // Just increment by 1, as the default response is a single register
} else {
ESP_LOGW(TAG,
"Could not match any register to address 0x%02X and default not allowed. Sending exception response.",
current_address);
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
return;
}
} }
} }
@@ -156,27 +174,27 @@ void ModbusController::on_modbus_write_registers(uint8_t function_code, const st
uint16_t number_of_registers; uint16_t number_of_registers;
uint16_t payload_offset; uint16_t payload_offset;
if (function_code == 0x10) { if (function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) {
number_of_registers = uint16_t(data[3]) | (uint16_t(data[2]) << 8); number_of_registers = uint16_t(data[3]) | (uint16_t(data[2]) << 8);
if (number_of_registers == 0 || number_of_registers > 0x7B) { if (number_of_registers == 0 || number_of_registers > modbus::MAX_NUM_OF_REGISTERS_TO_WRITE) {
ESP_LOGW(TAG, "Invalid number of registers %d. Sending exception response.", number_of_registers); ESP_LOGW(TAG, "Invalid number of registers %d. Sending exception response.", number_of_registers);
send_error(function_code, 3); this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
return; return;
} }
uint16_t payload_size = data[4]; uint16_t payload_size = data[4];
if (payload_size != number_of_registers * 2) { if (payload_size != number_of_registers * 2) {
ESP_LOGW(TAG, "Payload size of %d bytes is not 2 times the number of registers (%d). Sending exception response.", ESP_LOGW(TAG, "Payload size of %d bytes is not 2 times the number of registers (%d). Sending exception response.",
payload_size, number_of_registers); payload_size, number_of_registers);
send_error(function_code, 3); this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
return; return;
} }
payload_offset = 5; payload_offset = 5;
} else if (function_code == 0x06) { } else if (function_code == ModbusFunctionCode::WRITE_SINGLE_REGISTER) {
number_of_registers = 1; number_of_registers = 1;
payload_offset = 2; payload_offset = 2;
} else { } else {
ESP_LOGW(TAG, "Invalid function code 0x%X. Sending exception response.", function_code); ESP_LOGW(TAG, "Invalid function code 0x%X. Sending exception response.", function_code);
send_error(function_code, 1); this->send_error(function_code, ModbusExceptionCode::ILLEGAL_FUNCTION);
return; return;
} }
@@ -211,7 +229,7 @@ void ModbusController::on_modbus_write_registers(uint8_t function_code, const st
if (!for_each_register([](ServerRegister *server_register, uint16_t offset) -> bool { if (!for_each_register([](ServerRegister *server_register, uint16_t offset) -> bool {
return server_register->write_lambda != nullptr; return server_register->write_lambda != nullptr;
})) { })) {
send_error(function_code, 1); this->send_error(function_code, ModbusExceptionCode::ILLEGAL_FUNCTION);
return; return;
} }
@@ -220,7 +238,7 @@ void ModbusController::on_modbus_write_registers(uint8_t function_code, const st
int64_t number = payload_to_number(data, server_register->value_type, offset, 0xFFFFFFFF); int64_t number = payload_to_number(data, server_register->value_type, offset, 0xFFFFFFFF);
return server_register->write_lambda(number); return server_register->write_lambda(number);
})) { })) {
send_error(function_code, 4); this->send_error(function_code, ModbusExceptionCode::SERVICE_DEVICE_FAILURE);
return; return;
} }
@@ -431,8 +449,15 @@ void ModbusController::dump_config() {
"ModbusController:\n" "ModbusController:\n"
" Address: 0x%02X\n" " Address: 0x%02X\n"
" Max Command Retries: %d\n" " Max Command Retries: %d\n"
" Offline Skip Updates: %d", " Offline Skip Updates: %d\n"
this->address_, this->max_cmd_retries_, this->offline_skip_updates_); " Server Courtesy Response:\n"
" Enabled: %s\n"
" Register Last Address: 0x%02X\n"
" Register Value: %d",
this->address_, this->max_cmd_retries_, this->offline_skip_updates_,
this->server_courtesy_response_.enabled ? "true" : "false",
this->server_courtesy_response_.register_last_address, this->server_courtesy_response_.register_value);
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
ESP_LOGCONFIG(TAG, "sensormap"); ESP_LOGCONFIG(TAG, "sensormap");
for (auto &it : this->sensorset_) { for (auto &it : this->sensorset_) {

View File

@@ -16,35 +16,9 @@ namespace modbus_controller {
class ModbusController; class ModbusController;
enum class ModbusFunctionCode { using modbus::ModbusFunctionCode;
CUSTOM = 0x00, using modbus::ModbusRegisterType;
READ_COILS = 0x01, using modbus::ModbusExceptionCode;
READ_DISCRETE_INPUTS = 0x02,
READ_HOLDING_REGISTERS = 0x03,
READ_INPUT_REGISTERS = 0x04,
WRITE_SINGLE_COIL = 0x05,
WRITE_SINGLE_REGISTER = 0x06,
READ_EXCEPTION_STATUS = 0x07, // not implemented
DIAGNOSTICS = 0x08, // not implemented
GET_COMM_EVENT_COUNTER = 0x0B, // not implemented
GET_COMM_EVENT_LOG = 0x0C, // not implemented
WRITE_MULTIPLE_COILS = 0x0F,
WRITE_MULTIPLE_REGISTERS = 0x10,
REPORT_SERVER_ID = 0x11, // not implemented
READ_FILE_RECORD = 0x14, // not implemented
WRITE_FILE_RECORD = 0x15, // not implemented
MASK_WRITE_REGISTER = 0x16, // not implemented
READ_WRITE_MULTIPLE_REGISTERS = 0x17, // not implemented
READ_FIFO_QUEUE = 0x18, // not implemented
};
enum class ModbusRegisterType : uint8_t {
CUSTOM = 0x0,
COIL = 0x01,
DISCRETE_INPUT = 0x02,
HOLDING = 0x03,
READ = 0x04,
};
enum class SensorValueType : uint8_t { enum class SensorValueType : uint8_t {
RAW = 0x00, // variable length RAW = 0x00, // variable length
@@ -256,6 +230,12 @@ class SensorItem {
bool force_new_range{false}; bool force_new_range{false};
}; };
struct ServerCourtesyResponse {
bool enabled{false};
uint16_t register_last_address{0xFFFF};
uint16_t register_value{0};
};
class ServerRegister { class ServerRegister {
using ReadLambda = std::function<int64_t()>; using ReadLambda = std::function<int64_t()>;
using WriteLambda = std::function<bool(int64_t value)>; using WriteLambda = std::function<bool(int64_t value)>;
@@ -530,6 +510,12 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice {
void set_max_cmd_retries(uint8_t max_cmd_retries) { this->max_cmd_retries_ = max_cmd_retries; } void set_max_cmd_retries(uint8_t max_cmd_retries) { this->max_cmd_retries_ = max_cmd_retries; }
/// get how many times a command will be (re)sent if no response is received /// get how many times a command will be (re)sent if no response is received
uint8_t get_max_cmd_retries() { return this->max_cmd_retries_; } uint8_t get_max_cmd_retries() { return this->max_cmd_retries_; }
/// Called by esphome generated code to set the server courtesy response object
void set_server_courtesy_response(const ServerCourtesyResponse &server_courtesy_response) {
this->server_courtesy_response_ = server_courtesy_response;
}
/// Get the server courtesy response object
ServerCourtesyResponse get_server_courtesy_response() const { return this->server_courtesy_response_; }
protected: protected:
/// parse sensormap_ and create range of sequential addresses /// parse sensormap_ and create range of sequential addresses
@@ -572,6 +558,9 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice {
CallbackManager<void(int, int)> online_callback_{}; CallbackManager<void(int, int)> online_callback_{};
/// Server offline callback /// Server offline callback
CallbackManager<void(int, int)> offline_callback_{}; CallbackManager<void(int, int)> offline_callback_{};
/// Server courtesy response
ServerCourtesyResponse server_courtesy_response_{
.enabled = false, .register_last_address = 0xFFFF, .register_value = 0};
}; };
/** Convert vector<uint8_t> response payload to float. /** Convert vector<uint8_t> response payload to float.

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