Compare commits

...

81 Commits

Author SHA1 Message Date
J. Nick Koston
db0b32bfc9 [network] Fix IPAddress::str_to() to lowercase IPv6 hex digits (#13325) 2026-01-17 18:06:54 -10:00
J. Nick Koston
21794e28e5 [modbus_controller] Use stack buffers instead of heap-allocating string helpers (#13221)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-01-17 17:26:51 -10:00
J. Nick Koston
728236270c [weikai] Replace bitset to_string with format_bin_to (#13297) 2026-01-17 15:53:01 -10:00
J. Nick Koston
01cdc4ed58 [core] Add fnv1_hash_extend() string overloads, use in atm90e32 (#13326) 2026-01-17 15:52:19 -10:00
J. Nick Koston
d6a0c8ffbb [template] Store alarm control panel codes in flash instead of heap (#13329) 2026-01-17 15:52:06 -10:00
J. Nick Koston
4cc0f874f7 [wireguard] Store configuration strings in flash instead of heap (#13331) 2026-01-17 15:51:26 -10:00
J. Nick Koston
ed58b9372f [template] Store text initial_value in flash and avoid heap allocation in setup (#13332) 2026-01-17 15:51:12 -10:00
J. Nick Koston
ee2a81923b [sun] Store text sensor format string in flash (#13335) 2026-01-17 15:51:01 -10:00
J. Nick Koston
0a1e7ee50b [pipsolar] Store command strings in flash (#13336) 2026-01-17 15:50:42 -10:00
J. Nick Koston
4d4283bcfa [udp] Store addresses in flash instead of heap (#13330) 2026-01-17 15:50:23 -10:00
J. Nick Koston
e4fb6988ff [web_server] Use ESPHOME_F for canHandle domain checks to reduce ESP8266 RAM (#13315)
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
2026-01-17 22:29:29 +00:00
J. Nick Koston
d31b733dce [light] Store color mode JSON strings in flash on ESP8266 (#13314) 2026-01-17 16:06:25 -06:00
Keith Burzinski
b25a2f8d8e [infrared][web_server] Implement initial web_server support (#13202)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-01-17 16:01:13 -06:00
J. Nick Koston
3f892711c7 [core][opentherm] Add format_bin_to(), soft-deprecate format_bin() (#13232) 2026-01-17 11:09:42 -10:00
Jonathan Swoboda
798d3bd956 Merge branch 'beta' into dev 2026-01-16 23:45:36 -05:00
Jonathan Swoboda
77df3933db Merge pull request #13309 from esphome/bump-2026.1.0b3
2026.1.0b3
2026-01-16 23:45:26 -05:00
Jonathan Swoboda
19514ccdf4 Bump version to 2026.1.0b3 2026-01-16 23:05:59 -05:00
Mike Ford
2947642ca5 [http_request] Unable to handle chunked responses (#7884)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-01-16 23:05:59 -05:00
Stuart Parmenter
60e333db08 [hub75] Bump esp-hub75 version to 0.3.0 (#13243) 2026-01-16 23:05:59 -05:00
J. Nick Koston
d8463f4813 [hmac_sha256] Replace unsafe sprintf with format_hex_to (#13290) 2026-01-16 23:05:59 -05:00
mrtoy-me
e1800d2fe2 [ntc, resistance] change log level to verbose (#13268) 2026-01-16 23:05:59 -05:00
J. Nick Koston
50aa4b1992 [esp32_ble_client] Reduce GATT data event logging to prevent firmware update failures (#13252) 2026-01-16 23:05:59 -05:00
J. Nick Koston
edb303e495 [api] Fix clock conflicts when multiple clients connected to homeassistant time (#13253) 2026-01-16 23:05:59 -05:00
J. Nick Koston
973fc4c5dc [dallas_temp] Use const char* for set_timeout to fix deprecation warning and heap churn (#13250) 2026-01-16 23:05:59 -05:00
J. Nick Koston
f88e8fc43b [sprinkler] Fix scheduler deprecation warnings and heap churn with FixedVector (#13251) 2026-01-16 23:05:59 -05:00
Jonathan Swoboda
d830787c71 Merge branch 'release' into dev 2026-01-16 22:49:39 -05:00
Jonathan Swoboda
c4c31a2e8e Merge branch 'release' into beta 2026-01-16 22:49:38 -05:00
Jonathan Swoboda
e6790f0042 Merge pull request #13308 from esphome/bump-2025.12.7
2025.12.7
2026-01-16 22:49:26 -05:00
Jonathan Swoboda
ec7f72e280 Bump version to 2025.12.7 2026-01-16 22:24:05 -05:00
J. Nick Koston
6f29dbd6f1 [api] Use subtraction for protobuf bounds checking (#13306) 2026-01-16 22:24:05 -05:00
Kevin Ahrendt
9caf78aa7e [i2s_audio] Bugfix: Buffer overflow in software volume control (#13190) 2026-01-16 22:24:05 -05:00
Mike Ford
1f4221abfa [http_request] Unable to handle chunked responses (#7884)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-01-16 22:18:48 -05:00
Stuart Parmenter
92808a09c7 [hub75] Bump esp-hub75 version to 0.3.0 (#13243) 2026-01-16 22:17:36 -05:00
J. Nick Koston
e54d5ee898 [hmac_sha256] Replace unsafe sprintf with format_hex_to (#13290) 2026-01-16 22:16:38 -05:00
J. Nick Koston
bbe1155518 [web_server] Skip defer on ESP8266 where callbacks already run in main loop (#13261) 2026-01-16 20:08:04 -06:00
J. Nick Koston
69d7b6e921 [api] Use subtraction for protobuf bounds checking (#13306) 2026-01-16 15:46:15 -10:00
Keith Burzinski
510c874061 [helpers] Remove base85 functions (#13266) 2026-01-17 01:23:41 +00:00
Keith Burzinski
f7ad324d81 [infrared, remote_base] Replace base85 with base64url for web server infrared transmissions (#13265) 2026-01-16 18:15:27 -06:00
Keith Burzinski
58a9e30017 [helpers] Add base64_decode_int32_vector function (#13289)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-01-16 23:05:19 +00:00
J. Nick Koston
52ac9e1861 [remote_base] Replace unsafe sprintf with buf_append_printf; fix buffer overflow (#13257)
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-16 16:56:47 -06:00
Clyde Stubbs
c5e4a60884 [select] Add condition for testing select option (#13267)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2026-01-17 08:35:40 +11:00
dependabot[bot]
a680884138 Bump ruff from 0.14.12 to 0.14.13 (#13275)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-01-16 20:29:02 +00:00
Jonathan Swoboda
6832efbacc Add Claude Code PR workflow skill (#13271)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 10:24:28 -10:00
dependabot[bot]
3057a0484f Bump actions/cache from 5.0.1 to 5.0.2 in /.github/actions/restore-python (#13277)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-16 09:36:42 -10:00
dependabot[bot]
bc78f80f77 Bump actions/cache from 5.0.1 to 5.0.2 (#13276)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-16 09:36:29 -10:00
J. Nick Koston
916b028fb2 [mqtt] Replace sprintf with snprintf for friendly name hash (#13262) 2026-01-16 08:30:22 -10:00
mrtoy-me
16adae7359 [ntc, resistance] change log level to verbose (#13268) 2026-01-16 10:19:09 -05:00
Remco van Essen
4906f87751 [mipi_dsi] add JC8012P4A1 (#13241) 2026-01-16 21:17:32 +11:00
Keith Burzinski
5b37d2fb27 [helpers] Support base64url encoding (#13264) 2026-01-16 08:55:24 +00:00
J. Nick Koston
68affe0b9c [core] Add --device hint when DNS resolution fails (#13240) 2026-01-15 18:55:32 -10:00
J. Nick Koston
8263a8273f [debug] Add min_free heap sensor for ESP32 and LibreTiny, add fragmentation for ESP32 (#13231) 2026-01-15 18:08:26 -10:00
Keith Burzinski
14b7539094 [infrared, remote_base] Optimize IR transmit path for web_server base85 data (#13238) 2026-01-15 22:04:21 -06:00
J. Nick Koston
b37cb812a7 [core] Add buf_append_printf helper for safe buffer formatting (#13258) 2026-01-15 22:03:11 -06:00
J. Nick Koston
42491569c8 [analyze_memory] Add nRF52/Zephyr platform support for memory analysis (#13249) 2026-01-15 17:53:53 -10:00
J. Nick Koston
b1230ec6bb [esp32_ble_client] Reduce GATT data event logging to prevent firmware update failures (#13252) 2026-01-15 16:49:19 -10:00
J. Nick Koston
4eda9e965f [api] Fix clock conflicts when multiple clients connected to homeassistant time (#13253) 2026-01-15 16:49:01 -10:00
J. Nick Koston
d2528af649 [dallas_temp] Use const char* for set_timeout to fix deprecation warning and heap churn (#13250) 2026-01-15 16:48:44 -10:00
Keith Burzinski
2eabc1b96b [helpers] Add base85 support (#13254)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-01-16 02:22:05 +00:00
J. Nick Koston
535c3eb2a2 [sprinkler] Fix scheduler deprecation warnings and heap churn with FixedVector (#13251) 2026-01-15 11:32:02 -10:00
Jonathan Swoboda
20f937692e Merge branch 'beta' into dev 2026-01-15 16:24:19 -05:00
Jonathan Swoboda
c2737ca3bb Merge pull request #13247 from esphome/bump-2026.1.0b2
2026.1.0b2
2026-01-15 16:24:08 -05:00
J. Nick Koston
00cc9e44b6 [analyze_memory] Fix ELF section mapping for RTL87xx and LN882X platforms (#13213) 2026-01-15 10:38:24 -10:00
Jonathan Swoboda
c151b2da67 Bump version to 2026.1.0b2 2026-01-15 15:26:04 -05:00
J. Nick Koston
dacd185afb [web_server][captive_portal] Change default compression from Brotli to gzip (#13246) 2026-01-15 15:26:04 -05:00
John Stenger
f88cf1b83a [qr_code] Allocate and free memory for QR code buffer (#13161)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-01-15 15:26:04 -05:00
Jonathan Swoboda
3f6412ba07 [safe_mode] Detect bootloader rollback support at runtime (#13230)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 15:26:04 -05:00
J. Nick Koston
1ad0969099 [core] Fix ESP32-S2/S3 hardware SHA crash by aligning HashBase digest buffer (#13234)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-01-15 15:26:04 -05:00
J. Nick Koston
737c2b8732 [core] Fix platform subcomponents not filtering source files (#13208) 2026-01-15 15:26:04 -05:00
J. Nick Koston
9030dc9d4e [api] Fix state updates being sent to clients that did not subscribe (#13237) 2026-01-15 15:26:04 -05:00
J. Nick Koston
0b5a3506cc [core] Optimize and normalize entity state publishing logs with >> format (#13236) 2026-01-15 15:26:04 -05:00
Clyde Stubbs
3c63ff5e36 [image] Correctly handle dimensions in physical units (#13209) 2026-01-15 15:26:04 -05:00
dependabot[bot]
0427350101 Bump ruff from 0.14.11 to 0.14.12 (#13244)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-15 09:59:40 -10:00
J. Nick Koston
41dceb76ec [web_server][captive_portal] Change default compression from Brotli to gzip (#13246) 2026-01-15 19:56:35 +00:00
John Stenger
6380458d78 [qr_code] Allocate and free memory for QR code buffer (#13161)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-01-15 14:18:08 -05:00
Jonathan Swoboda
0dc5a7c9a4 [safe_mode] Detect bootloader rollback support at runtime (#13230)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 14:17:00 -05:00
J. Nick Koston
9003844eda [core] Fix ESP32-S2/S3 hardware SHA crash by aligning HashBase digest buffer (#13234)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-01-15 18:29:11 +00:00
J. Nick Koston
22a4ec69c2 [core] Fix platform subcomponents not filtering source files (#13208) 2026-01-15 07:38:44 -10:00
J. Nick Koston
9d42bfd161 [api] Fix state updates being sent to clients that did not subscribe (#13237) 2026-01-15 07:38:18 -10:00
J. Nick Koston
49c881d067 [core] Optimize and normalize entity state publishing logs with >> format (#13236) 2026-01-15 10:13:05 +00:00
J. Nick Koston
78aee4f498 [web_server] Remove unused button_state_json_generator (#13235) 2026-01-14 23:48:55 -06:00
Clyde Stubbs
9da2c08f36 [image] Correctly handle dimensions in physical units (#13209) 2026-01-15 03:27:26 +00:00
123 changed files with 2143 additions and 570 deletions

View File

@@ -0,0 +1,96 @@
---
name: pr-workflow
description: Create pull requests for esphome. Use when creating PRs, submitting changes, or preparing contributions.
allowed-tools: Read, Bash, Glob, Grep
---
# ESPHome PR Workflow
When creating a pull request for esphome, follow these steps:
## 1. Create Branch from Upstream
Always base your branch on **upstream** (not origin/fork) to ensure you have the latest code:
```bash
git fetch upstream
git checkout -b <branch-name> upstream/dev
```
## 2. Read the PR Template
Before creating a PR, read `.github/PULL_REQUEST_TEMPLATE.md` to understand required fields.
## 3. Create the PR
Use `gh pr create` with the **full template** filled in. Never skip or abbreviate sections.
Required fields:
- **What does this implement/fix?**: Brief description of changes
- **Types of changes**: Check ONE appropriate box (Bugfix, New feature, Breaking change, etc.)
- **Related issue**: Use `fixes <link>` syntax if applicable
- **Pull request in esphome-docs**: Link if docs are needed
- **Test Environment**: Check platforms you tested on
- **Example config.yaml**: Include working example YAML
- **Checklist**: Verify code is tested and tests added
## 4. Example PR Body
```markdown
# What does this implement/fix?
<describe your changes here>
## Types of changes
- [ ] Bugfix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Developer breaking change (an API change that could break external components)
- [ ] Code quality improvements to existing code or addition of tests
- [ ] Other
**Related issue or feature (if applicable):**
- fixes https://github.com/esphome/esphome/issues/XXX
**Pull request in [esphome-docs](https://github.com/esphome/esphome-docs) with documentation (if applicable):**
- esphome/esphome-docs#XXX
## Test Environment
- [x] ESP32
- [x] ESP32 IDF
- [ ] ESP8266
- [ ] RP2040
- [ ] BK72xx
- [ ] RTL87xx
- [ ] LN882x
- [ ] nRF52840
## Example entry for `config.yaml`:
```yaml
# Example config.yaml
component_name:
id: my_component
option: value
```
## Checklist:
- [x] The code change is tested and works locally.
- [x] Tests have been added to verify that the new code works (under `tests/` folder).
If user exposed functionality or configuration variables are added/changed:
- [ ] Documentation added/updated in [esphome-docs](https://github.com/esphome/esphome-docs).
```
## 5. Push and Create PR
```bash
git push -u origin <branch-name>
gh pr create --repo esphome/esphome --base dev --title "[component] Brief description"
```
Title should be prefixed with the component name in brackets, e.g. `[safe_mode] Add feature`.

View File

@@ -22,7 +22,7 @@ runs:
python-version: ${{ inputs.python-version }}
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with:
path: venv
# yamllint disable-line rule:line-length

View File

@@ -47,7 +47,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with:
path: venv
# yamllint disable-line rule:line-length
@@ -157,7 +157,7 @@ jobs:
token: ${{ secrets.CODECOV_TOKEN }}
- name: Save Python virtual environment cache
if: github.ref == 'refs/heads/dev'
uses: actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
uses: actions/cache/save@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with:
path: venv
key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
@@ -193,7 +193,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Restore components graph cache
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with:
path: .temp/components_graph.json
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
@@ -223,7 +223,7 @@ jobs:
echo "component-test-batches=$(echo "$output" | jq -c '.component_test_batches')" >> $GITHUB_OUTPUT
- name: Save components graph cache
if: github.ref == 'refs/heads/dev'
uses: actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
uses: actions/cache/save@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with:
path: .temp/components_graph.json
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
@@ -245,7 +245,7 @@ jobs:
python-version: "3.13"
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with:
path: venv
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
@@ -334,14 +334,14 @@ jobs:
- name: Cache platformio
if: github.ref == 'refs/heads/dev'
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with:
path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
- name: Cache platformio
if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with:
path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
@@ -413,14 +413,14 @@ jobs:
- name: Cache platformio
if: github.ref == 'refs/heads/dev'
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
- name: Cache platformio
if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
@@ -502,14 +502,14 @@ jobs:
- name: Cache platformio
if: github.ref == 'refs/heads/dev'
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
- name: Cache platformio
if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
@@ -735,7 +735,7 @@ jobs:
- name: Restore cached memory analysis
id: cache-memory-analysis
if: steps.check-script.outputs.skip != 'true'
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with:
path: memory-analysis-target.json
key: ${{ steps.cache-key.outputs.cache-key }}
@@ -759,7 +759,7 @@ jobs:
- name: Cache platformio
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with:
path: ~/.platformio
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
@@ -800,7 +800,7 @@ jobs:
- name: Save memory analysis to cache
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success'
uses: actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
uses: actions/cache/save@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with:
path: memory-analysis-target.json
key: ${{ steps.cache-key.outputs.cache-key }}
@@ -847,7 +847,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache platformio
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with:
path: ~/.platformio
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}

View File

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

View File

@@ -222,8 +222,13 @@ def choose_upload_log_host(
else:
resolved.append(device)
if not resolved:
if CORE.dashboard:
hint = "If you know the IP, set 'use_address' in your network config."
else:
hint = "If you know the IP, try --device <IP>"
raise EsphomeError(
f"All specified devices {defaults} could not be resolved. Is the device connected to the network?"
f"All specified devices {defaults} could not be resolved. "
f"Is the device connected to the network? {hint}"
)
return resolved

View File

@@ -22,7 +22,7 @@ from .helpers import (
map_section_name,
parse_symbol_line,
)
from .toolchain import find_tool, run_tool
from .toolchain import find_tool, resolve_tool_path, run_tool
if TYPE_CHECKING:
from esphome.platformio_api import IDEData
@@ -132,6 +132,12 @@ class MemoryAnalyzer:
readelf_path = readelf_path or idedata.readelf_path
_LOGGER.debug("Using toolchain paths from PlatformIO idedata")
# Validate paths exist, fall back to find_tool if they don't
# This handles cases like Zephyr where cc_path doesn't include full path
# and the toolchain prefix may differ (e.g., arm-zephyr-eabi- vs arm-none-eabi-)
objdump_path = resolve_tool_path("objdump", objdump_path, objdump_path)
readelf_path = resolve_tool_path("readelf", readelf_path, objdump_path)
self.objdump_path = objdump_path or "objdump"
self.readelf_path = readelf_path or "readelf"
self.external_components = external_components or set()

View File

@@ -9,11 +9,61 @@ ESPHOME_COMPONENT_PATTERN = re.compile(r"esphome::([a-zA-Z0-9_]+)::")
# Maps standard section names to their various platform-specific variants
# Note: Order matters! More specific patterns (.bss) must come before general ones (.dram)
# because ESP-IDF uses names like ".dram0.bss" which would match ".dram" otherwise
#
# Platform-specific sections:
# - ESP8266/ESP32: .iram*, .dram*
# - LibreTiny RTL87xx: .xip.code_* (flash), .ram.code_* (RAM)
# - LibreTiny BK7231: .itcm.code (fast RAM), .vectors (interrupt vectors)
# - LibreTiny LN882X: .flash_text, .flash_copy* (flash code)
# - Zephyr/nRF52: text, rodata, datas, bss (no leading dots)
SECTION_MAPPING = {
".text": frozenset([".text", ".iram"]),
".rodata": frozenset([".rodata"]),
".bss": frozenset([".bss"]), # Must be before .data to catch ".dram0.bss"
".data": frozenset([".data", ".dram"]),
".text": frozenset(
[
".text",
".iram",
# LibreTiny RTL87xx XIP (eXecute In Place) flash code
".xip.code",
# LibreTiny RTL87xx RAM code
".ram.code_text",
# LibreTiny BK7231 fast RAM code and vectors
".itcm.code",
".vectors",
# LibreTiny LN882X flash code
".flash_text",
".flash_copy",
# Zephyr/nRF52 sections (no leading dots)
"text",
"rom_start",
]
),
".rodata": frozenset(
[
".rodata",
# LibreTiny RTL87xx read-only data in RAM
".ram.code_rodata",
# Zephyr/nRF52 sections (no leading dots)
"rodata",
]
),
# .bss patterns - must be before .data to catch ".dram0.bss"
".bss": frozenset(
[
".bss",
# LibreTiny LN882X BSS
".bss_ram",
# Zephyr/nRF52 sections (no leading dots)
"bss",
"noinit",
]
),
".data": frozenset(
[
".data",
".dram",
# Zephyr/nRF52 sections (no leading dots)
"datas",
]
),
}
# Section to ComponentMemory attribute mapping

View File

@@ -94,13 +94,13 @@ def parse_symbol_line(line: str) -> tuple[str, str, int, str] | None:
return None
# Find section, size, and name
# Try each part as a potential section name
for i, part in enumerate(parts):
if not part.startswith("."):
continue
# Skip parts that are clearly flags, addresses, or other metadata
# Sections start with '.' (standard ELF) or are known section names (Zephyr)
section = map_section_name(part)
if not section:
break
continue
# Need at least size field after section
if i + 1 >= len(parts):

View File

@@ -3,6 +3,7 @@
from __future__ import annotations
import logging
import os
from pathlib import Path
import subprocess
from typing import TYPE_CHECKING
@@ -17,10 +18,82 @@ TOOLCHAIN_PREFIXES = [
"xtensa-lx106-elf-", # ESP8266
"xtensa-esp32-elf-", # ESP32
"xtensa-esp-elf-", # ESP32 (newer IDF)
"arm-zephyr-eabi-", # nRF52/Zephyr SDK
"arm-none-eabi-", # Generic ARM (RP2040, etc.)
"", # System default (no prefix)
]
def _find_in_platformio_packages(tool_name: str) -> str | None:
"""Search for a tool in PlatformIO package directories.
This handles cases like Zephyr SDK where tools are installed in nested
directories that aren't in PATH.
Args:
tool_name: Name of the tool (e.g., "readelf", "objdump")
Returns:
Full path to the tool or None if not found
"""
# Get PlatformIO packages directory
platformio_home = Path(os.path.expanduser("~/.platformio/packages"))
if not platformio_home.exists():
return None
# Search patterns for toolchains that might contain the tool
# Order matters - more specific patterns first
search_patterns = [
# Zephyr SDK deeply nested structure (4 levels)
# e.g., toolchain-gccarmnoneeabi/zephyr-sdk-0.17.4/arm-zephyr-eabi/bin/arm-zephyr-eabi-objdump
f"toolchain-*/*/*/bin/*-{tool_name}",
# Zephyr SDK nested structure (3 levels)
f"toolchain-*/*/bin/*-{tool_name}",
f"toolchain-*/bin/*-{tool_name}",
# Standard PlatformIO toolchain structure
f"toolchain-*/bin/*{tool_name}",
]
for pattern in search_patterns:
matches = list(platformio_home.glob(pattern))
if matches:
# Sort to get consistent results, prefer arm-zephyr-eabi over arm-none-eabi
matches.sort(key=lambda p: ("zephyr" not in str(p), str(p)))
tool_path = str(matches[0])
_LOGGER.debug("Found %s in PlatformIO packages: %s", tool_name, tool_path)
return tool_path
return None
def resolve_tool_path(
tool_name: str,
derived_path: str | None,
objdump_path: str | None = None,
) -> str | None:
"""Resolve a tool path, falling back to find_tool if derived path doesn't exist.
Args:
tool_name: Name of the tool (e.g., "objdump", "readelf")
derived_path: Path derived from idedata (may not exist for some platforms)
objdump_path: Path to objdump binary to derive other tool paths from
Returns:
Resolved path to the tool, or the original derived_path if it exists
"""
if derived_path and not Path(derived_path).exists():
found = find_tool(tool_name, objdump_path)
if found:
_LOGGER.debug(
"Derived %s path %s not found, using %s",
tool_name,
derived_path,
found,
)
return found
return derived_path
def find_tool(
tool_name: str,
objdump_path: str | None = None,
@@ -28,7 +101,8 @@ def find_tool(
"""Find a toolchain tool by name.
First tries to derive the tool path from objdump_path (if provided),
then falls back to searching for platform-specific tools.
then searches PlatformIO package directories (for cross-compile toolchains),
and finally falls back to searching for platform-specific tools in PATH.
Args:
tool_name: Name of the tool (e.g., "objdump", "nm", "c++filt")
@@ -47,7 +121,13 @@ def find_tool(
_LOGGER.debug("Found %s at: %s", tool_name, potential_path)
return potential_path
# Try platform-specific tools
# Search in PlatformIO packages directory first (handles Zephyr SDK, etc.)
# This must come before PATH search because system tools (e.g., /usr/bin/objdump)
# are for the host architecture, not the target (ARM, Xtensa, etc.)
if found := _find_in_platformio_packages(tool_name):
return found
# Try platform-specific tools in PATH (fallback for when tools are installed globally)
for prefix in TOOLCHAIN_PREFIXES:
cmd = f"{prefix}{tool_name}"
try:

View File

@@ -31,7 +31,8 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) {
this->last_update_ = millis();
if (state != this->current_state_) {
auto prev_state = this->current_state_;
ESP_LOGD(TAG, "Set state to: %s, previous: %s", LOG_STR_ARG(alarm_control_panel_state_to_string(state)),
ESP_LOGD(TAG, "'%s' >> %s (was %s)", this->get_name().c_str(),
LOG_STR_ARG(alarm_control_panel_state_to_string(state)),
LOG_STR_ARG(alarm_control_panel_state_to_string(prev_state)));
this->current_state_ = state;
// Single state callback - triggers check get_state() for specific states

View File

@@ -241,8 +241,10 @@ void APIServer::handle_disconnect(APIConnection *conn) {}
void APIServer::on_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \
if (obj->is_internal()) \
return; \
for (auto &c : this->clients_) \
c->send_##entity_name##_state(obj); \
for (auto &c : this->clients_) { \
if (c->flags_.state_subscription) \
c->send_##entity_name##_state(obj); \
} \
}
#ifdef USE_BINARY_SENSOR
@@ -321,8 +323,10 @@ API_DISPATCH_UPDATE(water_heater::WaterHeater, water_heater)
void APIServer::on_event(event::Event *obj) {
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_event(obj);
for (auto &c : this->clients_) {
if (c->flags_.state_subscription)
c->send_event(obj);
}
}
#endif
@@ -331,8 +335,10 @@ void APIServer::on_event(event::Event *obj) {
void APIServer::on_update(update::UpdateEntity *obj) {
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_update_state(obj);
for (auto &c : this->clients_) {
if (c->flags_.state_subscription)
c->send_update_state(obj);
}
}
#endif
@@ -552,8 +558,10 @@ bool APIServer::clear_noise_psk(bool make_active) {
#ifdef USE_HOMEASSISTANT_TIME
void APIServer::request_time() {
for (auto &client : this->clients_) {
if (!client->flags_.remove && client->is_authenticated())
if (!client->flags_.remove && client->is_authenticated()) {
client->send_time_request();
return; // Only request from one client to avoid clock conflicts
}
}
}
#endif

View File

@@ -48,14 +48,14 @@ uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size
}
uint32_t field_length = res->as_uint32();
ptr += consumed;
if (ptr + field_length > end) {
if (field_length > static_cast<size_t>(end - ptr)) {
return count; // Out of bounds
}
ptr += field_length;
break;
}
case WIRE_TYPE_FIXED32: { // 32-bit - skip 4 bytes
if (ptr + 4 > end) {
if (end - ptr < 4) {
return count;
}
ptr += 4;
@@ -110,7 +110,7 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
}
uint32_t field_length = res->as_uint32();
ptr += consumed;
if (ptr + field_length > end) {
if (field_length > static_cast<size_t>(end - ptr)) {
ESP_LOGV(TAG, "Out-of-bounds Length Delimited at offset %ld", (long) (ptr - buffer));
return;
}
@@ -121,7 +121,7 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
break;
}
case WIRE_TYPE_FIXED32: { // 32-bit
if (ptr + 4 > end) {
if (end - ptr < 4) {
ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at offset %ld", (long) (ptr - buffer));
return;
}

View File

@@ -158,12 +158,14 @@ void ATM90E32Component::setup() {
if (this->enable_offset_calibration_) {
// Initialize flash storage for offset calibrations
uint32_t o_hash = fnv1_hash(std::string("_offset_calibration_") + this->cs_summary_);
uint32_t o_hash = fnv1_hash("_offset_calibration_");
o_hash = fnv1_hash_extend(o_hash, this->cs_summary_);
this->offset_pref_ = global_preferences->make_preference<OffsetCalibration[3]>(o_hash, true);
this->restore_offset_calibrations_();
// Initialize flash storage for power offset calibrations
uint32_t po_hash = fnv1_hash(std::string("_power_offset_calibration_") + this->cs_summary_);
uint32_t po_hash = fnv1_hash("_power_offset_calibration_");
po_hash = fnv1_hash_extend(po_hash, this->cs_summary_);
this->power_offset_pref_ = global_preferences->make_preference<PowerOffsetCalibration[3]>(po_hash, true);
this->restore_power_offset_calibrations_();
} else {
@@ -183,7 +185,8 @@ void ATM90E32Component::setup() {
if (this->enable_gain_calibration_) {
// Initialize flash storage for gain calibration
uint32_t g_hash = fnv1_hash(std::string("_gain_calibration_") + this->cs_summary_);
uint32_t g_hash = fnv1_hash("_gain_calibration_");
g_hash = fnv1_hash_extend(g_hash, this->cs_summary_);
this->gain_calibration_pref_ = global_preferences->make_preference<GainCalibration[3]>(g_hash, true);
this->restore_gain_calibrations_();

View File

@@ -44,7 +44,7 @@ bool BinarySensor::set_new_state(const optional<bool> &new_state) {
#if defined(USE_BINARY_SENSOR) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_binary_sensor_update(this);
#endif
ESP_LOGD(TAG, "'%s': %s", this->get_name().c_str(), ONOFFMAYBE(new_state));
ESP_LOGD(TAG, "'%s' >> %s", this->get_name().c_str(), ONOFFMAYBE(new_state));
return true;
}
return false;

View File

@@ -44,7 +44,7 @@ CONFIG_SCHEMA = cv.All(
cv.GenerateID(CONF_WEB_SERVER_BASE_ID): cv.use_id(
web_server_base.WebServerBase
),
cv.Optional(CONF_COMPRESSION, default="br"): cv.one_of("br", "gzip"),
cv.Optional(CONF_COMPRESSION, default="gzip"): cv.one_of("gzip", "br"),
}
).extend(cv.COMPONENT_SCHEMA),
cv.only_on(

View File

@@ -436,7 +436,7 @@ void Climate::save_state_() {
}
void Climate::publish_state() {
ESP_LOGD(TAG, "'%s' - Sending state:", this->name_.c_str());
ESP_LOGD(TAG, "'%s' >>", this->name_.c_str());
auto traits = this->get_traits();
ESP_LOGD(TAG, " Mode: %s", LOG_STR_ARG(climate_mode_to_string(this->mode)));

View File

@@ -153,7 +153,7 @@ void Cover::publish_state(bool save) {
this->position = clamp(this->position, 0.0f, 1.0f);
this->tilt = clamp(this->tilt, 0.0f, 1.0f);
ESP_LOGD(TAG, "'%s' - Publishing:", this->name_.c_str());
ESP_LOGD(TAG, "'%s' >>", this->name_.c_str());
auto traits = this->get_traits();
if (traits.get_supports_position()) {
ESP_LOGD(TAG, " Position: %.0f%%", this->position * 100.0f);

View File

@@ -44,7 +44,7 @@ void DallasTemperatureSensor::update() {
this->send_command_(DALLAS_COMMAND_START_CONVERSION);
this->set_timeout(this->get_address_name(), this->millis_to_wait_for_conversion_(), [this] {
this->set_timeout(this->get_address_name().c_str(), this->millis_to_wait_for_conversion_(), [this] {
if (!this->read_scratch_pad_() || !this->check_scratch_pad_()) {
this->publish_state(NAN);
return;

View File

@@ -30,7 +30,7 @@ void DateEntity::publish_state() {
return;
}
this->set_has_state(true);
ESP_LOGD(TAG, "'%s': Sending date %d-%d-%d", this->get_name().c_str(), this->year_, this->month_, this->day_);
ESP_LOGD(TAG, "'%s' >> %d-%d-%d", this->get_name().c_str(), this->year_, this->month_, this->day_);
this->state_callback_.call();
#if defined(USE_DATETIME_DATE) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_date_update(this);

View File

@@ -45,8 +45,8 @@ void DateTimeEntity::publish_state() {
return;
}
this->set_has_state(true);
ESP_LOGD(TAG, "'%s': Sending datetime %04u-%02u-%02u %02d:%02d:%02d", this->get_name().c_str(), this->year_,
this->month_, this->day_, this->hour_, this->minute_, this->second_);
ESP_LOGD(TAG, "'%s' >> %04u-%02u-%02u %02d:%02d:%02d", this->get_name().c_str(), this->year_, this->month_,
this->day_, this->hour_, this->minute_, this->second_);
this->state_callback_.call();
#if defined(USE_DATETIME_DATETIME) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_datetime_update(this);

View File

@@ -26,8 +26,7 @@ void TimeEntity::publish_state() {
return;
}
this->set_has_state(true);
ESP_LOGD(TAG, "'%s': Sending time %02d:%02d:%02d", this->get_name().c_str(), this->hour_, this->minute_,
this->second_);
ESP_LOGD(TAG, "'%s' >> %02d:%02d:%02d", this->get_name().c_str(), this->hour_, this->minute_, this->second_);
this->state_callback_.call();
#if defined(USE_DATETIME_TIME) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_time_update(this);

View File

@@ -74,8 +74,11 @@ class DebugComponent : public PollingComponent {
#ifdef USE_SENSOR
void set_free_sensor(sensor::Sensor *free_sensor) { free_sensor_ = free_sensor; }
void set_block_sensor(sensor::Sensor *block_sensor) { block_sensor_ = block_sensor; }
#if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)
#if (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)) || defined(USE_ESP32)
void set_fragmentation_sensor(sensor::Sensor *fragmentation_sensor) { fragmentation_sensor_ = fragmentation_sensor; }
#endif
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
void set_min_free_sensor(sensor::Sensor *min_free_sensor) { min_free_sensor_ = min_free_sensor; }
#endif
void set_loop_time_sensor(sensor::Sensor *loop_time_sensor) { loop_time_sensor_ = loop_time_sensor; }
#ifdef USE_ESP32
@@ -97,8 +100,11 @@ class DebugComponent : public PollingComponent {
sensor::Sensor *free_sensor_{nullptr};
sensor::Sensor *block_sensor_{nullptr};
#if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)
#if (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)) || defined(USE_ESP32)
sensor::Sensor *fragmentation_sensor_{nullptr};
#endif
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
sensor::Sensor *min_free_sensor_{nullptr};
#endif
sensor::Sensor *loop_time_sensor_{nullptr};
#ifdef USE_ESP32

View File

@@ -234,8 +234,19 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
void DebugComponent::update_platform_() {
#ifdef USE_SENSOR
uint32_t max_alloc = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL);
if (this->block_sensor_ != nullptr) {
this->block_sensor_->publish_state(heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL));
this->block_sensor_->publish_state(max_alloc);
}
if (this->min_free_sensor_ != nullptr) {
this->min_free_sensor_->publish_state(heap_caps_get_minimum_free_size(MALLOC_CAP_INTERNAL));
}
if (this->fragmentation_sensor_ != nullptr) {
uint32_t free_heap = heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
if (free_heap > 0) {
float fragmentation = 100.0f - (100.0f * max_alloc / free_heap);
this->fragmentation_sensor_->publish_state(fragmentation);
}
}
if (this->psram_sensor_ != nullptr) {
this->psram_sensor_->publish_state(heap_caps_get_free_size(MALLOC_CAP_SPIRAM));

View File

@@ -51,6 +51,9 @@ void DebugComponent::update_platform_() {
if (this->block_sensor_ != nullptr) {
this->block_sensor_->publish_state(lt_heap_get_max_alloc());
}
if (this->min_free_sensor_ != nullptr) {
this->min_free_sensor_->publish_state(lt_heap_get_min_free());
}
#endif
}

View File

@@ -11,16 +11,24 @@ from esphome.const import (
ENTITY_CATEGORY_DIAGNOSTIC,
ICON_COUNTER,
ICON_TIMER,
PLATFORM_BK72XX,
PLATFORM_LN882X,
PLATFORM_RTL87XX,
UNIT_BYTES,
UNIT_HERTZ,
UNIT_MILLISECOND,
UNIT_PERCENT,
)
from . import CONF_DEBUG_ID, DebugComponent
from . import ( # noqa: F401 pylint: disable=unused-import
CONF_DEBUG_ID,
FILTER_SOURCE_FILES,
DebugComponent,
)
DEPENDENCIES = ["debug"]
CONF_MIN_FREE = "min_free"
CONF_PSRAM = "psram"
CONFIG_SCHEMA = {
@@ -38,8 +46,14 @@ CONFIG_SCHEMA = {
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
cv.Optional(CONF_FRAGMENTATION): cv.All(
cv.only_on_esp8266,
cv.require_framework_version(esp8266_arduino=cv.Version(2, 5, 2)),
cv.Any(
cv.All(
cv.only_on_esp8266,
cv.require_framework_version(esp8266_arduino=cv.Version(2, 5, 2)),
),
cv.only_on_esp32,
msg="This feature is only available on ESP8266 (Arduino 2.5.2+) and ESP32",
),
sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
icon=ICON_COUNTER,
@@ -47,6 +61,19 @@ CONFIG_SCHEMA = {
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
),
cv.Optional(CONF_MIN_FREE): cv.All(
cv.Any(
cv.only_on_esp32,
cv.only_on([PLATFORM_BK72XX, PLATFORM_LN882X, PLATFORM_RTL87XX]),
msg="This feature is only available on ESP32 and LibreTiny (BK72xx, LN882x, RTL87xx)",
),
sensor.sensor_schema(
unit_of_measurement=UNIT_BYTES,
icon=ICON_COUNTER,
accuracy_decimals=0,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
),
cv.Optional(CONF_LOOP_TIME): sensor.sensor_schema(
unit_of_measurement=UNIT_MILLISECOND,
icon=ICON_TIMER,
@@ -89,6 +116,10 @@ async def to_code(config):
sens = await sensor.new_sensor(fragmentation_conf)
cg.add(debug_component.set_fragmentation_sensor(sens))
if min_free_conf := config.get(CONF_MIN_FREE):
sens = await sensor.new_sensor(min_free_conf)
cg.add(debug_component.set_min_free_sensor(sens))
if loop_time_conf := config.get(CONF_LOOP_TIME):
sens = await sensor.new_sensor(loop_time_conf)
cg.add(debug_component.set_loop_time_sensor(sens))

View File

@@ -8,7 +8,11 @@ from esphome.const import (
ICON_RESTART,
)
from . import CONF_DEBUG_ID, DebugComponent
from . import ( # noqa: F401 pylint: disable=unused-import
CONF_DEBUG_ID,
FILTER_SOURCE_FILES,
DebugComponent,
)
DEPENDENCIES = ["debug"]

View File

@@ -193,10 +193,18 @@ void BLEClientBase::log_event_(const char *name) {
ESP_LOGD(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_, name);
}
void BLEClientBase::log_gattc_event_(const char *name) {
void BLEClientBase::log_gattc_lifecycle_event_(const char *name) {
ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_%s_EVT", this->connection_index_, this->address_str_, name);
}
void BLEClientBase::log_gattc_data_event_(const char *name) {
// Data transfer events are logged at VERBOSE level because logging to UART creates
// delays that cause timing issues during time-sensitive BLE operations. This is
// especially problematic during pairing or firmware updates which require rapid
// writes to many characteristics - the log spam can cause these operations to fail.
ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_%s_EVT", this->connection_index_, this->address_str_, name);
}
void BLEClientBase::log_gattc_warning_(const char *operation, esp_gatt_status_t status) {
ESP_LOGW(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str_, operation, status);
}
@@ -280,7 +288,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
case ESP_GATTC_OPEN_EVT: {
if (!this->check_addr(param->open.remote_bda))
return false;
this->log_gattc_event_("OPEN");
this->log_gattc_lifecycle_event_("OPEN");
// conn_id was already set in ESP_GATTC_CONNECT_EVT
this->service_count_ = 0;
@@ -331,7 +339,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
case ESP_GATTC_CONNECT_EVT: {
if (!this->check_addr(param->connect.remote_bda))
return false;
this->log_gattc_event_("CONNECT");
this->log_gattc_lifecycle_event_("CONNECT");
this->conn_id_ = param->connect.conn_id;
// Start MTU negotiation immediately as recommended by ESP-IDF examples
// (gatt_client, ble_throughput) which call esp_ble_gattc_send_mtu_req in
@@ -376,7 +384,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
case ESP_GATTC_CLOSE_EVT: {
if (this->conn_id_ != param->close.conn_id)
return false;
this->log_gattc_event_("CLOSE");
this->log_gattc_lifecycle_event_("CLOSE");
this->release_services();
this->set_state(espbt::ClientState::IDLE);
this->conn_id_ = UNSET_CONN_ID;
@@ -404,7 +412,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
case ESP_GATTC_SEARCH_CMPL_EVT: {
if (this->conn_id_ != param->search_cmpl.conn_id)
return false;
this->log_gattc_event_("SEARCH_CMPL");
this->log_gattc_lifecycle_event_("SEARCH_CMPL");
// For V3_WITHOUT_CACHE, switch back to medium connection parameters after service discovery
// This balances performance with bandwidth usage after the critical discovery phase
if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
@@ -431,35 +439,35 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
case ESP_GATTC_READ_DESCR_EVT: {
if (this->conn_id_ != param->write.conn_id)
return false;
this->log_gattc_event_("READ_DESCR");
this->log_gattc_data_event_("READ_DESCR");
break;
}
case ESP_GATTC_WRITE_DESCR_EVT: {
if (this->conn_id_ != param->write.conn_id)
return false;
this->log_gattc_event_("WRITE_DESCR");
this->log_gattc_data_event_("WRITE_DESCR");
break;
}
case ESP_GATTC_WRITE_CHAR_EVT: {
if (this->conn_id_ != param->write.conn_id)
return false;
this->log_gattc_event_("WRITE_CHAR");
this->log_gattc_data_event_("WRITE_CHAR");
break;
}
case ESP_GATTC_READ_CHAR_EVT: {
if (this->conn_id_ != param->read.conn_id)
return false;
this->log_gattc_event_("READ_CHAR");
this->log_gattc_data_event_("READ_CHAR");
break;
}
case ESP_GATTC_NOTIFY_EVT: {
if (this->conn_id_ != param->notify.conn_id)
return false;
this->log_gattc_event_("NOTIFY");
this->log_gattc_data_event_("NOTIFY");
break;
}
case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
this->log_gattc_event_("REG_FOR_NOTIFY");
this->log_gattc_data_event_("REG_FOR_NOTIFY");
if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
// Client is responsible for flipping the descriptor value
@@ -491,7 +499,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
esp_err_t status =
esp_ble_gattc_write_char_descr(this->gattc_if_, this->conn_id_, desc_result.handle, sizeof(notify_en),
(uint8_t *) &notify_en, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE);
ESP_LOGD(TAG, "Wrote notify descriptor %d, properties=%d", notify_en, char_result.properties);
ESP_LOGV(TAG, "Wrote notify descriptor %d, properties=%d", notify_en, char_result.properties);
if (status) {
this->log_gattc_warning_("esp_ble_gattc_write_char_descr", status);
}
@@ -499,13 +507,13 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
}
case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: {
this->log_gattc_event_("UNREG_FOR_NOTIFY");
this->log_gattc_data_event_("UNREG_FOR_NOTIFY");
break;
}
default:
// ideally would check all other events for matching conn_id
ESP_LOGD(TAG, "[%d] [%s] Event %d", this->connection_index_, this->address_str_, event);
// Unknown events logged at VERBOSE to avoid UART delays during time-sensitive operations
ESP_LOGV(TAG, "[%d] [%s] Event %d", this->connection_index_, this->address_str_, event);
break;
}
return true;

View File

@@ -127,7 +127,8 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
// 6 bytes used, 2 bytes padding
void log_event_(const char *name);
void log_gattc_event_(const char *name);
void log_gattc_lifecycle_event_(const char *name);
void log_gattc_data_event_(const char *name);
void update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout,
const char *param_type);
void set_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout,

View File

@@ -294,8 +294,7 @@ bool Esp32HostedUpdate::stream_firmware_to_coprocessor_() {
}
// Stream firmware to coprocessor while computing SHA256
// Hardware SHA acceleration requires 32-byte alignment on some chips (ESP32-S3 with IDF 5.5.x+)
alignas(32) sha256::SHA256 hasher;
sha256::SHA256 hasher;
hasher.init();
uint8_t buffer[CHUNK_SIZE];
@@ -352,8 +351,7 @@ bool Esp32HostedUpdate::write_embedded_firmware_to_coprocessor_() {
}
// Verify SHA256 before writing
// Hardware SHA acceleration requires 32-byte alignment on some chips (ESP32-S3 with IDF 5.5.x+)
alignas(32) sha256::SHA256 hasher;
sha256::SHA256 hasher;
hasher.init();
hasher.add(this->firmware_data_, this->firmware_size_);
hasher.calculate();

View File

@@ -563,11 +563,9 @@ bool ESPHomeOTAComponent::handle_auth_send_() {
// [1+hex_size...1+2*hex_size-1]: cnonce (hex_size bytes) - client's nonce
// [1+2*hex_size...1+3*hex_size-1]: response (hex_size bytes) - client's hash
// CRITICAL ESP32-S3 HARDWARE SHA ACCELERATION: Hash object must stay in same stack frame
// CRITICAL ESP32-S2/S3 HARDWARE SHA ACCELERATION: Hash object must stay in same stack frame
// (no passing to other functions). All hash operations must happen in this function.
// NOTE: On ESP32-S3 with IDF 5.5.x, the SHA256 context must be properly aligned for
// hardware SHA acceleration DMA operations.
alignas(32) sha256::SHA256 hasher;
sha256::SHA256 hasher;
const size_t hex_size = hasher.get_size() * 2;
const size_t nonce_len = hasher.get_size() / 4;
@@ -639,11 +637,9 @@ bool ESPHomeOTAComponent::handle_auth_read_() {
const char *cnonce = nonce + hex_size;
const char *response = cnonce + hex_size;
// CRITICAL ESP32-S3 HARDWARE SHA ACCELERATION: Hash object must stay in same stack frame
// CRITICAL ESP32-S2/S3 HARDWARE SHA ACCELERATION: Hash object must stay in same stack frame
// (no passing to other functions). All hash operations must happen in this function.
// NOTE: On ESP32-S3 with IDF 5.5.x, the SHA256 context must be properly aligned for
// hardware SHA acceleration DMA operations.
alignas(32) sha256::SHA256 hasher;
sha256::SHA256 hasher;
hasher.init();
hasher.add(this->password_.c_str(), this->password_.length());

View File

@@ -22,7 +22,7 @@ void Event::trigger(const std::string &event_type) {
return;
}
this->last_event_type_ = found;
ESP_LOGD(TAG, "'%s' Triggered event '%s'", this->get_name().c_str(), this->last_event_type_);
ESP_LOGD(TAG, "'%s' >> '%s'", this->get_name().c_str(), this->last_event_type_);
this->event_callback_.call(event_type);
#if defined(USE_EVENT) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_event(this);

View File

@@ -201,7 +201,7 @@ void Fan::publish_state() {
auto traits = this->get_traits();
ESP_LOGD(TAG,
"'%s' - Sending state:\n"
"'%s' >>\n"
" State: %s",
this->name_.c_str(), ONOFF(this->state));
if (traits.supports_speed()) {

View File

@@ -1,4 +1,3 @@
#include <cstdio>
#include <cstring>
#include "hmac_sha256.h"
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_HOST)
@@ -26,9 +25,7 @@ void HmacSHA256::calculate() { mbedtls_md_hmac_finish(&this->ctx_, this->digest_
void HmacSHA256::get_bytes(uint8_t *output) { memcpy(output, this->digest_, SHA256_DIGEST_SIZE); }
void HmacSHA256::get_hex(char *output) {
for (size_t i = 0; i < SHA256_DIGEST_SIZE; i++) {
sprintf(output + (i * 2), "%02x", this->digest_[i]);
}
format_hex_to(output, SHA256_DIGEST_SIZE * 2 + 1, this->digest_, SHA256_DIGEST_SIZE);
}
bool HmacSHA256::equals_bytes(const uint8_t *expected) {

View File

@@ -242,9 +242,7 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
return;
}
size_t content_length = container->content_length;
size_t max_length = std::min(content_length, this->max_response_buffer_size_);
size_t max_length = this->max_response_buffer_size_;
#ifdef USE_HTTP_REQUEST_RESPONSE
if (this->capture_response_.value(x...)) {
std::string response_body;

View File

@@ -213,18 +213,12 @@ int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
const uint32_t start = millis();
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
int bufsize = std::min(max_len, this->content_length - this->bytes_read_);
if (bufsize == 0) {
this->duration_ms += (millis() - start);
return 0;
this->feed_wdt();
int read_len = esp_http_client_read(this->client_, (char *) buf, max_len);
this->feed_wdt();
if (read_len > 0) {
this->bytes_read_ += read_len;
}
this->feed_wdt();
int read_len = esp_http_client_read(this->client_, (char *) buf, bufsize);
this->feed_wdt();
this->bytes_read_ += read_len;
this->duration_ms += (millis() - start);
return read_len;

View File

@@ -1,3 +1,4 @@
import logging
from typing import Any
from esphome import automation, pins
@@ -18,13 +19,16 @@ from esphome.const import (
CONF_ROTATION,
CONF_UPDATE_INTERVAL,
)
from esphome.core import ID
from esphome.core import ID, EnumValue
from esphome.cpp_generator import MockObj, TemplateArgsType
import esphome.final_validate as fv
from esphome.helpers import add_class_to_obj
from esphome.types import ConfigType
from . import boards, hub75_ns
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ["esp32"]
CODEOWNERS = ["@stuartparmenter"]
@@ -120,13 +124,51 @@ PANEL_LAYOUTS = {
}
Hub75ScanWiring = cg.global_ns.enum("Hub75ScanWiring", is_class=True)
SCAN_PATTERNS = {
SCAN_WIRINGS = {
"STANDARD_TWO_SCAN": Hub75ScanWiring.STANDARD_TWO_SCAN,
"FOUR_SCAN_16PX_HIGH": Hub75ScanWiring.FOUR_SCAN_16PX_HIGH,
"FOUR_SCAN_32PX_HIGH": Hub75ScanWiring.FOUR_SCAN_32PX_HIGH,
"FOUR_SCAN_64PX_HIGH": Hub75ScanWiring.FOUR_SCAN_64PX_HIGH,
"SCAN_1_4_16PX_HIGH": Hub75ScanWiring.SCAN_1_4_16PX_HIGH,
"SCAN_1_8_32PX_HIGH": Hub75ScanWiring.SCAN_1_8_32PX_HIGH,
"SCAN_1_8_40PX_HIGH": Hub75ScanWiring.SCAN_1_8_40PX_HIGH,
"SCAN_1_8_64PX_HIGH": Hub75ScanWiring.SCAN_1_8_64PX_HIGH,
}
# Deprecated scan wiring names - mapped to new names
DEPRECATED_SCAN_WIRINGS = {
"FOUR_SCAN_16PX_HIGH": "SCAN_1_4_16PX_HIGH",
"FOUR_SCAN_32PX_HIGH": "SCAN_1_8_32PX_HIGH",
"FOUR_SCAN_64PX_HIGH": "SCAN_1_8_64PX_HIGH",
}
def _validate_scan_wiring(value):
"""Validate scan_wiring with deprecation warnings for old names."""
value = cv.string(value).upper().replace(" ", "_")
# Check if using deprecated name
# Remove deprecated names in 2026.7.0
if value in DEPRECATED_SCAN_WIRINGS:
new_name = DEPRECATED_SCAN_WIRINGS[value]
_LOGGER.warning(
"Scan wiring '%s' is deprecated and will be removed in ESPHome 2026.7.0. "
"Please use '%s' instead.",
value,
new_name,
)
value = new_name
# Validate against allowed values
if value not in SCAN_WIRINGS:
raise cv.Invalid(
f"Unknown scan wiring '{value}'. "
f"Valid options are: {', '.join(sorted(SCAN_WIRINGS.keys()))}"
)
# Return as EnumValue like cv.enum does
result = add_class_to_obj(value, EnumValue)
result.enum_value = SCAN_WIRINGS[value]
return result
Hub75ClockSpeed = cg.global_ns.enum("Hub75ClockSpeed", is_class=True)
CLOCK_SPEEDS = {
"8MHZ": Hub75ClockSpeed.HZ_8M,
@@ -382,9 +424,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_LAYOUT_COLS): cv.positive_int,
cv.Optional(CONF_LAYOUT): cv.enum(PANEL_LAYOUTS, upper=True, space="_"),
# Panel hardware configuration
cv.Optional(CONF_SCAN_WIRING): cv.enum(
SCAN_PATTERNS, upper=True, space="_"
),
cv.Optional(CONF_SCAN_WIRING): _validate_scan_wiring,
cv.Optional(CONF_SHIFT_DRIVER): cv.enum(SHIFT_DRIVERS, upper=True),
# Display configuration
cv.Optional(CONF_DOUBLE_BUFFER): cv.boolean,
@@ -547,7 +587,7 @@ def _build_config_struct(
async def to_code(config: ConfigType) -> None:
add_idf_component(
name="esphome/esp-hub75",
ref="0.2.2",
ref="0.3.0",
)
# Set compile-time configuration via build flags (so external library sees them)

View File

@@ -665,15 +665,10 @@ async def write_image(config, all_frames=False):
if is_svg_file(path):
import resvg_py
if resize:
width, height = resize
# resvg-py allows rendering by width/height directly
image_data = resvg_py.svg_to_bytes(
svg_path=str(path), width=int(width), height=int(height)
)
else:
# Default size
image_data = resvg_py.svg_to_bytes(svg_path=str(path))
resize = resize or (None, None)
image_data = resvg_py.svg_to_bytes(
svg_path=str(path), width=resize[0], height=resize[1], dpi=100
)
# Convert bytes to Pillow Image
image = Image.open(io.BytesIO(image_data))

View File

@@ -18,7 +18,15 @@ InfraredCall &InfraredCall::set_carrier_frequency(uint32_t frequency) {
InfraredCall &InfraredCall::set_raw_timings(const std::vector<int32_t> &timings) {
this->raw_timings_ = &timings;
this->packed_data_ = nullptr; // Clear packed if vector is set
this->packed_data_ = nullptr;
this->base64url_ptr_ = nullptr;
return *this;
}
InfraredCall &InfraredCall::set_raw_timings_base64url(const std::string &base64url) {
this->base64url_ptr_ = &base64url;
this->raw_timings_ = nullptr;
this->packed_data_ = nullptr;
return *this;
}
@@ -26,7 +34,8 @@ InfraredCall &InfraredCall::set_raw_timings_packed(const uint8_t *data, uint16_t
this->packed_data_ = data;
this->packed_length_ = length;
this->packed_count_ = count;
this->raw_timings_ = nullptr; // Clear vector if packed is set
this->raw_timings_ = nullptr;
this->base64url_ptr_ = nullptr;
return *this;
}
@@ -92,6 +101,23 @@ void Infrared::control(const InfraredCall &call) {
call.get_packed_count());
ESP_LOGD(TAG, "Transmitting packed raw timings: count=%u, repeat=%u", call.get_packed_count(),
call.get_repeat_count());
} else if (call.is_base64url()) {
// Decode base64url (URL-safe) into transmit buffer
if (!transmit_data->set_data_from_base64url(call.get_base64url_data())) {
ESP_LOGE(TAG, "Invalid base64url data");
return;
}
// Sanity check: validate timing values are within reasonable bounds
constexpr int32_t max_timing_us = 500000; // 500ms absolute max
for (int32_t timing : transmit_data->get_data()) {
int32_t abs_timing = timing < 0 ? -timing : timing;
if (abs_timing > max_timing_us) {
ESP_LOGE(TAG, "Invalid timing value: %d µs (max %d)", timing, max_timing_us);
return;
}
}
ESP_LOGD(TAG, "Transmitting base64url raw timings: count=%zu, repeat=%u", transmit_data->get_data().size(),
call.get_repeat_count());
} else {
// From vector (lambdas/automations)
transmit_data->set_data(call.get_raw_timings());

View File

@@ -28,12 +28,29 @@ class InfraredCall {
/// Set the carrier frequency in Hz
InfraredCall &set_carrier_frequency(uint32_t frequency);
/// Set the raw timings (positive = mark, negative = space)
/// Note: The timings vector must outlive the InfraredCall (zero-copy reference)
// ===== Raw Timings Methods =====
// All set_raw_timings_* methods store pointers/references to external data.
// The referenced data must remain valid until perform() completes.
// Safe pattern: call.set_raw_timings_xxx(data); call.perform(); // synchronous
// Unsafe pattern: call.set_raw_timings_xxx(data); defer([call]() { call.perform(); }); // data may be gone!
/// Set the raw timings from a vector (positive = mark, negative = space)
/// @note Lifetime: Stores a pointer to the vector. The vector must outlive perform().
/// @note Usage: Primarily for lambdas/automations where the vector is in scope.
InfraredCall &set_raw_timings(const std::vector<int32_t> &timings);
/// Set the raw timings from packed protobuf sint32 data (zero-copy from wire)
/// Note: The data must outlive the InfraredCall
/// Set the raw timings from base64url-encoded little-endian int32 data
/// @note Lifetime: Stores a pointer to the string. The string must outlive perform().
/// @note Usage: For web_server - base64url is fully URL-safe (uses '-' and '_').
/// @note Decoding happens at perform() time, directly into the transmit buffer.
InfraredCall &set_raw_timings_base64url(const std::string &base64url);
/// Set the raw timings from packed protobuf sint32 data (zigzag + varint encoded)
/// @note Lifetime: Stores a pointer to the buffer. The buffer must outlive perform().
/// @note Usage: For API component where data comes directly from the protobuf message.
InfraredCall &set_raw_timings_packed(const uint8_t *data, uint16_t length, uint16_t count);
/// Set the number of times to repeat transmission (1 = transmit once, 2 = transmit twice, etc.)
InfraredCall &set_repeat_count(uint32_t count);
@@ -42,12 +59,18 @@ class InfraredCall {
/// Get the carrier frequency
const optional<uint32_t> &get_carrier_frequency() const { return this->carrier_frequency_; }
/// Get the raw timings (only valid if set via set_raw_timings, not packed)
/// Get the raw timings (only valid if set via set_raw_timings)
const std::vector<int32_t> &get_raw_timings() const { return *this->raw_timings_; }
/// Check if raw timings have been set (either vector or packed)
bool has_raw_timings() const { return this->raw_timings_ != nullptr || this->packed_data_ != nullptr; }
/// Check if raw timings have been set (any format)
bool has_raw_timings() const {
return this->raw_timings_ != nullptr || this->packed_data_ != nullptr || this->base64url_ptr_ != nullptr;
}
/// Check if using packed data format
bool is_packed() const { return this->packed_data_ != nullptr; }
/// Check if using base64url data format
bool is_base64url() const { return this->base64url_ptr_ != nullptr; }
/// Get the base64url data string
const std::string &get_base64url_data() const { return *this->base64url_ptr_; }
/// Get packed data (only valid if set via set_raw_timings_packed)
const uint8_t *get_packed_data() const { return this->packed_data_; }
uint16_t get_packed_length() const { return this->packed_length_; }
@@ -59,9 +82,11 @@ class InfraredCall {
uint32_t repeat_count_{1};
Infrared *parent_;
optional<uint32_t> carrier_frequency_;
// Vector-based timings (for lambdas/automations)
// Pointer to vector-based timings (caller-owned, must outlive perform())
const std::vector<int32_t> *raw_timings_{nullptr};
// Packed protobuf timings (for API zero-copy)
// Pointer to base64url-encoded string (caller-owned, must outlive perform())
const std::string *base64url_ptr_{nullptr};
// Pointer to packed protobuf buffer (caller-owned, must outlive perform())
const uint8_t *packed_data_{nullptr};
uint16_t packed_length_{0};
uint16_t packed_count_{0};

View File

@@ -1,4 +1,5 @@
#include "light_json_schema.h"
#include "color_mode.h"
#include "light_output.h"
#include "esphome/core/progmem.h"
@@ -8,29 +9,32 @@ namespace esphome::light {
// See https://www.home-assistant.io/integrations/light.mqtt/#json-schema for documentation on the schema
// Get JSON string for color mode using linear search (avoids large switch jump table)
static const char *get_color_mode_json_str(ColorMode mode) {
// Parallel arrays: mode values and their corresponding strings
// Uses less RAM than a switch jump table on sparse enum values
static constexpr ColorMode MODES[] = {
ColorMode::ON_OFF,
ColorMode::BRIGHTNESS,
ColorMode::WHITE,
ColorMode::COLOR_TEMPERATURE,
ColorMode::COLD_WARM_WHITE,
ColorMode::RGB,
ColorMode::RGB_WHITE,
ColorMode::RGB_COLOR_TEMPERATURE,
ColorMode::RGB_COLD_WARM_WHITE,
};
static constexpr const char *STRINGS[] = {
"onoff", "brightness", "white", "color_temp", "cwww", "rgb", "rgbw", "rgbct", "rgbww",
};
for (size_t i = 0; i < sizeof(MODES) / sizeof(MODES[0]); i++) {
if (MODES[i] == mode)
return STRINGS[i];
// Get JSON string for color mode.
// ColorMode enum values are sparse bitmasks (0, 1, 3, 7, 11, 19, 35, 39, 47, 51) which would
// generate a large jump table. Converting to bit index (0-9) allows a compact switch.
static ProgmemStr get_color_mode_json_str(ColorMode mode) {
switch (ColorModeBitPolicy::to_bit(mode)) {
case 1:
return ESPHOME_F("onoff");
case 2:
return ESPHOME_F("brightness");
case 3:
return ESPHOME_F("white");
case 4:
return ESPHOME_F("color_temp");
case 5:
return ESPHOME_F("cwww");
case 6:
return ESPHOME_F("rgb");
case 7:
return ESPHOME_F("rgbw");
case 8:
return ESPHOME_F("rgbct");
case 9:
return ESPHOME_F("rgbww");
default:
return nullptr;
}
return nullptr;
}
void LightJSONSchema::dump_json(LightState &state, JsonObject root) {
@@ -44,7 +48,7 @@ void LightJSONSchema::dump_json(LightState &state, JsonObject root) {
auto values = state.remote_values;
const auto color_mode = values.get_color_mode();
const char *mode_str = get_color_mode_json_str(color_mode);
const auto *mode_str = get_color_mode_json_str(color_mode);
if (mode_str != nullptr) {
root[ESPHOME_F("color_mode")] = mode_str;
}

View File

@@ -52,7 +52,7 @@ void Lock::publish_state(LockState state) {
this->state = state;
this->rtc_.save(&this->state);
ESP_LOGD(TAG, "'%s': Sending state %s", this->name_.c_str(), LOG_STR_ARG(lock_state_to_string(state)));
ESP_LOGD(TAG, "'%s' >> %s", this->name_.c_str(), LOG_STR_ARG(lock_state_to_string(state)));
this->state_callback_.call();
#if defined(USE_LOCK) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_lock_update(this);

View File

@@ -101,4 +101,225 @@ DriverChip(
(0xFF, 0x77, 0x01, 0x00, 0x00, 0x00),
]
)
# jc8012P4A1 Driver Configuration (jd9365)
# Using parameters from esp_lcd_jd9365.h and the working full init sequence
# ----------------------------------------------------------------------------------------------------------------------
# * Resolution: 800x1280
# * PCLK Frequency: 60 MHz
# * DSI Lane Bit Rate: 1 Gbps (using 2-Lane DSI configuration)
# * Horizontal Timing (hsync_pulse_width=20, hsync_back_porch=20, hsync_front_porch=40)
# * Vertical Timing (vsync_pulse_width=4, vsync_back_porch=8, vsync_front_porch=20)
# ----------------------------------------------------------------------------------------------------------------------
DriverChip(
"JC8012P4A1",
width=800,
height=1280,
hsync_back_porch=20,
hsync_pulse_width=20,
hsync_front_porch=40,
vsync_back_porch=8,
vsync_pulse_width=4,
vsync_front_porch=20,
pclk_frequency="60MHz",
lane_bit_rate="1Gbps",
swap_xy=cv.UNDEFINED,
color_order="RGB",
reset_pin=27,
initsequence=[
(0xE0, 0x00),
(0xE1, 0x93),
(0xE2, 0x65),
(0xE3, 0xF8),
(0x80, 0x01),
(0xE0, 0x01),
(0x00, 0x00),
(0x01, 0x39),
(0x03, 0x10),
(0x04, 0x41),
(0x0C, 0x74),
(0x17, 0x00),
(0x18, 0xD7),
(0x19, 0x00),
(0x1A, 0x00),
(0x1B, 0xD7),
(0x1C, 0x00),
(0x24, 0xFE),
(0x35, 0x26),
(0x37, 0x69),
(0x38, 0x05),
(0x39, 0x06),
(0x3A, 0x08),
(0x3C, 0x78),
(0x3D, 0xFF),
(0x3E, 0xFF),
(0x3F, 0xFF),
(0x40, 0x06),
(0x41, 0xA0),
(0x43, 0x14),
(0x44, 0x0B),
(0x45, 0x30),
(0x4B, 0x04),
(0x55, 0x02),
(0x57, 0x89),
(0x59, 0x0A),
(0x5A, 0x28),
(0x5B, 0x15),
(0x5D, 0x50),
(0x5E, 0x37),
(0x5F, 0x29),
(0x60, 0x1E),
(0x61, 0x1D),
(0x62, 0x12),
(0x63, 0x1A),
(0x64, 0x08),
(0x65, 0x25),
(0x66, 0x26),
(0x67, 0x28),
(0x68, 0x49),
(0x69, 0x3A),
(0x6A, 0x43),
(0x6B, 0x3A),
(0x6C, 0x3B),
(0x6D, 0x32),
(0x6E, 0x1F),
(0x6F, 0x0E),
(0x70, 0x50),
(0x71, 0x37),
(0x72, 0x29),
(0x73, 0x1E),
(0x74, 0x1D),
(0x75, 0x12),
(0x76, 0x1A),
(0x77, 0x08),
(0x78, 0x25),
(0x79, 0x26),
(0x7A, 0x28),
(0x7B, 0x49),
(0x7C, 0x3A),
(0x7D, 0x43),
(0x7E, 0x3A),
(0x7F, 0x3B),
(0x80, 0x32),
(0x81, 0x1F),
(0x82, 0x0E),
(0xE0, 0x02),
(0x00, 0x1F),
(0x01, 0x1F),
(0x02, 0x52),
(0x03, 0x51),
(0x04, 0x50),
(0x05, 0x4B),
(0x06, 0x4A),
(0x07, 0x49),
(0x08, 0x48),
(0x09, 0x47),
(0x0A, 0x46),
(0x0B, 0x45),
(0x0C, 0x44),
(0x0D, 0x40),
(0x0E, 0x41),
(0x0F, 0x1F),
(0x10, 0x1F),
(0x11, 0x1F),
(0x12, 0x1F),
(0x13, 0x1F),
(0x14, 0x1F),
(0x15, 0x1F),
(0x16, 0x1F),
(0x17, 0x1F),
(0x18, 0x52),
(0x19, 0x51),
(0x1A, 0x50),
(0x1B, 0x4B),
(0x1C, 0x4A),
(0x1D, 0x49),
(0x1E, 0x48),
(0x1F, 0x47),
(0x20, 0x46),
(0x21, 0x45),
(0x22, 0x44),
(0x23, 0x40),
(0x24, 0x41),
(0x25, 0x1F),
(0x26, 0x1F),
(0x27, 0x1F),
(0x28, 0x1F),
(0x29, 0x1F),
(0x2A, 0x1F),
(0x2B, 0x1F),
(0x2C, 0x1F),
(0x2D, 0x1F),
(0x2E, 0x52),
(0x2F, 0x40),
(0x30, 0x41),
(0x31, 0x48),
(0x32, 0x49),
(0x33, 0x4A),
(0x34, 0x4B),
(0x35, 0x44),
(0x36, 0x45),
(0x37, 0x46),
(0x38, 0x47),
(0x39, 0x51),
(0x3A, 0x50),
(0x3B, 0x1F),
(0x3C, 0x1F),
(0x3D, 0x1F),
(0x3E, 0x1F),
(0x3F, 0x1F),
(0x40, 0x1F),
(0x41, 0x1F),
(0x42, 0x1F),
(0x43, 0x1F),
(0x44, 0x52),
(0x45, 0x40),
(0x46, 0x41),
(0x47, 0x48),
(0x48, 0x49),
(0x49, 0x4A),
(0x4A, 0x4B),
(0x4B, 0x44),
(0x4C, 0x45),
(0x4D, 0x46),
(0x4E, 0x47),
(0x4F, 0x51),
(0x50, 0x50),
(0x51, 0x1F),
(0x52, 0x1F),
(0x53, 0x1F),
(0x54, 0x1F),
(0x55, 0x1F),
(0x56, 0x1F),
(0x57, 0x1F),
(0x58, 0x40),
(0x59, 0x00),
(0x5A, 0x00),
(0x5B, 0x10),
(0x5C, 0x05),
(0x5D, 0x50),
(0x5E, 0x01),
(0x5F, 0x02),
(0x60, 0x50),
(0x61, 0x06),
(0x62, 0x04),
(0x63, 0x03),
(0x64, 0x64),
(0x65, 0x65),
(0x66, 0x0B),
(0x67, 0x73),
(0x68, 0x07),
(0x69, 0x06),
(0x6A, 0x64),
(0x6B, 0x08),
(0x6C, 0x00),
(0x6D, 0x32),
(0x6E, 0x08),
(0xE0, 0x04),
(0x2C, 0x6B),
(0x35, 0x08),
(0x37, 0x00),
(0xE0, 0x00),
]
)
# fmt: on

View File

@@ -271,24 +271,31 @@ class ServerRegister {
// Formats a raw value into a string representation based on the value type for debugging
std::string format_value(int64_t value) const {
// max 44: float with %.1f can be up to 42 chars (3.4e38 → 39 integer digits + sign + decimal + 1 digit)
// plus null terminator = 43, rounded to 44 for 4-byte alignment
char buf[44];
switch (this->value_type) {
case SensorValueType::U_WORD:
case SensorValueType::U_DWORD:
case SensorValueType::U_DWORD_R:
case SensorValueType::U_QWORD:
case SensorValueType::U_QWORD_R:
return std::to_string(static_cast<uint64_t>(value));
buf_append_printf(buf, sizeof(buf), 0, "%" PRIu64, static_cast<uint64_t>(value));
return buf;
case SensorValueType::S_WORD:
case SensorValueType::S_DWORD:
case SensorValueType::S_DWORD_R:
case SensorValueType::S_QWORD:
case SensorValueType::S_QWORD_R:
return std::to_string(value);
buf_append_printf(buf, sizeof(buf), 0, "%" PRId64, value);
return buf;
case SensorValueType::FP32_R:
case SensorValueType::FP32:
return str_sprintf("%.1f", bit_cast<float>(static_cast<uint32_t>(value)));
buf_append_printf(buf, sizeof(buf), 0, "%.1f", bit_cast<float>(static_cast<uint32_t>(value)));
return buf;
default:
return std::to_string(value);
buf_append_printf(buf, sizeof(buf), 0, "%" PRId64, value);
return buf;
}
}

View File

@@ -16,12 +16,20 @@ void ModbusTextSensor::parse_and_publish(const std::vector<uint8_t> &data) {
while ((items_left > 0) && index < data.size()) {
uint8_t b = data[index];
switch (this->encode_) {
case RawEncoding::HEXBYTES:
output_str += str_snprintf("%02x", 2, b);
case RawEncoding::HEXBYTES: {
// max 3: 2 hex digits + null
char hex_buf[3];
snprintf(hex_buf, sizeof(hex_buf), "%02x", b);
output_str += hex_buf;
break;
case RawEncoding::COMMA:
output_str += str_sprintf(index != this->offset ? ",%d" : "%d", b);
}
case RawEncoding::COMMA: {
// max 5: optional ','(1) + uint8(3) + null, for both ",%d" and "%d"
char dec_buf[5];
snprintf(dec_buf, sizeof(dec_buf), index != this->offset ? ",%d" : "%d", b);
output_str += dec_buf;
break;
}
case RawEncoding::ANSI:
if (b < 0x20)
break;

View File

@@ -189,8 +189,7 @@ bool MQTTComponent::send_discovery_() {
StringRef object_id = this->get_default_object_id_to_(object_id_buf);
if (discovery_info.unique_id_generator == MQTT_MAC_ADDRESS_UNIQUE_ID_GENERATOR) {
char friendly_name_hash[9];
sprintf(friendly_name_hash, "%08" PRIx32, fnv1_hash(this->friendly_name_()));
friendly_name_hash[8] = 0; // ensure the hash-string ends with null
snprintf(friendly_name_hash, sizeof(friendly_name_hash), "%08" PRIx32, fnv1_hash(this->friendly_name_()));
// Format: mac-component_type-hash (e.g. "aabbccddeeff-sensor-12345678")
// MAC (12) + "-" (1) + domain (max 20) + "-" (1) + hash (8) + null (1) = 43
char unique_id[MAC_ADDRESS_BUFFER_SIZE + ESPHOME_DOMAIN_MAX_LEN + 11];

View File

@@ -43,6 +43,14 @@ namespace network {
/// Buffer size for IP address string (IPv6 max: 39 chars + null)
static constexpr size_t IP_ADDRESS_BUFFER_SIZE = 40;
/// Lowercase hex digits in IP address string (A-F -> a-f for IPv6 per RFC 5952)
inline void lowercase_ip_str(char *buf) {
for (char *p = buf; *p; ++p) {
if (*p >= 'A' && *p <= 'F')
*p += 32;
}
}
struct IPAddress {
public:
#ifdef USE_HOST
@@ -52,10 +60,15 @@ struct IPAddress {
}
IPAddress(const std::string &in_address) { inet_aton(in_address.c_str(), &ip_addr_); }
IPAddress(const ip_addr_t *other_ip) { ip_addr_ = *other_ip; }
std::string str() const { return str_lower_case(inet_ntoa(ip_addr_)); }
std::string str() const {
char buf[IP_ADDRESS_BUFFER_SIZE];
this->str_to(buf);
return buf;
}
/// Write IP address to buffer. Buffer must be at least IP_ADDRESS_BUFFER_SIZE bytes.
char *str_to(char *buf) const {
return const_cast<char *>(inet_ntop(AF_INET, &ip_addr_, buf, IP_ADDRESS_BUFFER_SIZE));
inet_ntop(AF_INET, &ip_addr_, buf, IP_ADDRESS_BUFFER_SIZE);
return buf; // IPv4 only, no hex letters to lowercase
}
#else
IPAddress() { ip_addr_set_zero(&ip_addr_); }
@@ -134,9 +147,18 @@ struct IPAddress {
bool is_ip4() const { return IP_IS_V4(&ip_addr_); }
bool is_ip6() const { return IP_IS_V6(&ip_addr_); }
bool is_multicast() const { return ip_addr_ismulticast(&ip_addr_); }
std::string str() const { return str_lower_case(ipaddr_ntoa(&ip_addr_)); }
std::string str() const {
char buf[IP_ADDRESS_BUFFER_SIZE];
this->str_to(buf);
return buf;
}
/// Write IP address to buffer. Buffer must be at least IP_ADDRESS_BUFFER_SIZE bytes.
char *str_to(char *buf) const { return ipaddr_ntoa_r(&ip_addr_, buf, IP_ADDRESS_BUFFER_SIZE); }
/// Output is lowercased per RFC 5952 (IPv6 hex digits a-f).
char *str_to(char *buf) const {
ipaddr_ntoa_r(&ip_addr_, buf, IP_ADDRESS_BUFFER_SIZE);
lowercase_ip_str(buf);
return buf;
}
bool operator==(const IPAddress &other) const { return ip_addr_cmp(&ip_addr_, &other.ip_addr_); }
bool operator!=(const IPAddress &other) const { return !ip_addr_cmp(&ip_addr_, &other.ip_addr_); }
IPAddress &operator+=(uint8_t increase) {

View File

@@ -11,7 +11,12 @@ from esphome.const import (
)
from esphome.core import CORE, TimePeriod
from . import Nextion, nextion_ns, nextion_ref
from . import ( # noqa: F401 pylint: disable=unused-import
FILTER_SOURCE_FILES,
Nextion,
nextion_ns,
nextion_ref,
)
from .base_component import (
CONF_AUTO_WAKE_ON_TOUCH,
CONF_COMMAND_SPACING,

View File

@@ -23,7 +23,7 @@ void NTC::process_(float value) {
double v = this->a_ + this->b_ * lr + this->c_ * lr * lr * lr;
auto temp = float(1.0 / v - 273.15);
ESP_LOGD(TAG, "'%s' - Temperature: %.1f°C", this->name_.c_str(), temp);
ESP_LOGV(TAG, "'%s' - Temperature: %.1f°C", this->name_.c_str(), temp);
this->publish_state(temp);
}

View File

@@ -31,7 +31,7 @@ void log_number(const char *tag, const char *prefix, const char *type, Number *o
void Number::publish_state(float state) {
this->set_has_state(true);
this->state = state;
ESP_LOGD(TAG, "'%s': Sending state %f", this->get_name().c_str(), state);
ESP_LOGD(TAG, "'%s' >> %.2f", this->get_name().c_str(), state);
this->state_callback_.call(state);
#if defined(USE_NUMBER) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_number_update(this);

View File

@@ -561,8 +561,9 @@ const char *OpenTherm::message_id_to_str(MessageId id) {
}
void OpenTherm::debug_data(OpenthermData &data) {
ESP_LOGD(TAG, "%s %s %s %s", format_bin(data.type).c_str(), format_bin(data.id).c_str(),
format_bin(data.valueHB).c_str(), format_bin(data.valueLB).c_str());
char type_buf[9], id_buf[9], hb_buf[9], lb_buf[9];
ESP_LOGD(TAG, "%s %s %s %s", format_bin_to(type_buf, data.type), format_bin_to(id_buf, data.id),
format_bin_to(hb_buf, data.valueHB), format_bin_to(lb_buf, data.valueLB));
ESP_LOGD(TAG, "type: %s; id: %u; HB: %u; LB: %u; uint_16: %u; float: %f",
this->message_type_to_str((MessageType) data.type), data.id, data.valueHB, data.valueLB, data.u16(),
data.f88());

View File

@@ -9,7 +9,7 @@ static const char *const TAG = "pipsolar.output";
void PipsolarOutput::write_state(float state) {
char tmp[10];
sprintf(tmp, this->set_command_.c_str(), state);
snprintf(tmp, sizeof(tmp), this->set_command_, state);
if (std::find(this->possible_values_.begin(), this->possible_values_.end(), state) != this->possible_values_.end()) {
ESP_LOGD(TAG, "Will write: %s out of value %f / %02.0f", tmp, state, state);

View File

@@ -15,13 +15,15 @@ class PipsolarOutput : public output::FloatOutput {
public:
PipsolarOutput() {}
void set_parent(Pipsolar *parent) { this->parent_ = parent; }
void set_set_command(const std::string &command) { this->set_command_ = command; };
void set_set_command(const char *command) { this->set_command_ = command; }
/// Prevent accidental use of std::string which would dangle
void set_set_command(const std::string &command) = delete;
void set_possible_values(std::vector<float> possible_values) { this->possible_values_ = std::move(possible_values); }
void set_value(float value) { this->write_state(value); };
void set_value(float value) { this->write_state(value); }
protected:
void write_state(float state) override;
std::string set_command_;
const char *set_command_{nullptr};
Pipsolar *parent_;
std::vector<float> possible_values_;
};

View File

@@ -9,14 +9,9 @@ static const char *const TAG = "pipsolar.switch";
void PipsolarSwitch::dump_config() { LOG_SWITCH("", "Pipsolar Switch", this); }
void PipsolarSwitch::write_state(bool state) {
if (state) {
if (!this->on_command_.empty()) {
this->parent_->queue_command(this->on_command_);
}
} else {
if (!this->off_command_.empty()) {
this->parent_->queue_command(this->off_command_);
}
const char *command = state ? this->on_command_ : this->off_command_;
if (command != nullptr) {
this->parent_->queue_command(command);
}
}

View File

@@ -9,15 +9,18 @@ namespace pipsolar {
class Pipsolar;
class PipsolarSwitch : public switch_::Switch, public Component {
public:
void set_parent(Pipsolar *parent) { this->parent_ = parent; };
void set_on_command(const std::string &command) { this->on_command_ = command; };
void set_off_command(const std::string &command) { this->off_command_ = command; };
void set_parent(Pipsolar *parent) { this->parent_ = parent; }
void set_on_command(const char *command) { this->on_command_ = command; }
void set_off_command(const char *command) { this->off_command_ = command; }
/// Prevent accidental use of std::string which would dangle
void set_on_command(const std::string &command) = delete;
void set_off_command(const std::string &command) = delete;
void dump_config() override;
protected:
void write_state(bool state) override;
std::string on_command_;
std::string off_command_;
const char *on_command_{nullptr};
const char *off_command_{nullptr};
Pipsolar *parent_;
};

View File

@@ -27,7 +27,16 @@ void QrCode::set_ecc(qrcodegen_Ecc ecc) {
void QrCode::generate_qr_code() {
ESP_LOGV(TAG, "Generating QR code");
#ifdef USE_ESP32
// ESP32 has 8KB stack, safe to allocate ~4KB buffer on stack
uint8_t tempbuffer[qrcodegen_BUFFER_LEN_MAX];
#else
// Other platforms (ESP8266: 4KB, RP2040: 2KB, LibreTiny: ~4KB) have smaller stacks
// Allocate buffer on heap to avoid stack overflow
auto tempbuffer_owner = std::make_unique<uint8_t[]>(qrcodegen_BUFFER_LEN_MAX);
uint8_t *tempbuffer = tempbuffer_owner.get();
#endif
if (!qrcodegen_encodeText(this->value_.c_str(), tempbuffer, this->qr_, this->ecc_, qrcodegen_VERSION_MIN,
qrcodegen_VERSION_MAX, qrcodegen_Mask_AUTO, true)) {

View File

@@ -85,8 +85,8 @@ optional<AEHAData> AEHAProtocol::decode(RemoteReceiveData src) {
std::string AEHAProtocol::format_data_(const std::vector<uint8_t> &data) {
std::string out;
for (uint8_t byte : data) {
char buf[6];
sprintf(buf, "0x%02X,", byte);
char buf[8]; // "0x%02X," = 5 chars + null + margin
snprintf(buf, sizeof(buf), "0x%02X,", byte);
out += buf;
}
out.pop_back();

View File

@@ -1,4 +1,5 @@
#include "raw_protocol.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
@@ -8,36 +9,30 @@ static const char *const TAG = "remote.raw";
bool RawDumper::dump(RemoteReceiveData src) {
char buffer[256];
uint32_t buffer_offset = 0;
buffer_offset += sprintf(buffer, "Received Raw: ");
size_t pos = buf_append_printf(buffer, sizeof(buffer), 0, "Received Raw: ");
for (int32_t i = 0; i < src.size() - 1; i++) {
const int32_t value = src[i];
const uint32_t remaining_length = sizeof(buffer) - buffer_offset;
int written;
size_t prev_pos = pos;
if (i + 1 < src.size() - 1) {
written = snprintf(buffer + buffer_offset, remaining_length, "%" PRId32 ", ", value);
pos = buf_append_printf(buffer, sizeof(buffer), pos, "%" PRId32 ", ", value);
} else {
written = snprintf(buffer + buffer_offset, remaining_length, "%" PRId32, value);
pos = buf_append_printf(buffer, sizeof(buffer), pos, "%" PRId32, value);
}
if (written < 0 || written >= int(remaining_length)) {
// write failed, flush...
buffer[buffer_offset] = '\0';
if (pos >= sizeof(buffer) - 1) {
// buffer full, flush and continue
buffer[prev_pos] = '\0';
ESP_LOGI(TAG, "%s", buffer);
buffer_offset = 0;
written = sprintf(buffer, " ");
if (i + 1 < src.size() - 1) {
written += sprintf(buffer + written, "%" PRId32 ", ", value);
pos = buf_append_printf(buffer, sizeof(buffer), 0, " %" PRId32 ", ", value);
} else {
written += sprintf(buffer + written, "%" PRId32, value);
pos = buf_append_printf(buffer, sizeof(buffer), 0, " %" PRId32, value);
}
}
buffer_offset += written;
}
if (buffer_offset != 0) {
if (pos != 0) {
ESP_LOGI(TAG, "%s", buffer);
}
return true;

View File

@@ -1,8 +1,7 @@
#include "remote_base.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <cinttypes>
namespace esphome {
namespace remote_base {
@@ -159,42 +158,41 @@ void RemoteTransmitData::set_data_from_packed_sint32(const uint8_t *data, size_t
}
}
bool RemoteTransmitData::set_data_from_base64url(const std::string &base64url) {
return base64_decode_int32_vector(base64url, this->data_);
}
/* RemoteTransmitterBase */
void RemoteTransmitterBase::send_(uint32_t send_times, uint32_t send_wait) {
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
const auto &vec = this->temp_.get_data();
char buffer[256];
uint32_t buffer_offset = 0;
buffer_offset += sprintf(buffer, "Sending times=%" PRIu32 " wait=%" PRIu32 "ms: ", send_times, send_wait);
size_t pos = buf_append_printf(buffer, sizeof(buffer), 0,
"Sending times=%" PRIu32 " wait=%" PRIu32 "ms: ", send_times, send_wait);
for (size_t i = 0; i < vec.size(); i++) {
const int32_t value = vec[i];
const uint32_t remaining_length = sizeof(buffer) - buffer_offset;
int written;
size_t prev_pos = pos;
if (i + 1 < vec.size()) {
written = snprintf(buffer + buffer_offset, remaining_length, "%" PRId32 ", ", value);
pos = buf_append_printf(buffer, sizeof(buffer), pos, "%" PRId32 ", ", value);
} else {
written = snprintf(buffer + buffer_offset, remaining_length, "%" PRId32, value);
pos = buf_append_printf(buffer, sizeof(buffer), pos, "%" PRId32, value);
}
if (written < 0 || written >= int(remaining_length)) {
// write failed, flush...
buffer[buffer_offset] = '\0';
if (pos >= sizeof(buffer) - 1) {
// buffer full, flush and continue
buffer[prev_pos] = '\0';
ESP_LOGVV(TAG, "%s", buffer);
buffer_offset = 0;
written = sprintf(buffer, " ");
if (i + 1 < vec.size()) {
written += sprintf(buffer + written, "%" PRId32 ", ", value);
pos = buf_append_printf(buffer, sizeof(buffer), 0, " %" PRId32 ", ", value);
} else {
written += sprintf(buffer + written, "%" PRId32, value);
pos = buf_append_printf(buffer, sizeof(buffer), 0, " %" PRId32, value);
}
}
buffer_offset += written;
}
if (buffer_offset != 0) {
if (pos != 0) {
ESP_LOGVV(TAG, "%s", buffer);
}
#endif

View File

@@ -36,6 +36,11 @@ class RemoteTransmitData {
/// @param len Length of the buffer in bytes
/// @param count Number of values (for reserve optimization)
void set_data_from_packed_sint32(const uint8_t *data, size_t len, size_t count);
/// Set data from base64url-encoded little-endian int32 values
/// Base64url is URL-safe: uses '-' instead of '+', '_' instead of '/'
/// @param base64url Base64url-encoded string of little-endian int32 values
/// @return true if successful, false if decode failed or invalid size
bool set_data_from_base64url(const std::string &base64url);
void reset() {
this->data_.clear();
this->carrier_frequency_ = 0;

View File

@@ -1,5 +1,7 @@
from esphome.components import binary_sensor, remote_base
from . import FILTER_SOURCE_FILES # noqa: F401 pylint: disable=unused-import
DEPENDENCIES = ["remote_receiver"]
CONFIG_SCHEMA = remote_base.validate_binary_sensor

View File

@@ -39,7 +39,7 @@ void ResistanceSensor::process_(float value) {
}
res *= this->resistor_;
ESP_LOGD(TAG, "'%s' - Resistance %.1fΩ", this->name_.c_str(), res);
ESP_LOGV(TAG, "'%s' - Resistance %.1fΩ", this->name_.c_str(), res);
this->publish_state(res);
}

View File

@@ -9,7 +9,7 @@
#include <cinttypes>
#include <cstdio>
#ifdef USE_OTA_ROLLBACK
#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK)
#include <esp_ota_ops.h>
#endif
@@ -26,6 +26,17 @@ void SafeModeComponent::dump_config() {
this->safe_mode_boot_is_good_after_ / 1000, // because milliseconds
this->safe_mode_num_attempts_,
this->safe_mode_enable_time_ / 1000); // because milliseconds
#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK)
const char *state_str;
if (this->ota_state_ == ESP_OTA_IMG_NEW) {
state_str = "not supported";
} else if (this->ota_state_ == ESP_OTA_IMG_PENDING_VERIFY) {
state_str = "supported";
} else {
state_str = "support unknown";
}
ESP_LOGCONFIG(TAG, " Bootloader rollback: %s", state_str);
#endif
if (this->safe_mode_rtc_value_ > 1 && this->safe_mode_rtc_value_ != SafeModeComponent::ENTER_SAFE_MODE_MAGIC) {
auto remaining_restarts = this->safe_mode_num_attempts_ - this->safe_mode_rtc_value_;
@@ -36,7 +47,7 @@ void SafeModeComponent::dump_config() {
}
}
#ifdef USE_OTA_ROLLBACK
#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK)
const esp_partition_t *last_invalid = esp_ota_get_last_invalid_partition();
if (last_invalid != nullptr) {
ESP_LOGW(TAG,
@@ -55,7 +66,7 @@ void SafeModeComponent::loop() {
ESP_LOGI(TAG, "Boot seems successful; resetting boot loop counter");
this->clean_rtc();
this->boot_successful_ = true;
#ifdef USE_OTA_ROLLBACK
#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK)
// Mark OTA partition as valid to prevent rollback
esp_ota_mark_app_valid_cancel_rollback();
#endif
@@ -90,6 +101,12 @@ bool SafeModeComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t en
this->safe_mode_num_attempts_ = num_attempts;
this->rtc_ = global_preferences->make_preference<uint32_t>(233825507UL, false);
#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK)
// Check partition state to detect if bootloader supports rollback
const esp_partition_t *running = esp_ota_get_running_partition();
esp_ota_get_state_partition(running, &this->ota_state_);
#endif
uint32_t rtc_val = this->read_rtc_();
this->safe_mode_rtc_value_ = rtc_val;

View File

@@ -5,6 +5,10 @@
#include "esphome/core/helpers.h"
#include "esphome/core/preferences.h"
#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK)
#include <esp_ota_ops.h>
#endif
namespace esphome::safe_mode {
/// SafeModeComponent provides a safe way to recover from repeated boot failures
@@ -42,6 +46,9 @@ class SafeModeComponent : public Component {
// Group 1-byte members together to minimize padding
bool boot_successful_{false}; ///< set to true after boot is considered successful
uint8_t safe_mode_num_attempts_{0};
#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK)
esp_ota_img_states_t ota_state_{ESP_OTA_IMG_UNDEFINED};
#endif
// Larger objects at the end
ESPPreferenceObject rtc_;
#ifdef USE_SAFE_MODE_CALLBACK

View File

@@ -8,17 +8,20 @@ from esphome.const import (
CONF_ICON,
CONF_ID,
CONF_INDEX,
CONF_LAMBDA,
CONF_MODE,
CONF_MQTT_ID,
CONF_ON_VALUE,
CONF_OPERATION,
CONF_OPTION,
CONF_OPTIONS,
CONF_TRIGGER_ID,
CONF_WEB_SERVER,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.cpp_generator import MockObjClass
from esphome.cpp_generator import MockObjClass, TemplateArguments
from esphome.cpp_types import global_ns
CODEOWNERS = ["@esphome/core"]
IS_PLATFORM_COMPONENT = True
@@ -38,6 +41,9 @@ SelectSetAction = select_ns.class_("SelectSetAction", automation.Action)
SelectSetIndexAction = select_ns.class_("SelectSetIndexAction", automation.Action)
SelectOperationAction = select_ns.class_("SelectOperationAction", automation.Action)
# Conditions
SelectIsCondition = select_ns.class_("SelectIsCondition", automation.Condition)
# Enums
SelectOperation = select_ns.enum("SelectOperation")
SELECT_OPERATION_OPTIONS = {
@@ -165,6 +171,41 @@ async def select_set_index_to_code(config, action_id, template_arg, args):
return var
@automation.register_condition(
"select.is",
SelectIsCondition,
OPERATION_BASE_SCHEMA.extend(
{
cv.Optional(CONF_OPTIONS): cv.All(
cv.ensure_list(cv.string_strict), cv.Length(min=1)
),
cv.Optional(CONF_LAMBDA): cv.returning_lambda,
}
).add_extra(cv.has_exactly_one_key(CONF_OPTIONS, CONF_LAMBDA)),
)
async def select_is_to_code(config, condition_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
if options := config.get(CONF_OPTIONS):
# List of constant options
# Create a constexpr and pass that with a template length
arr_id = ID(
f"{condition_id}_data",
is_declaration=True,
type=global_ns.namespace("constexpr char * const"),
)
arg = cg.static_const_array(arr_id, cg.ArrayInitializer(*options))
template_arg = TemplateArguments(len(options), *template_arg)
else:
# Lambda
arg = await cg.process_lambda(
config[CONF_LAMBDA],
[(global_ns.namespace("StringRef &").operator("const"), "current")] + args,
return_type=cg.bool_,
)
template_arg = TemplateArguments(0, *template_arg)
return cg.new_Pvariable(condition_id, template_arg, paren, arg)
@automation.register_action(
"select.operation",
SelectOperationAction,

View File

@@ -66,4 +66,34 @@ template<typename... Ts> class SelectOperationAction : public Action<Ts...> {
Select *select_;
};
template<size_t N, typename... Ts> class SelectIsCondition : public Condition<Ts...> {
public:
SelectIsCondition(Select *parent, const char *const *option_list) : parent_(parent), option_list_(option_list) {}
bool check(const Ts &...x) override {
auto current = this->parent_->current_option();
for (size_t i = 0; i != N; i++) {
if (current == this->option_list_[i]) {
return true;
}
}
return false;
}
protected:
Select *parent_;
const char *const *option_list_;
};
template<typename... Ts> class SelectIsCondition<0, Ts...> : public Condition<Ts...> {
public:
SelectIsCondition(Select *parent, std::function<bool(const StringRef &, const Ts &...)> &&f)
: parent_(parent), f_(f) {}
bool check(const Ts &...x) override { return this->f_(this->parent_->current_option(), x...); }
protected:
Select *parent_;
std::function<bool(const StringRef &, const Ts &...)> f_;
};
} // namespace esphome::select

View File

@@ -31,7 +31,7 @@ void Select::publish_state(size_t index) {
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
this->state = option; // Update deprecated member for backward compatibility
#pragma GCC diagnostic pop
ESP_LOGD(TAG, "'%s': Sending state %s (index %zu)", this->get_name().c_str(), option, index);
ESP_LOGD(TAG, "'%s' >> %s (%zu)", this->get_name().c_str(), option, index);
this->state_callback_.call(index);
#if defined(USE_SELECT) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_select_update(this);

View File

@@ -126,8 +126,8 @@ float Sensor::get_raw_state() const { return this->raw_state; }
void Sensor::internal_send_state_to_frontend(float state) {
this->set_has_state(true);
this->state = state;
ESP_LOGD(TAG, "'%s': Sending state %.5f %s with %d decimals of accuracy", this->get_name().c_str(), state,
this->get_unit_of_measurement_ref().c_str(), this->get_accuracy_decimals());
ESP_LOGD(TAG, "'%s' >> %.*f %s", this->get_name().c_str(), std::max(0, (int) this->get_accuracy_decimals()), state,
this->get_unit_of_measurement_ref().c_str());
this->callback_.call(state);
#if defined(USE_SENSOR) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_sensor_update(this);

View File

@@ -10,26 +10,24 @@ namespace esphome::sha256 {
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
// CRITICAL ESP32-S3 HARDWARE SHA ACCELERATION REQUIREMENTS (IDF 5.5.x):
// CRITICAL ESP32 HARDWARE SHA ACCELERATION REQUIREMENTS (IDF 5.5.x):
//
// The ESP32-S3 uses hardware DMA for SHA acceleration. The mbedtls_sha256_context structure contains
// internal state that the DMA engine references. This imposes three critical constraints:
// ESP32 variants (except original ESP32) use DMA-based hardware SHA acceleration that requires
// 32-byte aligned digest buffers. This is handled automatically via HashBase::digest_ which has
// alignas(32) on these platforms. Two additional constraints apply:
//
// 1. ALIGNMENT: The SHA256 object MUST be declared with `alignas(32)` for proper DMA alignment.
// Without this, the DMA engine may crash with an abort in sha_hal_read_digest().
//
// 2. NO VARIABLE LENGTH ARRAYS (VLAs): VLAs corrupt the stack layout, causing the DMA engine to
// 1. NO VARIABLE LENGTH ARRAYS (VLAs): VLAs corrupt the stack layout, causing the DMA engine to
// write to incorrect memory locations. This results in null pointer dereferences and crashes.
// ALWAYS use fixed-size arrays (e.g., char buf[65], not char buf[size+1]).
//
// 3. SAME STACK FRAME ONLY: The SHA256 object must be created and used entirely within the same
// 2. SAME STACK FRAME ONLY: The SHA256 object must be created and used entirely within the same
// function. NEVER pass the SHA256 object or HashBase pointer to another function. When the stack
// frame changes (function call/return), the DMA references become invalid and will produce
// truncated hash output (20 bytes instead of 32) or corrupt memory.
//
// CORRECT USAGE:
// void my_function() {
// alignas(32) sha256::SHA256 hasher; // Created locally with proper alignment
// sha256::SHA256 hasher;
// hasher.init();
// hasher.add(data, len); // Any size, no chunking needed
// hasher.calculate();
@@ -37,9 +35,9 @@ namespace esphome::sha256 {
// // hasher destroyed when function returns
// }
//
// INCORRECT USAGE (WILL FAIL ON ESP32-S3):
// INCORRECT USAGE (WILL FAIL):
// void my_function() {
// sha256::SHA256 hasher; // WRONG: Missing alignas(32)
// sha256::SHA256 hasher;
// helper(&hasher); // WRONG: Passed to different stack frame
// }
// void helper(HashBase *h) {

View File

@@ -24,13 +24,14 @@ namespace esphome::sha256 {
/// SHA256 hash implementation.
///
/// CRITICAL for ESP32-S3 with IDF 5.5.x hardware SHA acceleration:
/// 1. SHA256 objects MUST be declared with `alignas(32)` for proper DMA alignment
/// 2. The object MUST stay in the same stack frame (no passing to other functions)
/// 3. NO Variable Length Arrays (VLAs) in the same function
/// CRITICAL for ESP32 variants (except original) with IDF 5.5.x hardware SHA acceleration:
/// 1. The object MUST stay in the same stack frame (no passing to other functions)
/// 2. NO Variable Length Arrays (VLAs) in the same function
///
/// Note: Alignment is handled automatically via the HashBase::digest_ member.
///
/// Example usage:
/// alignas(32) sha256::SHA256 hasher;
/// sha256::SHA256 hasher;
/// hasher.init();
/// hasher.add(data, len);
/// hasher.calculate();

View File

@@ -332,6 +332,7 @@ Sprinkler::Sprinkler(const std::string &name) {
// The `name` is needed to set timers up, hence non-default constructor
// replaces `set_name()` method previously existed
this->name_ = name;
this->timer_.init(2);
this->timer_.push_back({this->name_ + "sm", false, 0, 0, std::bind(&Sprinkler::sm_timer_callback_, this)});
this->timer_.push_back({this->name_ + "vs", false, 0, 0, std::bind(&Sprinkler::valve_selection_callback_, this)});
}
@@ -1574,7 +1575,8 @@ const LogString *Sprinkler::state_as_str_(SprinklerState state) {
void Sprinkler::start_timer_(const SprinklerTimerIndex timer_index) {
if (this->timer_duration_(timer_index) > 0) {
this->set_timeout(this->timer_[timer_index].name, this->timer_duration_(timer_index),
// FixedVector ensures timer_ can't be resized, so .c_str() pointers remain valid
this->set_timeout(this->timer_[timer_index].name.c_str(), this->timer_duration_(timer_index),
this->timer_cbf_(timer_index));
this->timer_[timer_index].start_time = millis();
this->timer_[timer_index].active = true;
@@ -1585,7 +1587,7 @@ void Sprinkler::start_timer_(const SprinklerTimerIndex timer_index) {
bool Sprinkler::cancel_timer_(const SprinklerTimerIndex timer_index) {
this->timer_[timer_index].active = false;
return this->cancel_timeout(this->timer_[timer_index].name);
return this->cancel_timeout(this->timer_[timer_index].name.c_str());
}
bool Sprinkler::timer_active_(const SprinklerTimerIndex timer_index) { return this->timer_[timer_index].active; }

View File

@@ -3,6 +3,7 @@
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/components/number/number.h"
#include "esphome/components/switch/switch.h"
@@ -553,8 +554,8 @@ class Sprinkler : public Component {
/// Sprinkler valve operator objects
std::vector<SprinklerValveOperator> valve_op_{2};
/// Valve control timers
std::vector<SprinklerTimer> timer_{};
/// Valve control timers - FixedVector enforces that this can never grow beyond init() size
FixedVector<SprinklerTimer> timer_;
/// Other Sprinkler instances we should be aware of (used to check if pumps are in use)
std::vector<Sprinkler *> other_controllers_;

View File

@@ -14,7 +14,9 @@ class SunTextSensor : public text_sensor::TextSensor, public PollingComponent {
void set_parent(Sun *parent) { parent_ = parent; }
void set_elevation(double elevation) { elevation_ = elevation; }
void set_sunrise(bool sunrise) { sunrise_ = sunrise; }
void set_format(const std::string &format) { format_ = format; }
void set_format(const char *format) { this->format_ = format; }
/// Prevent accidental use of std::string which would dangle
void set_format(const std::string &format) = delete;
void update() override {
optional<ESPTime> res;
@@ -29,14 +31,14 @@ class SunTextSensor : public text_sensor::TextSensor, public PollingComponent {
}
char buf[ESPTime::STRFTIME_BUFFER_SIZE];
size_t len = res->strftime_to(buf, this->format_.c_str());
size_t len = res->strftime_to(buf, this->format_);
this->publish_state(buf, len);
}
void dump_config() override;
protected:
std::string format_{};
const char *format_{nullptr};
Sun *parent_;
double elevation_;
bool sunrise_;

View File

@@ -62,7 +62,7 @@ void Switch::publish_state(bool state) {
if (restore_mode & RESTORE_MODE_PERSISTENT_MASK)
this->rtc_.save(&this->state);
ESP_LOGD(TAG, "'%s': Sending state %s", this->name_.c_str(), ONOFF(this->state));
ESP_LOGD(TAG, "'%s' >> %s", this->name_.c_str(), ONOFF(this->state));
this->state_callback_.call(this->state);
#if defined(USE_SWITCH) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_switch_update(this);

View File

@@ -118,8 +118,7 @@ async def to_code(config):
var = await alarm_control_panel.new_alarm_control_panel(config)
await cg.register_component(var, config)
if CONF_CODES in config:
for acode in config[CONF_CODES]:
cg.add(var.add_code(acode))
cg.add(var.set_codes(config[CONF_CODES]))
if CONF_REQUIRES_CODE_TO_ARM in config:
cg.add(var.set_requires_code_to_arm(config[CONF_REQUIRES_CODE_TO_ARM]))

View File

@@ -206,7 +206,13 @@ bool TemplateAlarmControlPanel::is_code_valid_(optional<std::string> code) {
if (!this->codes_.empty()) {
if (code.has_value()) {
ESP_LOGVV(TAG, "Checking code: %s", code.value().c_str());
return (std::count(this->codes_.begin(), this->codes_.end(), code.value()) == 1);
// Use strcmp for const char* comparison
const char *code_cstr = code.value().c_str();
for (const char *stored_code : this->codes_) {
if (strcmp(stored_code, code_cstr) == 0)
return true;
}
return false;
}
ESP_LOGD(TAG, "No code provided");
return false;

View File

@@ -1,6 +1,7 @@
#pragma once
#include <cinttypes>
#include <cstring>
#include <vector>
#include "esphome/core/automation.h"
@@ -86,11 +87,14 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl
AlarmSensorType type = ALARM_SENSOR_TYPE_DELAYED);
#endif
/** add a code
/** Set the codes (from initializer list).
*
* @param code The code
* @param codes The list of valid codes
*/
void add_code(const std::string &code) { this->codes_.push_back(code); }
void set_codes(std::initializer_list<const char *> codes) { this->codes_ = codes; }
// Deleted overload to catch incorrect std::string usage at compile time
void set_codes(std::initializer_list<std::string> codes) = delete;
/** set requires a code to arm
*
@@ -155,8 +159,8 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl
uint32_t pending_time_;
// the time in trigger
uint32_t trigger_time_;
// a list of codes
std::vector<std::string> codes_;
// a list of codes (const char* pointers to string literals in flash)
FixedVector<const char *> codes_;
// requires a code to arm
bool requires_code_to_arm_ = false;
bool supports_arm_home_ = false;

View File

@@ -8,16 +8,23 @@ static const char *const TAG = "template.text";
void TemplateText::setup() {
if (this->f_.has_value())
return;
std::string value = this->initial_value_;
if (!this->pref_) {
ESP_LOGD(TAG, "State from initial: %s", value.c_str());
} else {
uint32_t key = this->get_preference_hash();
key += this->traits.get_min_length() << 2;
key += this->traits.get_max_length() << 4;
key += fnv1_hash(this->traits.get_pattern_c_str()) << 6;
this->pref_->setup(key, value);
if (this->pref_ == nullptr) {
// No restore - use const char* directly, no heap allocation needed
if (this->initial_value_ != nullptr && this->initial_value_[0] != '\0') {
ESP_LOGD(TAG, "State from initial: %s", this->initial_value_);
this->publish_state(this->initial_value_);
}
return;
}
// Need std::string for pref_->setup() to fill from flash
std::string value{this->initial_value_ != nullptr ? this->initial_value_ : ""};
uint32_t key = this->get_preference_hash();
key += this->traits.get_min_length() << 2;
key += this->traits.get_max_length() << 4;
key += fnv1_hash(this->traits.get_pattern_c_str()) << 6;
this->pref_->setup(key, value);
if (!value.empty())
this->publish_state(value);
}

View File

@@ -70,13 +70,15 @@ class TemplateText final : public text::Text, public PollingComponent {
Trigger<std::string> *get_set_trigger() const { return this->set_trigger_; }
void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
void set_initial_value(const std::string &initial_value) { this->initial_value_ = initial_value; }
void set_initial_value(const char *initial_value) { this->initial_value_ = initial_value; }
/// Prevent accidental use of std::string which would dangle
void set_initial_value(const std::string &initial_value) = delete;
void set_value_saver(TemplateTextSaverBase *restore_value_saver) { this->pref_ = restore_value_saver; }
protected:
void control(const std::string &value) override;
bool optimistic_ = false;
std::string initial_value_;
const char *initial_value_{nullptr};
Trigger<std::string> *set_trigger_ = new Trigger<std::string>();
TemplateLambda<std::string> f_{};

View File

@@ -20,9 +20,9 @@ void Text::publish_state(const char *state, size_t len) {
this->state.assign(state, len);
}
if (this->traits.get_mode() == TEXT_MODE_PASSWORD) {
ESP_LOGD(TAG, "'%s': Sending state " LOG_SECRET("'%s'"), this->get_name().c_str(), this->state.c_str());
ESP_LOGD(TAG, "'%s' >> " LOG_SECRET("'%s'"), this->get_name().c_str(), this->state.c_str());
} else {
ESP_LOGD(TAG, "'%s': Sending state %s", this->get_name().c_str(), this->state.c_str());
ESP_LOGD(TAG, "'%s' >> '%s'", this->get_name().c_str(), this->state.c_str());
}
this->state_callback_.call(this->state);
#if defined(USE_TEXT) && defined(USE_CONTROLLER_REGISTRY)

View File

@@ -116,7 +116,7 @@ void TextSensor::internal_send_state_to_frontend(const char *state, size_t len)
void TextSensor::notify_frontend_() {
this->set_has_state(true);
ESP_LOGD(TAG, "'%s': Sending state '%s'", this->name_.c_str(), this->state.c_str());
ESP_LOGD(TAG, "'%s' >> '%s'", this->name_.c_str(), this->state.c_str());
this->callback_.call(this->state);
#if defined(USE_TEXT_SENSOR) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_text_sensor_update(this);

View File

@@ -31,6 +31,18 @@ void RealTimeClock::dump_config() {
void RealTimeClock::synchronize_epoch_(uint32_t epoch) {
ESP_LOGVV(TAG, "Got epoch %" PRIu32, epoch);
// Skip if time is already synchronized to avoid unnecessary writes, log spam,
// and prevent clock jumping backwards due to network latency
constexpr time_t min_valid_epoch = 1546300800; // January 1, 2019
time_t current_time = this->timestamp_now();
// Check if time is valid (year >= 2019) before comparing
if (current_time >= min_valid_epoch) {
// Unsigned subtraction handles wraparound correctly, then cast to signed
int32_t diff = static_cast<int32_t>(epoch - static_cast<uint32_t>(current_time));
if (diff >= -1 && diff <= 1) {
return;
}
}
// Update UTC epoch time.
#ifdef USE_ZEPHYR
struct timespec ts;

View File

@@ -108,8 +108,7 @@ async def to_code(config):
cg.add(var.set_broadcast_port(conf_port[CONF_BROADCAST_PORT]))
if (listen_address := str(config[CONF_LISTEN_ADDRESS])) != "255.255.255.255":
cg.add(var.set_listen_address(listen_address))
for address in config[CONF_ADDRESSES]:
cg.add(var.add_address(str(address)))
cg.add(var.set_addresses([str(addr) for addr in config[CONF_ADDRESSES]]))
if on_receive := config.get(CONF_ON_RECEIVE):
on_receive = on_receive[0]
trigger = cg.new_Pvariable(on_receive[CONF_TRIGGER_ID])

View File

@@ -5,8 +5,7 @@
#include "esphome/components/network/util.h"
#include "udp_component.h"
namespace esphome {
namespace udp {
namespace esphome::udp {
static const char *const TAG = "udp";
@@ -95,7 +94,7 @@ void UDPComponent::setup() {
// 8266 and RP2040 `Duino
for (const auto &address : this->addresses_) {
auto ipaddr = IPAddress();
ipaddr.fromString(address.c_str());
ipaddr.fromString(address);
this->ipaddrs_.push_back(ipaddr);
}
if (this->should_listen_)
@@ -130,8 +129,8 @@ void UDPComponent::dump_config() {
" Listen Port: %u\n"
" Broadcast Port: %u",
this->listen_port_, this->broadcast_port_);
for (const auto &address : this->addresses_)
ESP_LOGCONFIG(TAG, " Address: %s", address.c_str());
for (const char *address : this->addresses_)
ESP_LOGCONFIG(TAG, " Address: %s", address);
if (this->listen_address_.has_value()) {
char addr_buf[network::IP_ADDRESS_BUFFER_SIZE];
ESP_LOGCONFIG(TAG, " Listen address: %s", this->listen_address_.value().str_to(addr_buf));
@@ -162,7 +161,6 @@ void UDPComponent::send_packet(const uint8_t *data, size_t size) {
}
#endif
}
} // namespace udp
} // namespace esphome
} // namespace esphome::udp
#endif

View File

@@ -2,6 +2,7 @@
#include "esphome/core/defines.h"
#ifdef USE_NETWORK
#include "esphome/core/helpers.h"
#include "esphome/components/network/ip_address.h"
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
#include "esphome/components/socket/socket.h"
@@ -9,15 +10,17 @@
#ifdef USE_SOCKET_IMPL_LWIP_TCP
#include <WiFiUdp.h>
#endif
#include <initializer_list>
#include <vector>
namespace esphome {
namespace udp {
namespace esphome::udp {
static const size_t MAX_PACKET_SIZE = 508;
class UDPComponent : public Component {
public:
void add_address(const char *addr) { this->addresses_.emplace_back(addr); }
void set_addresses(std::initializer_list<const char *> addresses) { this->addresses_ = addresses; }
/// Prevent accidental use of std::string which would dangle
void set_addresses(std::initializer_list<std::string> addresses) = delete;
void set_listen_address(const char *listen_addr) { this->listen_address_ = network::IPAddress(listen_addr); }
void set_listen_port(uint16_t port) { this->listen_port_ = port; }
void set_broadcast_port(uint16_t port) { this->broadcast_port_ = port; }
@@ -49,11 +52,10 @@ class UDPComponent : public Component {
std::vector<IPAddress> ipaddrs_{};
WiFiUDP udp_client_{};
#endif
std::vector<std::string> addresses_{};
FixedVector<const char *> addresses_{};
optional<network::IPAddress> listen_address_{};
};
} // namespace udp
} // namespace esphome
} // namespace esphome::udp
#endif

View File

@@ -10,7 +10,7 @@ static const char *const TAG = "update";
void UpdateEntity::publish_state() {
ESP_LOGD(TAG,
"'%s' - Publishing:\n"
"'%s' >>\n"
" Current Version: %s",
this->name_.c_str(), this->update_info_.current_version.c_str());

View File

@@ -133,7 +133,7 @@ void Valve::add_on_state_callback(std::function<void()> &&f) { this->state_callb
void Valve::publish_state(bool save) {
this->position = clamp(this->position, 0.0f, 1.0f);
ESP_LOGD(TAG, "'%s' - Publishing:", this->name_.c_str());
ESP_LOGD(TAG, "'%s' >>", this->name_.c_str());
auto traits = this->get_traits();
if (traits.get_supports_position()) {
ESP_LOGD(TAG, " Position: %.0f%%", this->position * 100.0f);

View File

@@ -153,7 +153,7 @@ void WaterHeater::setup() {
void WaterHeater::publish_state() {
auto traits = this->get_traits();
ESP_LOGD(TAG,
"'%s' - Sending state:\n"
"'%s' >>\n"
" Mode: %s",
this->name_.c_str(), LOG_STR_ARG(water_heater_mode_to_string(this->mode_)));
if (!std::isnan(this->current_temperature_)) {

View File

@@ -203,7 +203,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_OTA): cv.boolean,
cv.Optional(CONF_LOG, default=True): cv.boolean,
cv.Optional(CONF_LOCAL): cv.boolean,
cv.Optional(CONF_COMPRESSION, default="br"): cv.one_of("br", "gzip"),
cv.Optional(CONF_COMPRESSION, default="gzip"): cv.one_of("gzip", "br"),
cv.Optional(CONF_SORTING_GROUPS): cv.ensure_list(sorting_group),
}
).extend(cv.COMPONENT_SCHEMA),

View File

@@ -143,7 +143,7 @@ bool ListEntitiesIterator::on_water_heater(water_heater::WaterHeater *obj) {
#ifdef USE_INFRARED
bool ListEntitiesIterator::on_infrared(infrared::Infrared *obj) {
// Infrared web_server support not yet implemented - this stub acknowledges the entity
this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::infrared_all_json_generator);
return true;
}
#endif

View File

@@ -33,6 +33,10 @@
#include "esphome/components/water_heater/water_heater.h"
#endif
#ifdef USE_INFRARED
#include "esphome/components/infrared/infrared.h"
#endif
#ifdef USE_WEBSERVER_LOCAL
#if USE_WEBSERVER_VERSION == 2
#include "server_index_v2.h"
@@ -658,6 +662,24 @@ std::string WebServer::text_sensor_json_(text_sensor::TextSensor *obj, const std
#endif
#ifdef USE_SWITCH
enum SwitchAction : uint8_t { SWITCH_ACTION_NONE, SWITCH_ACTION_TOGGLE, SWITCH_ACTION_TURN_ON, SWITCH_ACTION_TURN_OFF };
static void execute_switch_action(switch_::Switch *obj, SwitchAction action) {
switch (action) {
case SWITCH_ACTION_TOGGLE:
obj->toggle();
break;
case SWITCH_ACTION_TURN_ON:
obj->turn_on();
break;
case SWITCH_ACTION_TURN_OFF:
obj->turn_off();
break;
default:
break;
}
}
void WebServer::on_switch_update(switch_::Switch *obj) {
if (!this->include_internal_ && obj->is_internal())
return;
@@ -676,34 +698,22 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM
return;
}
// Handle action methods with single defer and response
enum SwitchAction { NONE, TOGGLE, TURN_ON, TURN_OFF };
SwitchAction action = NONE;
SwitchAction action = SWITCH_ACTION_NONE;
if (match.method_equals(ESPHOME_F("toggle"))) {
action = TOGGLE;
action = SWITCH_ACTION_TOGGLE;
} else if (match.method_equals(ESPHOME_F("turn_on"))) {
action = TURN_ON;
action = SWITCH_ACTION_TURN_ON;
} else if (match.method_equals(ESPHOME_F("turn_off"))) {
action = TURN_OFF;
action = SWITCH_ACTION_TURN_OFF;
}
if (action != NONE) {
this->defer([obj, action]() {
switch (action) {
case TOGGLE:
obj->toggle();
break;
case TURN_ON:
obj->turn_on();
break;
case TURN_OFF:
obj->turn_off();
break;
default:
break;
}
});
if (action != SWITCH_ACTION_NONE) {
#ifdef USE_ESP8266
execute_switch_action(obj, action);
#else
this->defer([obj, action]() { execute_switch_action(obj, action); });
#endif
request->send(200);
} else {
request->send(404);
@@ -743,7 +753,7 @@ void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlM
std::string data = this->button_json_(obj, detail);
request->send(200, "application/json", data.c_str());
} else if (match.method_equals(ESPHOME_F("press"))) {
this->defer([obj]() { obj->press(); });
DEFER_ACTION(obj, obj->press());
request->send(200);
return;
} else {
@@ -753,9 +763,6 @@ void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlM
}
request->send(404);
}
std::string WebServer::button_state_json_generator(WebServer *web_server, void *source) {
return web_server->button_json_((button::Button *) (source), DETAIL_STATE);
}
std::string WebServer::button_all_json_generator(WebServer *web_server, void *source) {
return web_server->button_json_((button::Button *) (source), DETAIL_ALL);
}
@@ -831,7 +838,7 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc
std::string data = this->fan_json_(obj, detail);
request->send(200, "application/json", data.c_str());
} else if (match.method_equals(ESPHOME_F("toggle"))) {
this->defer([obj]() { obj->toggle().perform(); });
DEFER_ACTION(obj, obj->toggle().perform());
request->send(200);
} else {
bool is_on = match.method_equals(ESPHOME_F("turn_on"));
@@ -862,7 +869,7 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc
return;
}
}
this->defer([call]() mutable { call.perform(); });
DEFER_ACTION(call, call.perform());
request->send(200);
}
return;
@@ -912,7 +919,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa
std::string data = this->light_json_(obj, detail);
request->send(200, "application/json", data.c_str());
} else if (match.method_equals(ESPHOME_F("toggle"))) {
this->defer([obj]() { obj->toggle().perform(); });
DEFER_ACTION(obj, obj->toggle().perform());
request->send(200);
} else {
bool is_on = match.method_equals(ESPHOME_F("turn_on"));
@@ -941,7 +948,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa
parse_string_param_(request, ESPHOME_F("effect"), call, &decltype(call)::set_effect);
}
this->defer([call]() mutable { call.perform(); });
DEFER_ACTION(call, call.perform());
request->send(200);
}
return;
@@ -1030,7 +1037,7 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa
parse_float_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position);
parse_float_param_(request, ESPHOME_F("tilt"), call, &decltype(call)::set_tilt);
this->defer([call]() mutable { call.perform(); });
DEFER_ACTION(call, call.perform());
request->send(200);
return;
}
@@ -1089,7 +1096,7 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM
auto call = obj->make_call();
parse_float_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_value);
this->defer([call]() mutable { call.perform(); });
DEFER_ACTION(call, call.perform());
request->send(200);
return;
}
@@ -1162,7 +1169,7 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat
parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_date);
this->defer([call]() mutable { call.perform(); });
DEFER_ACTION(call, call.perform());
request->send(200);
return;
}
@@ -1226,7 +1233,7 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat
parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_time);
this->defer([call]() mutable { call.perform(); });
DEFER_ACTION(call, call.perform());
request->send(200);
return;
}
@@ -1289,7 +1296,7 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur
parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_datetime);
this->defer([call]() mutable { call.perform(); });
DEFER_ACTION(call, call.perform());
request->send(200);
return;
}
@@ -1349,7 +1356,7 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat
auto call = obj->make_call();
parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_value);
this->defer([call]() mutable { call.perform(); });
DEFER_ACTION(call, call.perform());
request->send(200);
return;
}
@@ -1407,7 +1414,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM
auto call = obj->make_call();
parse_string_param_(request, ESPHOME_F("option"), call, &decltype(call)::set_option);
this->defer([call]() mutable { call.perform(); });
DEFER_ACTION(call, call.perform());
request->send(200);
return;
}
@@ -1476,7 +1483,7 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url
parse_float_param_(request, ESPHOME_F("target_temperature_low"), call, &decltype(call)::set_target_temperature_low);
parse_float_param_(request, ESPHOME_F("target_temperature"), call, &decltype(call)::set_target_temperature);
this->defer([call]() mutable { call.perform(); });
DEFER_ACTION(call, call.perform());
request->send(200);
return;
}
@@ -1592,6 +1599,24 @@ std::string WebServer::climate_json_(climate::Climate *obj, JsonDetail start_con
#endif
#ifdef USE_LOCK
enum LockAction : uint8_t { LOCK_ACTION_NONE, LOCK_ACTION_LOCK, LOCK_ACTION_UNLOCK, LOCK_ACTION_OPEN };
static void execute_lock_action(lock::Lock *obj, LockAction action) {
switch (action) {
case LOCK_ACTION_LOCK:
obj->lock();
break;
case LOCK_ACTION_UNLOCK:
obj->unlock();
break;
case LOCK_ACTION_OPEN:
obj->open();
break;
default:
break;
}
}
void WebServer::on_lock_update(lock::Lock *obj) {
if (!this->include_internal_ && obj->is_internal())
return;
@@ -1610,34 +1635,22 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat
return;
}
// Handle action methods with single defer and response
enum LockAction { NONE, LOCK, UNLOCK, OPEN };
LockAction action = NONE;
LockAction action = LOCK_ACTION_NONE;
if (match.method_equals(ESPHOME_F("lock"))) {
action = LOCK;
action = LOCK_ACTION_LOCK;
} else if (match.method_equals(ESPHOME_F("unlock"))) {
action = UNLOCK;
action = LOCK_ACTION_UNLOCK;
} else if (match.method_equals(ESPHOME_F("open"))) {
action = OPEN;
action = LOCK_ACTION_OPEN;
}
if (action != NONE) {
this->defer([obj, action]() {
switch (action) {
case LOCK:
obj->lock();
break;
case UNLOCK:
obj->unlock();
break;
case OPEN:
obj->open();
break;
default:
break;
}
});
if (action != LOCK_ACTION_NONE) {
#ifdef USE_ESP8266
execute_lock_action(obj, action);
#else
this->defer([obj, action]() { execute_lock_action(obj, action); });
#endif
request->send(200);
} else {
request->send(404);
@@ -1720,7 +1733,7 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa
parse_float_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position);
this->defer([call]() mutable { call.perform(); });
DEFER_ACTION(call, call.perform());
request->send(200);
return;
}
@@ -1799,7 +1812,7 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques
return;
}
this->defer([call]() mutable { call.perform(); });
DEFER_ACTION(call, call.perform());
request->send(200);
return;
}
@@ -1875,7 +1888,7 @@ void WebServer::handle_water_heater_request(AsyncWebServerRequest *request, cons
// Parse on/off parameter
parse_bool_param_(request, ESPHOME_F("is_on"), base_call, &water_heater::WaterHeaterCall::set_on);
this->defer([call]() mutable { call.perform(); });
DEFER_ACTION(call, call.perform());
request->send(200);
return;
}
@@ -1943,6 +1956,110 @@ std::string WebServer::water_heater_json_(water_heater::WaterHeater *obj, JsonDe
}
#endif
#ifdef USE_INFRARED
void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (infrared::Infrared *obj : App.get_infrareds()) {
auto entity_match = match.match_entity(obj);
if (!entity_match.matched)
continue;
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request);
std::string data = this->infrared_json_(obj, detail);
request->send(200, ESPHOME_F("application/json"), data.c_str());
return;
}
if (!match.method_equals(ESPHOME_F("transmit"))) {
request->send(404);
return;
}
// Only allow transmit if the device supports it
if (!obj->has_transmitter()) {
request->send(400, ESPHOME_F("text/plain"), "Device does not support transmission");
return;
}
// Parse parameters
auto call = obj->make_call();
// Parse carrier frequency (optional)
if (request->hasParam(ESPHOME_F("carrier_frequency"))) {
auto value = parse_number<uint32_t>(request->getParam(ESPHOME_F("carrier_frequency"))->value().c_str());
if (value.has_value()) {
call.set_carrier_frequency(*value);
}
}
// Parse repeat count (optional, defaults to 1)
if (request->hasParam(ESPHOME_F("repeat_count"))) {
auto value = parse_number<uint32_t>(request->getParam(ESPHOME_F("repeat_count"))->value().c_str());
if (value.has_value()) {
call.set_repeat_count(*value);
}
}
// Parse base64url-encoded raw timings (required)
// Base64url is URL-safe: uses A-Za-z0-9-_ (no special characters needing escaping)
if (!request->hasParam(ESPHOME_F("data"))) {
request->send(400, ESPHOME_F("text/plain"), "Missing 'data' parameter");
return;
}
// .c_str() is required for Arduino framework where value() returns Arduino String instead of std::string
std::string encoded =
request->getParam(ESPHOME_F("data"))->value().c_str(); // NOLINT(readability-redundant-string-cstr)
// Validate base64url is not empty
if (encoded.empty()) {
request->send(400, ESPHOME_F("text/plain"), "Empty 'data' parameter");
return;
}
#ifdef USE_ESP8266
// ESP8266 is single-threaded, call directly
call.set_raw_timings_base64url(encoded);
call.perform();
#else
// Defer to main loop for thread safety. Move encoded string into lambda to ensure
// it outlives the call - set_raw_timings_base64url stores a pointer, so the string
// must remain valid until perform() completes.
this->defer([call, encoded = std::move(encoded)]() mutable {
call.set_raw_timings_base64url(encoded);
call.perform();
});
#endif
request->send(200);
return;
}
request->send(404);
}
std::string WebServer::infrared_all_json_generator(WebServer *web_server, void *source) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
return web_server->infrared_json_(static_cast<infrared::Infrared *>(source), DETAIL_ALL);
}
std::string WebServer::infrared_json_(infrared::Infrared *obj, JsonDetail start_config) {
json::JsonBuilder builder;
JsonObject root = builder.root();
set_json_icon_state_value(root, obj, "infrared", "", 0, start_config);
auto traits = obj->get_traits();
root[ESPHOME_F("supports_transmitter")] = traits.get_supports_transmitter();
root[ESPHOME_F("supports_receiver")] = traits.get_supports_receiver();
if (start_config == DETAIL_ALL) {
this->add_sorting_info_(root, obj);
}
return builder.serialize();
}
#endif
#ifdef USE_EVENT
void WebServer::on_event(event::Event *obj) {
if (!this->include_internal_ && obj->is_internal())
@@ -2035,7 +2152,7 @@ void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlM
return;
}
this->defer([obj]() mutable { obj->perform(); });
DEFER_ACTION(obj, obj->perform());
request->send(200);
return;
}
@@ -2074,24 +2191,21 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) const {
const auto &url = request->url();
const auto method = request->method();
// Static URL checks
static const char *const STATIC_URLS[] = {
"/",
// Static URL checks - use ESPHOME_F to keep strings in flash on ESP8266
if (url == ESPHOME_F("/"))
return true;
#if !defined(USE_ESP32) && defined(USE_ARDUINO)
"/events",
if (url == ESPHOME_F("/events"))
return true;
#endif
#ifdef USE_WEBSERVER_CSS_INCLUDE
"/0.css",
if (url == ESPHOME_F("/0.css"))
return true;
#endif
#ifdef USE_WEBSERVER_JS_INCLUDE
"/0.js",
if (url == ESPHOME_F("/0.js"))
return true;
#endif
};
for (const auto &static_url : STATIC_URLS) {
if (url == static_url)
return true;
}
#ifdef USE_WEBSERVER_PRIVATE_NETWORK_ACCESS
if (method == HTTP_OPTIONS && request->hasHeader(ESPHOME_F("Access-Control-Request-Private-Network")))
@@ -2111,90 +2225,100 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) const {
if (!is_get_or_post)
return false;
// Use lookup tables for domain checks
static const char *const GET_ONLY_DOMAINS[] = {
// Check GET-only domains - use ESPHOME_F to keep strings in flash on ESP8266
if (is_get) {
#ifdef USE_SENSOR
"sensor",
if (match.domain_equals(ESPHOME_F("sensor")))
return true;
#endif
#ifdef USE_BINARY_SENSOR
"binary_sensor",
if (match.domain_equals(ESPHOME_F("binary_sensor")))
return true;
#endif
#ifdef USE_TEXT_SENSOR
"text_sensor",
if (match.domain_equals(ESPHOME_F("text_sensor")))
return true;
#endif
#ifdef USE_EVENT
"event",
if (match.domain_equals(ESPHOME_F("event")))
return true;
#endif
};
static const char *const GET_POST_DOMAINS[] = {
#ifdef USE_SWITCH
"switch",
#endif
#ifdef USE_BUTTON
"button",
#endif
#ifdef USE_FAN
"fan",
#endif
#ifdef USE_LIGHT
"light",
#endif
#ifdef USE_COVER
"cover",
#endif
#ifdef USE_NUMBER
"number",
#endif
#ifdef USE_DATETIME_DATE
"date",
#endif
#ifdef USE_DATETIME_TIME
"time",
#endif
#ifdef USE_DATETIME_DATETIME
"datetime",
#endif
#ifdef USE_TEXT
"text",
#endif
#ifdef USE_SELECT
"select",
#endif
#ifdef USE_CLIMATE
"climate",
#endif
#ifdef USE_LOCK
"lock",
#endif
#ifdef USE_VALVE
"valve",
#endif
#ifdef USE_ALARM_CONTROL_PANEL
"alarm_control_panel",
#endif
#ifdef USE_UPDATE
"update",
#endif
#ifdef USE_WATER_HEATER
"water_heater",
#endif
};
// Check GET-only domains
if (is_get) {
for (const auto &domain : GET_ONLY_DOMAINS) {
if (match.domain_equals(domain))
return true;
}
}
// Check GET+POST domains
if (is_get_or_post) {
for (const auto &domain : GET_POST_DOMAINS) {
if (match.domain_equals(domain))
return true;
}
#ifdef USE_SWITCH
if (match.domain_equals(ESPHOME_F("switch")))
return true;
#endif
#ifdef USE_BUTTON
if (match.domain_equals(ESPHOME_F("button")))
return true;
#endif
#ifdef USE_FAN
if (match.domain_equals(ESPHOME_F("fan")))
return true;
#endif
#ifdef USE_LIGHT
if (match.domain_equals(ESPHOME_F("light")))
return true;
#endif
#ifdef USE_COVER
if (match.domain_equals(ESPHOME_F("cover")))
return true;
#endif
#ifdef USE_NUMBER
if (match.domain_equals(ESPHOME_F("number")))
return true;
#endif
#ifdef USE_DATETIME_DATE
if (match.domain_equals(ESPHOME_F("date")))
return true;
#endif
#ifdef USE_DATETIME_TIME
if (match.domain_equals(ESPHOME_F("time")))
return true;
#endif
#ifdef USE_DATETIME_DATETIME
if (match.domain_equals(ESPHOME_F("datetime")))
return true;
#endif
#ifdef USE_TEXT
if (match.domain_equals(ESPHOME_F("text")))
return true;
#endif
#ifdef USE_SELECT
if (match.domain_equals(ESPHOME_F("select")))
return true;
#endif
#ifdef USE_CLIMATE
if (match.domain_equals(ESPHOME_F("climate")))
return true;
#endif
#ifdef USE_LOCK
if (match.domain_equals(ESPHOME_F("lock")))
return true;
#endif
#ifdef USE_VALVE
if (match.domain_equals(ESPHOME_F("valve")))
return true;
#endif
#ifdef USE_ALARM_CONTROL_PANEL
if (match.domain_equals(ESPHOME_F("alarm_control_panel")))
return true;
#endif
#ifdef USE_UPDATE
if (match.domain_equals(ESPHOME_F("update")))
return true;
#endif
#ifdef USE_WATER_HEATER
if (match.domain_equals(ESPHOME_F("water_heater")))
return true;
#endif
#ifdef USE_INFRARED
if (match.domain_equals(ESPHOME_F("infrared")))
return true;
#endif
}
return false;
@@ -2343,6 +2467,11 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) {
else if (match.domain_equals(ESPHOME_F("water_heater"))) {
this->handle_water_heater_request(request, match);
}
#endif
#ifdef USE_INFRARED
else if (match.domain_equals(ESPHOME_F("infrared"))) {
this->handle_infrared_request(request, match);
}
#endif
else {
// No matching handler found - send 404

View File

@@ -42,6 +42,14 @@ using ParamNameType = const __FlashStringHelper *;
using ParamNameType = const char *;
#endif
// ESP8266 is single-threaded, so actions can execute directly in request context.
// Multi-core platforms need to defer to main loop thread for thread safety.
#ifdef USE_ESP8266
#define DEFER_ACTION(capture, action) action
#else
#define DEFER_ACTION(capture, action) this->defer([capture]() mutable { action; })
#endif
/// Result of matching a URL against an entity
struct EntityMatchResult {
bool matched; ///< True if entity matched the URL
@@ -295,7 +303,7 @@ class WebServer : public Controller,
/// Handle a button request under '/button/<id>/press'.
void handle_button_request(AsyncWebServerRequest *request, const UrlMatch &match);
static std::string button_state_json_generator(WebServer *web_server, void *source);
// Buttons are stateless, so there is no button_state_json_generator
static std::string button_all_json_generator(WebServer *web_server, void *source);
#endif
@@ -452,6 +460,13 @@ class WebServer : public Controller,
static std::string water_heater_all_json_generator(WebServer *web_server, void *source);
#endif
#ifdef USE_INFRARED
/// Handle an infrared request under '/infrared/<id>/transmit'.
void handle_infrared_request(AsyncWebServerRequest *request, const UrlMatch &match);
static std::string infrared_all_json_generator(WebServer *web_server, void *source);
#endif
#ifdef USE_EVENT
void on_event(event::Event *obj) override;
@@ -654,6 +669,9 @@ class WebServer : public Controller,
#ifdef USE_WATER_HEATER
std::string water_heater_json_(water_heater::WaterHeater *obj, JsonDetail start_config);
#endif
#ifdef USE_INFRARED
std::string infrared_json_(infrared::Infrared *obj, JsonDetail start_config);
#endif
#ifdef USE_UPDATE
std::string update_json_(update::UpdateEntity *obj, JsonDetail start_config);
#endif

View File

@@ -4,19 +4,13 @@
/// @details The classes declared in this file can be used by the Weikai family
#include "weikai.h"
#include "esphome/core/helpers.h"
namespace esphome {
namespace weikai {
static const char *const TAG = "weikai";
/// @brief convert an int to binary representation as C++ std::string
/// @param val integer to convert
/// @return a std::string
inline std::string i2s(uint8_t val) { return std::bitset<8>(val).to_string(); }
/// Convert std::string to C string
#define I2S2CS(val) (i2s(val).c_str())
/// @brief measure the time elapsed between two calls
/// @param last_time time of the previous call
/// @return the elapsed time in milliseconds
@@ -170,17 +164,18 @@ void WeikaiComponent::test_gpio_input_() {
static bool init_input{false};
static uint8_t state{0};
uint8_t value;
char bin_buf[9]; // 8 binary digits + null
if (!init_input) {
init_input = true;
// set all pins in input mode
this->reg(WKREG_GPDIR, 0) = 0x00;
ESP_LOGI(TAG, "initializing all pins to input mode");
state = this->reg(WKREG_GPDAT, 0);
ESP_LOGI(TAG, "initial input data state = %02X (%s)", state, I2S2CS(state));
ESP_LOGI(TAG, "initial input data state = %02X (%s)", state, format_bin_to(bin_buf, state));
}
value = this->reg(WKREG_GPDAT, 0);
if (value != state) {
ESP_LOGI(TAG, "Input data changed from %02X to %02X (%s)", state, value, I2S2CS(value));
ESP_LOGI(TAG, "Input data changed from %02X to %02X (%s)", state, value, format_bin_to(bin_buf, value));
state = value;
}
}
@@ -188,6 +183,7 @@ void WeikaiComponent::test_gpio_input_() {
void WeikaiComponent::test_gpio_output_() {
static bool init_output{false};
static uint8_t state{0};
char bin_buf[9]; // 8 binary digits + null
if (!init_output) {
init_output = true;
// set all pins in output mode
@@ -198,7 +194,7 @@ void WeikaiComponent::test_gpio_output_() {
}
state = ~state;
this->reg(WKREG_GPDAT, 0) = state;
ESP_LOGI(TAG, "Flipping all outputs to %02X (%s)", state, I2S2CS(state));
ESP_LOGI(TAG, "Flipping all outputs to %02X (%s)", state, format_bin_to(bin_buf, state));
delay(100); // NOLINT
}
#endif
@@ -208,7 +204,9 @@ void WeikaiComponent::test_gpio_output_() {
///////////////////////////////////////////////////////////////////////////////
bool WeikaiComponent::read_pin_val_(uint8_t pin) {
this->input_state_ = this->reg(WKREG_GPDAT, 0);
ESP_LOGVV(TAG, "reading input pin %u = %u in_state %s", pin, this->input_state_ & (1 << pin), I2S2CS(input_state_));
char bin_buf[9];
ESP_LOGVV(TAG, "reading input pin %u = %u in_state %s", pin, this->input_state_ & (1 << pin),
format_bin_to(bin_buf, this->input_state_));
return this->input_state_ & (1 << pin);
}
@@ -218,7 +216,9 @@ void WeikaiComponent::write_pin_val_(uint8_t pin, bool value) {
} else {
this->output_state_ &= ~(1 << pin);
}
ESP_LOGVV(TAG, "writing output pin %d with %d out_state %s", pin, uint8_t(value), I2S2CS(this->output_state_));
char bin_buf[9];
ESP_LOGVV(TAG, "writing output pin %d with %d out_state %s", pin, uint8_t(value),
format_bin_to(bin_buf, this->output_state_));
this->reg(WKREG_GPDAT, 0) = this->output_state_;
}
@@ -232,7 +232,8 @@ void WeikaiComponent::set_pin_direction_(uint8_t pin, gpio::Flags flags) {
ESP_LOGE(TAG, "pin %d direction invalid", pin);
}
}
ESP_LOGVV(TAG, "setting pin %d direction to %d pin_config=%s", pin, flags, I2S2CS(this->pin_config_));
char bin_buf[9];
ESP_LOGVV(TAG, "setting pin %d direction to %d pin_config=%s", pin, flags, format_bin_to(bin_buf, this->pin_config_));
this->reg(WKREG_GPDIR, 0) = this->pin_config_; // TODO check ~
}
@@ -241,7 +242,6 @@ void WeikaiGPIOPin::setup() {
flags_ == gpio::FLAG_INPUT ? "Input"
: this->flags_ == gpio::FLAG_OUTPUT ? "Output"
: "NOT SPECIFIED");
// ESP_LOGCONFIG(TAG, "Setting GPIO pins mode to '%s' %02X", I2S2CS(this->flags_), this->flags_);
this->pin_mode(this->flags_);
}
@@ -297,8 +297,9 @@ void WeikaiChannel::set_line_param_() {
break; // no parity 000x
}
this->reg(WKREG_LCR) = lcr; // write LCR
char bin_buf[9];
ESP_LOGV(TAG, " line config: %d data_bits, %d stop_bits, parity %s register [%s]", this->data_bits_,
this->stop_bits_, p2s(this->parity_), I2S2CS(lcr));
this->stop_bits_, p2s(this->parity_), format_bin_to(bin_buf, lcr));
}
void WeikaiChannel::set_baudrate_() {
@@ -334,7 +335,8 @@ size_t WeikaiChannel::tx_in_fifo_() {
if (tfcnt == 0) {
uint8_t const fsr = this->reg(WKREG_FSR);
if (fsr & FSR_TFFULL) {
ESP_LOGVV(TAG, "tx FIFO full FSR=%s", I2S2CS(fsr));
char bin_buf[9];
ESP_LOGVV(TAG, "tx FIFO full FSR=%s", format_bin_to(bin_buf, fsr));
tfcnt = FIFO_SIZE;
}
}
@@ -346,14 +348,15 @@ size_t WeikaiChannel::rx_in_fifo_() {
size_t available = this->reg(WKREG_RFCNT);
uint8_t const fsr = this->reg(WKREG_FSR);
if (fsr & (FSR_RFOE | FSR_RFLB | FSR_RFFE | FSR_RFPE)) {
char bin_buf[9];
if (fsr & FSR_RFOE)
ESP_LOGE(TAG, "Receive data overflow FSR=%s", I2S2CS(fsr));
ESP_LOGE(TAG, "Receive data overflow FSR=%s", format_bin_to(bin_buf, fsr));
if (fsr & FSR_RFLB)
ESP_LOGE(TAG, "Receive line break FSR=%s", I2S2CS(fsr));
ESP_LOGE(TAG, "Receive line break FSR=%s", format_bin_to(bin_buf, fsr));
if (fsr & FSR_RFFE)
ESP_LOGE(TAG, "Receive frame error FSR=%s", I2S2CS(fsr));
ESP_LOGE(TAG, "Receive frame error FSR=%s", format_bin_to(bin_buf, fsr));
if (fsr & FSR_RFPE)
ESP_LOGE(TAG, "Receive parity error FSR=%s", I2S2CS(fsr));
ESP_LOGE(TAG, "Receive parity error FSR=%s", format_bin_to(bin_buf, fsr));
}
if ((available == 0) && (fsr & FSR_RFDAT)) {
// here we should be very careful because we can have something like this:
@@ -362,11 +365,13 @@ size_t WeikaiChannel::rx_in_fifo_() {
// - so to be sure we need to do another read of RFCNT and if it is still zero -> buffer full
available = this->reg(WKREG_RFCNT);
if (available == 0) { // still zero ?
ESP_LOGV(TAG, "rx FIFO is full FSR=%s", I2S2CS(fsr));
char bin_buf[9];
ESP_LOGV(TAG, "rx FIFO is full FSR=%s", format_bin_to(bin_buf, fsr));
available = FIFO_SIZE;
}
}
ESP_LOGVV(TAG, "rx FIFO contain %d bytes - FSR status=%s", available, I2S2CS(fsr));
char bin_buf2[9];
ESP_LOGVV(TAG, "rx FIFO contain %d bytes - FSR status=%s", available, format_bin_to(bin_buf2, fsr));
return available;
}

View File

@@ -8,7 +8,6 @@
/// wk2132_i2c, wk2168_i2c, wk2204_i2c, wk2212_i2c
#pragma once
#include <bitset>
#include <memory>
#include <cinttypes>
#include "esphome/core/component.h"

View File

@@ -10,13 +10,6 @@ namespace weikai_spi {
using namespace weikai;
static const char *const TAG = "weikai_spi";
/// @brief convert an int to binary representation as C++ std::string
/// @param val integer to convert
/// @return a std::string
inline std::string i2s(uint8_t val) { return std::bitset<8>(val).to_string(); }
/// Convert std::string to C string
#define I2S2CS(val) (i2s(val).c_str())
/// @brief measure the time elapsed between two calls
/// @param last_time time of the previous call
/// @return the elapsed time in microseconds
@@ -107,7 +100,8 @@ uint8_t WeikaiRegisterSPI::read_reg() const {
spi_comp->write_byte(cmd);
uint8_t val = spi_comp->read_byte();
spi_comp->disable();
ESP_LOGVV(TAG, "WeikaiRegisterSPI::read_reg() cmd=%s(%02X) reg=%s ch=%d buf=%02X", I2S2CS(cmd), cmd,
char bin_buf[9];
ESP_LOGVV(TAG, "WeikaiRegisterSPI::read_reg() cmd=%s(%02X) reg=%s ch=%d buf=%02X", format_bin_to(bin_buf, cmd), cmd,
reg_to_str(this->register_, this->comp_->page1()), this->channel_, val);
return val;
}
@@ -120,8 +114,9 @@ void WeikaiRegisterSPI::read_fifo(uint8_t *data, size_t length) const {
spi_comp->read_array(data, length);
spi_comp->disable();
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
ESP_LOGVV(TAG, "WeikaiRegisterSPI::read_fifo() cmd=%s(%02X) ch=%d len=%d buffer", I2S2CS(cmd), cmd, this->channel_,
length);
char bin_buf[9];
ESP_LOGVV(TAG, "WeikaiRegisterSPI::read_fifo() cmd=%s(%02X) ch=%d len=%d buffer", format_bin_to(bin_buf, cmd), cmd,
this->channel_, length);
print_buffer(data, length);
#endif
}
@@ -132,8 +127,9 @@ void WeikaiRegisterSPI::write_reg(uint8_t value) {
spi_comp->enable();
spi_comp->write_array(buf, 2);
spi_comp->disable();
ESP_LOGVV(TAG, "WeikaiRegisterSPI::write_reg() cmd=%s(%02X) reg=%s ch=%d buf=%02X", I2S2CS(buf[0]), buf[0],
reg_to_str(this->register_, this->comp_->page1()), this->channel_, buf[1]);
char bin_buf[9];
ESP_LOGVV(TAG, "WeikaiRegisterSPI::write_reg() cmd=%s(%02X) reg=%s ch=%d buf=%02X", format_bin_to(bin_buf, buf[0]),
buf[0], reg_to_str(this->register_, this->comp_->page1()), this->channel_, buf[1]);
}
void WeikaiRegisterSPI::write_fifo(uint8_t *data, size_t length) {
@@ -145,8 +141,9 @@ void WeikaiRegisterSPI::write_fifo(uint8_t *data, size_t length) {
spi_comp->disable();
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
ESP_LOGVV(TAG, "WeikaiRegisterSPI::write_fifo() cmd=%s(%02X) ch=%d len=%d buffer", I2S2CS(cmd), cmd, this->channel_,
length);
char bin_buf[9];
ESP_LOGVV(TAG, "WeikaiRegisterSPI::write_fifo() cmd=%s(%02X) ch=%d len=%d buffer", format_bin_to(bin_buf, cmd), cmd,
this->channel_, length);
print_buffer(data, length);
#endif
}

View File

@@ -6,7 +6,6 @@
/// wk2124_spi, wk2132_spi, wk2168_spi, wk2204_spi, wk2212_spi,
#pragma once
#include <bitset>
#include <memory>
#include "esphome/core/component.h"
#include "esphome/components/uart/uart.h"

View File

@@ -30,6 +30,7 @@ _WG_KEY_REGEX = re.compile(r"^[A-Za-z0-9+/]{42}[AEIMQUYcgkosw480]=$")
wireguard_ns = cg.esphome_ns.namespace("wireguard")
Wireguard = wireguard_ns.class_("Wireguard", cg.Component, cg.PollingComponent)
AllowedIP = wireguard_ns.struct("AllowedIP")
WireguardPeerOnlineCondition = wireguard_ns.class_(
"WireguardPeerOnlineCondition", automation.Condition
)
@@ -108,8 +109,18 @@ async def to_code(config):
)
)
for ip in allowed_ips:
cg.add(var.add_allowed_ip(str(ip.network_address), str(ip.netmask)))
cg.add(
var.set_allowed_ips(
[
cg.StructInitializer(
AllowedIP,
("ip", str(ip.network_address)),
("netmask", str(ip.netmask)),
)
for ip in allowed_ips
]
)
)
cg.add(var.set_srctime(await cg.get_variable(config[CONF_TIME_ID])))

View File

@@ -13,8 +13,7 @@
#include <esp_wireguard.h>
#include <esp_wireguard_err.h>
namespace esphome {
namespace wireguard {
namespace esphome::wireguard {
static const char *const TAG = "wireguard";
@@ -28,16 +27,16 @@ static const char *const LOGMSG_ONLINE = "online";
static const char *const LOGMSG_OFFLINE = "offline";
void Wireguard::setup() {
this->wg_config_.address = this->address_.c_str();
this->wg_config_.private_key = this->private_key_.c_str();
this->wg_config_.endpoint = this->peer_endpoint_.c_str();
this->wg_config_.public_key = this->peer_public_key_.c_str();
this->wg_config_.address = this->address_;
this->wg_config_.private_key = this->private_key_;
this->wg_config_.endpoint = this->peer_endpoint_;
this->wg_config_.public_key = this->peer_public_key_;
this->wg_config_.port = this->peer_port_;
this->wg_config_.netmask = this->netmask_.c_str();
this->wg_config_.netmask = this->netmask_;
this->wg_config_.persistent_keepalive = this->keepalive_;
if (!this->preshared_key_.empty())
this->wg_config_.preshared_key = this->preshared_key_.c_str();
if (this->preshared_key_ != nullptr)
this->wg_config_.preshared_key = this->preshared_key_;
this->publish_enabled_state();
@@ -131,6 +130,10 @@ void Wireguard::update() {
}
void Wireguard::dump_config() {
char private_key_masked[MASK_KEY_BUFFER_SIZE];
char preshared_key_masked[MASK_KEY_BUFFER_SIZE];
mask_key_to(private_key_masked, sizeof(private_key_masked), this->private_key_);
mask_key_to(preshared_key_masked, sizeof(preshared_key_masked), this->preshared_key_);
// clang-format off
ESP_LOGCONFIG(
TAG,
@@ -142,13 +145,13 @@ void Wireguard::dump_config() {
" Peer Port: " LOG_SECRET("%d") "\n"
" Peer Public Key: " LOG_SECRET("%s") "\n"
" Peer Pre-shared Key: " LOG_SECRET("%s"),
this->address_.c_str(), this->netmask_.c_str(), mask_key(this->private_key_).c_str(),
this->peer_endpoint_.c_str(), this->peer_port_, this->peer_public_key_.c_str(),
(!this->preshared_key_.empty() ? mask_key(this->preshared_key_).c_str() : "NOT IN USE"));
this->address_, this->netmask_, private_key_masked,
this->peer_endpoint_, this->peer_port_, this->peer_public_key_,
(this->preshared_key_ != nullptr ? preshared_key_masked : "NOT IN USE"));
// clang-format on
ESP_LOGCONFIG(TAG, " Peer Allowed IPs:");
for (auto &allowed_ip : this->allowed_ips_) {
ESP_LOGCONFIG(TAG, " - %s/%s", std::get<0>(allowed_ip).c_str(), std::get<1>(allowed_ip).c_str());
for (const AllowedIP &allowed_ip : this->allowed_ips_) {
ESP_LOGCONFIG(TAG, " - %s/%s", allowed_ip.ip, allowed_ip.netmask);
}
ESP_LOGCONFIG(TAG, " Peer Persistent Keepalive: %d%s", this->keepalive_,
(this->keepalive_ > 0 ? "s" : " (DISABLED)"));
@@ -176,18 +179,6 @@ time_t Wireguard::get_latest_handshake() const {
return result;
}
void Wireguard::set_address(const std::string &address) { this->address_ = address; }
void Wireguard::set_netmask(const std::string &netmask) { this->netmask_ = netmask; }
void Wireguard::set_private_key(const std::string &key) { this->private_key_ = key; }
void Wireguard::set_peer_endpoint(const std::string &endpoint) { this->peer_endpoint_ = endpoint; }
void Wireguard::set_peer_public_key(const std::string &key) { this->peer_public_key_ = key; }
void Wireguard::set_peer_port(const uint16_t port) { this->peer_port_ = port; }
void Wireguard::set_preshared_key(const std::string &key) { this->preshared_key_ = key; }
void Wireguard::add_allowed_ip(const std::string &ip, const std::string &netmask) {
this->allowed_ips_.emplace_back(ip, netmask);
}
void Wireguard::set_keepalive(const uint16_t seconds) { this->keepalive_ = seconds; }
void Wireguard::set_reboot_timeout(const uint32_t seconds) { this->reboot_timeout_ = seconds; }
void Wireguard::set_srctime(time::RealTimeClock *srctime) { this->srctime_ = srctime; }
@@ -274,9 +265,8 @@ void Wireguard::start_connection_() {
ESP_LOGD(TAG, "Configuring allowed IPs list");
bool allowed_ips_ok = true;
for (std::tuple<std::string, std::string> ip : this->allowed_ips_) {
allowed_ips_ok &=
(esp_wireguard_add_allowed_ip(&(this->wg_ctx_), std::get<0>(ip).c_str(), std::get<1>(ip).c_str()) == ESP_OK);
for (const AllowedIP &ip : this->allowed_ips_) {
allowed_ips_ok &= (esp_wireguard_add_allowed_ip(&(this->wg_ctx_), ip.ip, ip.netmask) == ESP_OK);
}
if (allowed_ips_ok) {
@@ -299,8 +289,25 @@ void Wireguard::stop_connection_() {
}
}
std::string mask_key(const std::string &key) { return (key.substr(0, 5) + "[...]="); }
void mask_key_to(char *buffer, size_t len, const char *key) {
// Format: "XXXXX[...]=\0" = MASK_KEY_BUFFER_SIZE chars minimum
if (len < MASK_KEY_BUFFER_SIZE || key == nullptr) {
if (len > 0)
buffer[0] = '\0';
return;
}
// Copy first 5 characters of the key
size_t i = 0;
for (; i < 5 && key[i] != '\0'; ++i) {
buffer[i] = key[i];
}
// Append "[...]="
const char *suffix = "[...]=";
for (size_t j = 0; suffix[j] != '\0' && (i + j) < len - 1; ++j) {
buffer[i + j] = suffix[j];
}
buffer[i + 6] = '\0';
}
} // namespace wireguard
} // namespace esphome
} // namespace esphome::wireguard
#endif

View File

@@ -2,10 +2,10 @@
#include "esphome/core/defines.h"
#ifdef USE_WIREGUARD
#include <ctime>
#include <vector>
#include <tuple>
#include <initializer_list>
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/components/time/real_time_clock.h"
#ifdef USE_BINARY_SENSOR
@@ -22,8 +22,13 @@
#include <esp_wireguard.h>
namespace esphome {
namespace wireguard {
namespace esphome::wireguard {
/// Allowed IP entry for WireGuard peer configuration.
struct AllowedIP {
const char *ip;
const char *netmask;
};
/// Main Wireguard component class.
class Wireguard : public PollingComponent {
@@ -37,15 +42,25 @@ class Wireguard : public PollingComponent {
float get_setup_priority() const override { return esphome::setup_priority::BEFORE_CONNECTION; }
void set_address(const std::string &address);
void set_netmask(const std::string &netmask);
void set_private_key(const std::string &key);
void set_peer_endpoint(const std::string &endpoint);
void set_peer_public_key(const std::string &key);
void set_peer_port(uint16_t port);
void set_preshared_key(const std::string &key);
void set_address(const char *address) { this->address_ = address; }
void set_netmask(const char *netmask) { this->netmask_ = netmask; }
void set_private_key(const char *key) { this->private_key_ = key; }
void set_peer_endpoint(const char *endpoint) { this->peer_endpoint_ = endpoint; }
void set_peer_public_key(const char *key) { this->peer_public_key_ = key; }
void set_peer_port(uint16_t port) { this->peer_port_ = port; }
void set_preshared_key(const char *key) { this->preshared_key_ = key; }
void add_allowed_ip(const std::string &ip, const std::string &netmask);
/// Prevent accidental use of std::string which would dangle
void set_address(const std::string &address) = delete;
void set_netmask(const std::string &netmask) = delete;
void set_private_key(const std::string &key) = delete;
void set_peer_endpoint(const std::string &endpoint) = delete;
void set_peer_public_key(const std::string &key) = delete;
void set_preshared_key(const std::string &key) = delete;
void set_allowed_ips(std::initializer_list<AllowedIP> ips) { this->allowed_ips_ = ips; }
/// Prevent accidental use of std::string which would dangle
void set_allowed_ips(std::initializer_list<std::tuple<std::string, std::string>> ips) = delete;
void set_keepalive(uint16_t seconds);
void set_reboot_timeout(uint32_t seconds);
@@ -83,14 +98,14 @@ class Wireguard : public PollingComponent {
time_t get_latest_handshake() const;
protected:
std::string address_;
std::string netmask_;
std::string private_key_;
std::string peer_endpoint_;
std::string peer_public_key_;
std::string preshared_key_;
const char *address_{nullptr};
const char *netmask_{nullptr};
const char *private_key_{nullptr};
const char *peer_endpoint_{nullptr};
const char *peer_public_key_{nullptr};
const char *preshared_key_{nullptr};
std::vector<std::tuple<std::string, std::string>> allowed_ips_;
FixedVector<AllowedIP> allowed_ips_;
uint16_t peer_port_;
uint16_t keepalive_;
@@ -142,8 +157,11 @@ class Wireguard : public PollingComponent {
void suspend_wdt();
void resume_wdt();
/// Size of buffer required for mask_key_to: 5 chars + "[...]=" + null = 12
static constexpr size_t MASK_KEY_BUFFER_SIZE = 12;
/// Strip most part of the key only for secure printing
std::string mask_key(const std::string &key);
void mask_key_to(char *buffer, size_t len, const char *key);
/// Condition to check if remote peer is online.
template<typename... Ts> class WireguardPeerOnlineCondition : public Condition<Ts...>, public Parented<Wireguard> {
@@ -169,6 +187,5 @@ template<typename... Ts> class WireguardDisableAction : public Action<Ts...>, pu
void play(const Ts &...x) override { this->parent_->disable(); }
};
} // namespace wireguard
} // namespace esphome
} // namespace esphome::wireguard
#endif

View File

@@ -44,7 +44,15 @@ class HashBase {
virtual size_t get_size() const = 0;
protected:
uint8_t digest_[32]; // Storage sized for max(MD5=16, SHA256=32) bytes
// ESP32 variants with DMA-based hardware SHA (all except original ESP32) require 32-byte aligned buffers.
// Original ESP32 uses a different hardware SHA implementation without DMA alignment requirements.
// Other platforms (ESP8266, RP2040, LibreTiny) use software SHA and don't need alignment.
// Storage sized for max(MD5=16, SHA256=32) bytes
#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32)
alignas(32) uint8_t digest_[32];
#else
uint8_t digest_[32];
#endif
};
} // namespace esphome

View File

@@ -404,15 +404,31 @@ std::string format_hex_pretty(const std::string &data, char separator, bool show
return format_hex_pretty_uint8(reinterpret_cast<const uint8_t *>(data.data()), data.length(), separator, show_length);
}
char *format_bin_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length) {
if (buffer_size == 0) {
return buffer;
}
// Calculate max bytes we can format: each byte needs 8 chars
size_t max_bytes = (buffer_size - 1) / 8;
if (max_bytes == 0 || length == 0) {
buffer[0] = '\0';
return buffer;
}
size_t bytes_to_format = std::min(length, max_bytes);
for (size_t byte_idx = 0; byte_idx < bytes_to_format; byte_idx++) {
for (size_t bit_idx = 0; bit_idx < 8; bit_idx++) {
buffer[byte_idx * 8 + bit_idx] = ((data[byte_idx] >> (7 - bit_idx)) & 1) + '0';
}
}
buffer[bytes_to_format * 8] = '\0';
return buffer;
}
std::string format_bin(const uint8_t *data, size_t length) {
std::string result;
result.resize(length * 8);
for (size_t byte_idx = 0; byte_idx < length; byte_idx++) {
for (size_t bit_idx = 0; bit_idx < 8; bit_idx++) {
result[byte_idx * 8 + bit_idx] = ((data[byte_idx] >> (7 - bit_idx)) & 1) + '0';
}
}
format_bin_to(&result[0], length * 8 + 1, data, length);
return result;
}
@@ -487,19 +503,26 @@ static constexpr const char *BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789+/";
// Helper function to find the index of a base64 character in the lookup table.
// Helper function to find the index of a base64/base64url character in the lookup table.
// Returns the character's position (0-63) if found, or 0 if not found.
// Supports both standard base64 (+/) and base64url (-_) alphabets.
// NOTE: This returns 0 for both 'A' (valid base64 char at index 0) and invalid characters.
// This is safe because is_base64() is ALWAYS checked before calling this function,
// preventing invalid characters from ever reaching here. The base64_decode function
// stops processing at the first invalid character due to the is_base64() check in its
// while loop condition, making this edge case harmless in practice.
static inline uint8_t base64_find_char(char c) {
// Handle base64url variants: '-' maps to '+' (index 62), '_' maps to '/' (index 63)
if (c == '-')
return 62;
if (c == '_')
return 63;
const char *pos = strchr(BASE64_CHARS, c);
return pos ? (pos - BASE64_CHARS) : 0;
}
static inline bool is_base64(char c) { return (isalnum(c) || (c == '+') || (c == '/')); }
// Check if character is valid base64 or base64url
static inline bool is_base64(char c) { return (isalnum(c) || (c == '+') || (c == '/') || (c == '-') || (c == '_')); }
std::string base64_encode(const std::vector<uint8_t> &buf) { return base64_encode(buf.data(), buf.size()); }
@@ -617,6 +640,46 @@ std::vector<uint8_t> base64_decode(const std::string &encoded_string) {
return ret;
}
/// Decode base64/base64url string directly into vector of little-endian int32 values
/// @param base64 Base64 or base64url encoded string (both +/ and -_ accepted)
/// @param out Output vector (cleared and filled with decoded int32 values)
/// @return true if successful, false if decode failed or invalid size
bool base64_decode_int32_vector(const std::string &base64, std::vector<int32_t> &out) {
// Decode in chunks to minimize stack usage
constexpr size_t chunk_bytes = 48; // 12 int32 values
constexpr size_t chunk_chars = 64; // 48 * 4/3 = 64 chars
uint8_t chunk[chunk_bytes];
out.clear();
const uint8_t *input = reinterpret_cast<const uint8_t *>(base64.data());
size_t remaining = base64.size();
size_t pos = 0;
while (remaining > 0) {
size_t chars_to_decode = std::min(remaining, chunk_chars);
size_t decoded_len = base64_decode(input + pos, chars_to_decode, chunk, chunk_bytes);
if (decoded_len == 0)
return false;
// Parse little-endian int32 values
for (size_t i = 0; i + 3 < decoded_len; i += 4) {
int32_t timing = static_cast<int32_t>(encode_uint32(chunk[i + 3], chunk[i + 2], chunk[i + 1], chunk[i]));
out.push_back(timing);
}
// Check for incomplete int32 in last chunk
if (remaining <= chunk_chars && (decoded_len % 4) != 0)
return false;
pos += chars_to_decode;
remaining -= chars_to_decode;
}
return !out.empty();
}
// Colors
float gamma_correct(float value, float gamma) {

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