Compare commits

..

116 Commits

Author SHA1 Message Date
J. Nick Koston
ff01b97e9d Merge branch 'dev' into ezo_pmp 2026-01-18 22:29:09 -10:00
J. Nick Koston
f8bd4ef57d [template][event] Use StringRef for set_action and on_event triggers (#13328) 2026-01-18 22:22:57 -10:00
J. Nick Koston
bfcc0e26a3 [dfrobot_sen0395][pipsolar][sim800l][wl_134] Replace sprintf with snprintf/buf_append_printf (#13301)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-18 22:22:44 -10:00
J. Nick Koston
86a1b4cf69 [select][fan] Use StringRef for on_value/on_preset_set triggers to avoid heap allocation (#13324) 2026-01-18 19:51:11 -10:00
J. Nick Koston
d8a28f6fba [scheduler] Replace resize() with erase() to save ~ 436 bytes flash (#13214) 2026-01-18 18:54:30 -10:00
J. Nick Koston
e80a940222 [gdk101] Use stack buffer to eliminate heap allocation for firmware version (#13224) 2026-01-18 18:52:49 -10:00
J. Nick Koston
e99dbe05f7 [toshiba] Replace to_string with stack buffer in debug logging (#13296) 2026-01-18 18:52:34 -10:00
J. Nick Koston
f453a8d9a1 [dfrobot_sen0395] Reduce heap allocations in command building (#13219) 2026-01-18 18:44:56 -10:00
J. Nick Koston
126190d26a [ezo] Replace str_sprintf with stack-based formatting (#13218)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-18 18:44:41 -10:00
J. Nick Koston
e40201a98d [cse7766] Use stack buffer for verbose debug logging (#13217) 2026-01-18 18:44:27 -10:00
J. Nick Koston
8142f5db44 [zephyr] Avoid heap allocation in preferences key formatting (#13215) 2026-01-18 18:43:50 -10:00
J. Nick Koston
98ccab87a7 [tormatic] Use stack buffers instead of str_sprintf in debug methods (#13225) 2026-01-18 18:43:36 -10:00
J. Nick Koston
b9e72a8774 [daikin_arc] Fix undefined behavior in sprintf calls (#13279)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-18 18:43:19 -10:00
J. Nick Koston
d9fc625c6a [web_server] Simplify datetime formatting with buf_append_printf (#13281) 2026-01-18 18:43:05 -10:00
J. Nick Koston
dfbf79d6d6 [homeassistant] Use buf_append_printf for ESP8266 flash optimization (#13284) 2026-01-18 18:42:19 -10:00
J. Nick Koston
ea0fac96cb [core][mqtt] Add str_sanitize_to(), soft-deprecate str_sanitize() (#13233) 2026-01-18 18:42:04 -10:00
J. Nick Koston
3182222d60 [esp32_hosted] Use stack buffer instead of str_sprintf for version string (#13226) 2026-01-18 18:41:47 -10:00
J. Nick Koston
d8849b16f2 [gpio] Use buf_append_printf in dump_summary for ESP8266 flash optimization (#13283) 2026-01-18 18:41:34 -10:00
J. Nick Koston
635983f163 [uptime] Use buf_append_printf for ESP8266 flash optimization (#13282) 2026-01-18 18:41:19 -10:00
J. Nick Koston
6cbe672004 [tuya] Use buf_append_printf for ESP8266 flash optimization (#13287) 2026-01-18 18:41:07 -10:00
J. Nick Koston
226867b05c [esp8266] Use direct SDK calls instead of Arduino ESP class wrappers (#13353) 2026-01-18 18:40:53 -10:00
J. Nick Koston
67871a1683 [ccs811] Use buf_append_printf for buffer safety and ESP8266 flash optimization (#13300) 2026-01-18 18:40:14 -10:00
J. Nick Koston
f60c03e350 [syslog] Use buf_append_printf for ESP8266 flash optimization (#13286) 2026-01-18 18:39:53 -10:00
J. Nick Koston
eb66429144 [sml] Use stack buffers instead of str_sprintf (#13222)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-18 18:39:23 -10:00
J. Nick Koston
0f3bac5dd6 [nextion] Replace to_string with stack buffer and fix unsafe sprintf (#13295) 2026-01-18 18:37:29 -10:00
J. Nick Koston
5b92d0b89e [wiegand] Replace heap-allocating to_string with stack buffers (#13294) 2026-01-18 18:37:14 -10:00
J. Nick Koston
052b05df56 [tuya] Replace unsafe sprintf with snprintf in light color formatting (#13292) 2026-01-18 18:37:02 -10:00
J. Nick Koston
7b0db659d1 [rc522_spi] Replace unsafe sprintf with buf_append_printf (#13291) 2026-01-18 18:36:46 -10:00
J. Nick Koston
2f7270cf8f [uart] Replace unsafe sprintf with buf_append_printf in debugger (#13288) 2026-01-18 18:36:32 -10:00
J. Nick Koston
b44727aee6 [socket] Eliminate heap allocations in set_sockaddr() (#13228) 2026-01-18 18:29:31 -10:00
J. Nick Koston
1a55254258 [status] Convert to PollingComponent to reduce CPU usage (#13342) 2026-01-18 18:28:24 -10:00
J. Nick Koston
baf2b0e3c9 [api] Fix truncation of Home Assistant attributes longer than 255 characters (#13348) 2026-01-18 18:23:11 -10:00
J. Nick Koston
680e92a226 [core] Add str_endswith_ignore_case to avoid heap allocation in audio file type detection (#13313) 2026-01-18 08:36:56 -10:00
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
J. Nick Koston
687b8d1d95 fix security issue 2026-01-16 14:32:18 -10: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
J. Nick Koston
0390c3a8a6 [ezo_pmp] Replace sprintf with bounds-checked snprintf 2026-01-16 14:09:47 -10: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
191 changed files with 2869 additions and 839 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

@@ -69,6 +69,7 @@ from esphome.cpp_types import ( # noqa: F401
JsonObjectConst,
Parented,
PollingComponent,
StringRef,
arduino_json_ns,
bool_,
const_char_ptr,

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

@@ -1712,17 +1712,16 @@ void APIConnection::on_home_assistant_state_response(const HomeAssistantStateRes
}
// Create null-terminated state for callback (parse_number needs null-termination)
// HA state max length is 255, so 256 byte buffer covers all cases
char state_buf[256];
size_t copy_len = msg.state.size();
if (copy_len >= sizeof(state_buf)) {
copy_len = sizeof(state_buf) - 1; // Truncate to leave space for null terminator
// HA state max length is 255 characters, but attributes can be much longer
// Use stack buffer for common case (states), heap fallback for large attributes
size_t state_len = msg.state.size();
SmallBufferWithHeapFallback<256> state_buf_alloc(state_len + 1);
char *state_buf = reinterpret_cast<char *>(state_buf_alloc.get());
if (state_len > 0) {
memcpy(state_buf, msg.state.c_str(), state_len);
}
if (copy_len > 0) {
memcpy(state_buf, msg.state.c_str(), copy_len);
}
state_buf[copy_len] = '\0';
it.callback(StringRef(state_buf, copy_len));
state_buf[state_len] = '\0';
it.callback(StringRef(state_buf, state_len));
}
}
#endif

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

@@ -185,18 +185,16 @@ esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) {
return err;
}
std::string url_string = str_lower_case(url);
if (str_endswith(url_string, ".wav")) {
if (str_endswith_ignore_case(url, ".wav")) {
file_type = AudioFileType::WAV;
}
#ifdef USE_AUDIO_MP3_SUPPORT
else if (str_endswith(url_string, ".mp3")) {
else if (str_endswith_ignore_case(url, ".mp3")) {
file_type = AudioFileType::MP3;
}
#endif
#ifdef USE_AUDIO_FLAC_SUPPORT
else if (str_endswith(url_string, ".flac")) {
else if (str_endswith_ignore_case(url, ".flac")) {
file_type = AudioFileType::FLAC;
}
#endif

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

@@ -81,8 +81,8 @@ void CCS811Component::setup() {
bootloader_version, application_version);
if (this->version_ != nullptr) {
char version[20]; // "15.15.15 (0xffff)" is 17 chars, plus NUL, plus wiggle room
sprintf(version, "%d.%d.%d (0x%02x)", (application_version >> 12 & 15), (application_version >> 8 & 15),
(application_version >> 4 & 15), application_version);
buf_append_printf(version, sizeof(version), 0, "%d.%d.%d (0x%02x)", (application_version >> 12 & 15),
(application_version >> 8 & 15), (application_version >> 4 & 15), application_version);
ESP_LOGD(TAG, "publishing version state: %s", version);
this->version_->publish_state(version);
}

View File

@@ -133,7 +133,7 @@ bool CH422GGPIOPin::digital_read() { return this->parent_->digital_read(this->pi
void CH422GGPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value ^ this->inverted_); }
size_t CH422GGPIOPin::dump_summary(char *buffer, size_t len) const {
return snprintf(buffer, len, "EXIO%u via CH422G", this->pin_);
return buf_append_printf(buffer, len, 0, "EXIO%u via CH422G", this->pin_);
}
void CH422GGPIOPin::set_flags(gpio::Flags flags) {
flags_ = flags;

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

@@ -207,20 +207,24 @@ void CSE7766Component::parse_data_() {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
{
std::string buf = "Parsed:";
// Buffer: 7 + 15 + 33 + 15 + 25 = 95 chars max + null, rounded to 128 for safety margin.
// Float sizes with %.4f can be up to 11 chars for large values (e.g., 999999.9999).
char buf[128];
size_t pos = buf_append_printf(buf, sizeof(buf), 0, "Parsed:");
if (have_voltage) {
buf += str_sprintf(" V=%fV", voltage);
pos = buf_append_printf(buf, sizeof(buf), pos, " V=%.4fV", voltage);
}
if (have_current) {
buf += str_sprintf(" I=%fmA (~%fmA)", current * 1000.0f, calculated_current * 1000.0f);
pos = buf_append_printf(buf, sizeof(buf), pos, " I=%.4fmA (~%.4fmA)", current * 1000.0f,
calculated_current * 1000.0f);
}
if (have_power) {
buf += str_sprintf(" P=%fW", power);
pos = buf_append_printf(buf, sizeof(buf), pos, " P=%.4fW", power);
}
if (energy != 0.0f) {
buf += str_sprintf(" E=%fkWh (%u)", energy, cf_pulses);
buf_append_printf(buf, sizeof(buf), pos, " E=%.4fkWh (%u)", energy, cf_pulses);
}
ESP_LOGVV(TAG, "%s", buf.c_str());
ESP_LOGVV(TAG, "%s", buf);
}
#endif
}

View File

@@ -258,8 +258,9 @@ bool DaikinArcClimate::parse_state_frame_(const uint8_t frame[]) {
}
char buf[DAIKIN_STATE_FRAME_SIZE * 3 + 1] = {0};
size_t pos = 0;
for (size_t i = 0; i < DAIKIN_STATE_FRAME_SIZE; i++) {
sprintf(buf, "%s%02x ", buf, frame[i]);
pos = buf_append_printf(buf, sizeof(buf), pos, "%02x ", frame[i]);
}
ESP_LOGD(TAG, "FRAME %s", buf);
@@ -349,8 +350,9 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) {
if (data.expect_item(DAIKIN_HEADER_MARK, DAIKIN_HEADER_SPACE)) {
valid_daikin_frame = true;
size_t bytes_count = data.size() / 2 / 8;
std::unique_ptr<char[]> buf(new char[bytes_count * 3 + 1]);
buf[0] = '\0';
size_t buf_size = bytes_count * 3 + 1;
std::unique_ptr<char[]> buf(new char[buf_size]()); // value-initialize (zero-fill)
size_t buf_pos = 0;
for (size_t i = 0; i < bytes_count; i++) {
uint8_t byte = 0;
for (int8_t bit = 0; bit < 8; bit++) {
@@ -361,19 +363,19 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) {
break;
}
}
sprintf(buf.get(), "%s%02x ", buf.get(), byte);
buf_pos = buf_append_printf(buf.get(), buf_size, buf_pos, "%02x ", byte);
}
ESP_LOGD(TAG, "WHOLE FRAME %s size: %d", buf.get(), data.size());
}
if (!valid_daikin_frame) {
char sbuf[16 * 10 + 1];
sbuf[0] = '\0';
char sbuf[16 * 10 + 1] = {0};
size_t sbuf_pos = 0;
for (size_t j = 0; j < static_cast<size_t>(data.size()); j++) {
if ((j - 2) % 16 == 0) {
if (j > 0) {
ESP_LOGD(TAG, "DATA %04x: %s", (j - 16 > 0xffff ? 0 : j - 16), sbuf);
}
sbuf[0] = '\0';
sbuf_pos = 0;
}
char type_ch = ' ';
// debug_tolerance = 25%
@@ -401,9 +403,10 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) {
type_ch = '0';
if (abs(data[j]) > 100000) {
sprintf(sbuf, "%s%-5d[%c] ", sbuf, data[j] > 0 ? 99999 : -99999, type_ch);
sbuf_pos = buf_append_printf(sbuf, sizeof(sbuf), sbuf_pos, "%-5d[%c] ", data[j] > 0 ? 99999 : -99999, type_ch);
} else {
sprintf(sbuf, "%s%-5d[%c] ", sbuf, (int) (round(data[j] / 10.) * 10), type_ch);
sbuf_pos =
buf_append_printf(sbuf, sizeof(sbuf), sbuf_pos, "%-5d[%c] ", (int) (round(data[j] / 10.) * 10), type_ch);
}
if (j + 1 == static_cast<size_t>(data.size())) {
ESP_LOGD(TAG, "DATA %04x: %s", (j - 8 > 0xffff ? 0 : j - 8), sbuf);

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

@@ -127,7 +127,9 @@ DetRangeCfgCommand::DetRangeCfgCommand(float min1, float max1, float min2, float
this->min2_ = min2 = this->max2_ = max2 = this->min3_ = min3 = this->max3_ = max3 = this->min4_ = min4 =
this->max4_ = max4 = -1;
this->cmd_ = str_sprintf("detRangeCfg -1 %.0f %.0f", min1 / 0.15, max1 / 0.15);
char buf[72]; // max 72: "detRangeCfg -1 "(15) + 8 * (float(5) + space(1)) + null
snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f", min1 / 0.15, max1 / 0.15);
this->cmd_ = buf;
} else if (min3 < 0 || max3 < 0) {
this->min1_ = min1 = round(min1 / 0.15) * 0.15;
this->max1_ = max1 = round(max1 / 0.15) * 0.15;
@@ -135,7 +137,10 @@ DetRangeCfgCommand::DetRangeCfgCommand(float min1, float max1, float min2, float
this->max2_ = max2 = round(max2 / 0.15) * 0.15;
this->min3_ = min3 = this->max3_ = max3 = this->min4_ = min4 = this->max4_ = max4 = -1;
this->cmd_ = str_sprintf("detRangeCfg -1 %.0f %.0f %.0f %.0f", min1 / 0.15, max1 / 0.15, min2 / 0.15, max2 / 0.15);
char buf[72]; // max 72: "detRangeCfg -1 "(15) + 8 * (float(5) + space(1)) + null
snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f %.0f %.0f", min1 / 0.15, max1 / 0.15, min2 / 0.15,
max2 / 0.15);
this->cmd_ = buf;
} else if (min4 < 0 || max4 < 0) {
this->min1_ = min1 = round(min1 / 0.15) * 0.15;
this->max1_ = max1 = round(max1 / 0.15) * 0.15;
@@ -145,9 +150,10 @@ DetRangeCfgCommand::DetRangeCfgCommand(float min1, float max1, float min2, float
this->max3_ = max3 = round(max3 / 0.15) * 0.15;
this->min4_ = min4 = this->max4_ = max4 = -1;
this->cmd_ = str_sprintf("detRangeCfg -1 "
"%.0f %.0f %.0f %.0f %.0f %.0f",
min1 / 0.15, max1 / 0.15, min2 / 0.15, max2 / 0.15, min3 / 0.15, max3 / 0.15);
char buf[72]; // max 72: "detRangeCfg -1 "(15) + 8 * (float(5) + space(1)) + null
snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f %.0f %.0f %.0f %.0f", min1 / 0.15, max1 / 0.15, min2 / 0.15,
max2 / 0.15, min3 / 0.15, max3 / 0.15);
this->cmd_ = buf;
} else {
this->min1_ = min1 = round(min1 / 0.15) * 0.15;
this->max1_ = max1 = round(max1 / 0.15) * 0.15;
@@ -158,10 +164,10 @@ DetRangeCfgCommand::DetRangeCfgCommand(float min1, float max1, float min2, float
this->min4_ = min4 = round(min4 / 0.15) * 0.15;
this->max4_ = max4 = round(max4 / 0.15) * 0.15;
this->cmd_ = str_sprintf("detRangeCfg -1 "
"%.0f %.0f %.0f %.0f %.0f %.0f %.0f %.0f",
min1 / 0.15, max1 / 0.15, min2 / 0.15, max2 / 0.15, min3 / 0.15, max3 / 0.15, min4 / 0.15,
max4 / 0.15);
char buf[72]; // max 72: "detRangeCfg -1 "(15) + 8 * (float(5) + space(1)) + null
snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f %.0f %.0f %.0f %.0f %.0f %.0f", min1 / 0.15, max1 / 0.15,
min2 / 0.15, max2 / 0.15, min3 / 0.15, max3 / 0.15, min4 / 0.15, max4 / 0.15);
this->cmd_ = buf;
}
this->min1_ = min1;
@@ -203,7 +209,10 @@ SetLatencyCommand::SetLatencyCommand(float delay_after_detection, float delay_af
delay_after_disappear = std::round(delay_after_disappear / 0.025f) * 0.025f;
this->delay_after_detection_ = clamp(delay_after_detection, 0.0f, 1638.375f);
this->delay_after_disappear_ = clamp(delay_after_disappear, 0.0f, 1638.375f);
this->cmd_ = str_sprintf("setLatency %.03f %.03f", this->delay_after_detection_, this->delay_after_disappear_);
// max 32: "setLatency "(11) + float(8) + " "(1) + float(8) + null, rounded to 32
char buf[32];
snprintf(buf, sizeof(buf), "setLatency %.03f %.03f", this->delay_after_detection_, this->delay_after_disappear_);
this->cmd_ = buf;
};
uint8_t SetLatencyCommand::on_message(std::string &message) {

View File

@@ -75,8 +75,8 @@ class SetLatencyCommand : public Command {
class SensorCfgStartCommand : public Command {
public:
SensorCfgStartCommand(bool startup_mode) : startup_mode_(startup_mode) {
char tmp_cmd[20] = {0};
sprintf(tmp_cmd, "sensorCfgStart %d", startup_mode);
char tmp_cmd[20]; // "sensorCfgStart " (15) + "0/1" (1) + null = 17
buf_append_printf(tmp_cmd, sizeof(tmp_cmd), 0, "sensorCfgStart %d", startup_mode);
cmd_ = std::string(tmp_cmd);
}
uint8_t on_message(std::string &message) override;
@@ -142,8 +142,8 @@ class SensitivityCommand : public Command {
SensitivityCommand(uint8_t sensitivity) : sensitivity_(sensitivity) {
if (sensitivity > 9)
sensitivity_ = sensitivity = 9;
char tmp_cmd[20] = {0};
sprintf(tmp_cmd, "setSensitivity %d", sensitivity);
char tmp_cmd[20]; // "setSensitivity " (15) + "0-9" (1) + null = 17
buf_append_printf(tmp_cmd, sizeof(tmp_cmd), 0, "setSensitivity %d", sensitivity);
cmd_ = std::string(tmp_cmd);
};
uint8_t on_message(std::string &message) override;

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

@@ -69,7 +69,10 @@ void Esp32HostedUpdate::setup() {
// Get coprocessor version
esp_hosted_coprocessor_fwver_t ver_info;
if (esp_hosted_get_coprocessor_fwversion(&ver_info) == ESP_OK) {
this->update_info_.current_version = str_sprintf("%d.%d.%d", ver_info.major1, ver_info.minor1, ver_info.patch1);
// 16 bytes: "255.255.255" (11 chars) + null + safety margin
char buf[16];
snprintf(buf, sizeof(buf), "%d.%d.%d", ver_info.major1, ver_info.minor1, ver_info.patch1);
this->update_info_.current_version = buf;
} else {
this->update_info_.current_version = "unknown";
}
@@ -294,8 +297,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 +354,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

@@ -6,7 +6,11 @@
#include "esphome/core/helpers.h"
#include "preferences.h"
#include <Arduino.h>
#include <Esp.h>
#include <core_esp8266_features.h>
extern "C" {
#include <user_interface.h>
}
namespace esphome {
@@ -16,23 +20,19 @@ void IRAM_ATTR HOT delay(uint32_t ms) { ::delay(ms); }
uint32_t IRAM_ATTR HOT micros() { return ::micros(); }
void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); }
void arch_restart() {
ESP.restart(); // NOLINT(readability-static-accessed-through-instance)
system_restart();
// restart() doesn't always end execution
while (true) { // NOLINT(clang-diagnostic-unreachable-code)
yield();
}
}
void arch_init() {}
void IRAM_ATTR HOT arch_feed_wdt() {
ESP.wdtFeed(); // NOLINT(readability-static-accessed-through-instance)
}
void IRAM_ATTR HOT arch_feed_wdt() { system_soft_wdt_feed(); }
uint8_t progmem_read_byte(const uint8_t *addr) {
return pgm_read_byte(addr); // NOLINT
}
uint32_t IRAM_ATTR HOT arch_get_cpu_cycle_count() {
return ESP.getCycleCount(); // NOLINT(readability-static-accessed-through-instance)
}
uint32_t IRAM_ATTR HOT arch_get_cpu_cycle_count() { return esp_get_cycle_count(); }
uint32_t arch_get_cpu_freq_hz() { return F_CPU; }
void force_link_symbols() {

View File

@@ -99,7 +99,7 @@ void ESP8266GPIOPin::pin_mode(gpio::Flags flags) {
}
size_t ESP8266GPIOPin::dump_summary(char *buffer, size_t len) const {
return snprintf(buffer, len, "GPIO%u", this->pin_);
return buf_append_printf(buffer, len, 0, "GPIO%u", this->pin_);
}
bool ESP8266GPIOPin::digital_read() {

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

@@ -90,9 +90,7 @@ async def setup_event_core_(var, config, *, event_types: list[str]):
for conf in config.get(CONF_ON_EVENT, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger, [(cg.std_string, "event_type")], conf
)
await automation.build_automation(trigger, [(cg.StringRef, "event_type")], conf)
cg.add(var.set_event_types(event_types))

View File

@@ -14,10 +14,10 @@ template<typename... Ts> class TriggerEventAction : public Action<Ts...>, public
void play(const Ts &...x) override { this->parent_->trigger(this->event_type_.value(x...)); }
};
class EventTrigger : public Trigger<std::string> {
class EventTrigger : public Trigger<StringRef> {
public:
EventTrigger(Event *event) {
event->add_on_event_callback([this](const std::string &event_type) { this->trigger(event_type); });
event->add_on_event_callback([this](StringRef event_type) { this->trigger(event_type); });
}
};

View File

@@ -22,8 +22,8 @@ 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_);
this->event_callback_.call(event_type);
ESP_LOGD(TAG, "'%s' >> '%s'", this->get_name().c_str(), this->last_event_type_);
this->event_callback_.call(StringRef(found));
#if defined(USE_EVENT) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_event(this);
#endif
@@ -45,7 +45,7 @@ void Event::set_event_types(const std::vector<const char *> &event_types) {
this->last_event_type_ = nullptr; // Reset when types change
}
void Event::add_on_event_callback(std::function<void(const std::string &event_type)> &&callback) {
void Event::add_on_event_callback(std::function<void(StringRef event_type)> &&callback) {
this->event_callback_.add(std::move(callback));
}

View File

@@ -70,10 +70,10 @@ class Event : public EntityBase, public EntityBase_DeviceClass {
/// Check if an event has been triggered.
bool has_event() const { return this->last_event_type_ != nullptr; }
void add_on_event_callback(std::function<void(const std::string &event_type)> &&callback);
void add_on_event_callback(std::function<void(StringRef event_type)> &&callback);
protected:
LazyCallbackManager<void(const std::string &event_type)> event_callback_;
LazyCallbackManager<void(StringRef event_type)> event_callback_;
FixedVector<const char *> types_;
private:

View File

@@ -160,7 +160,7 @@ void EZOSensor::loop() {
this->commands_.pop_front();
}
void EZOSensor::add_command_(const std::string &command, EzoCommandType command_type, uint16_t delay_ms) {
void EZOSensor::add_command_(const char *command, EzoCommandType command_type, uint16_t delay_ms) {
std::unique_ptr<EzoCommand> ezo_command(new EzoCommand);
ezo_command->command = command;
ezo_command->command_type = command_type;
@@ -169,13 +169,17 @@ void EZOSensor::add_command_(const std::string &command, EzoCommandType command_
}
void EZOSensor::set_calibration_point_(EzoCalibrationType type, float value) {
std::string payload = str_sprintf("Cal,%s,%0.2f", EZO_CALIBRATION_TYPE_STRINGS[type], value);
// max 21: "Cal,"(4) + type(4) + ","(1) + float(11) + null; use 24 for safety
char payload[24];
snprintf(payload, sizeof(payload), "Cal,%s,%0.2f", EZO_CALIBRATION_TYPE_STRINGS[type], value);
this->add_command_(payload, EzoCommandType::EZO_CALIBRATION, 900);
}
void EZOSensor::set_address(uint8_t address) {
if (address > 0 && address < 128) {
std::string payload = str_sprintf("I2C,%u", address);
// max 8: "I2C,"(4) + uint8(3) + null
char payload[8];
snprintf(payload, sizeof(payload), "I2C,%u", address);
this->new_address_ = address;
this->add_command_(payload, EzoCommandType::EZO_I2C);
} else {
@@ -194,7 +198,9 @@ void EZOSensor::get_slope() { this->add_command_("Slope,?", EzoCommandType::EZO_
void EZOSensor::get_t() { this->add_command_("T,?", EzoCommandType::EZO_T); }
void EZOSensor::set_t(float value) {
std::string payload = str_sprintf("T,%0.2f", value);
// max 14 bytes: "T,"(2) + float with "%0.2f" (up to 11 chars) + null(1); use 16 for alignment
char payload[16];
snprintf(payload, sizeof(payload), "T,%0.2f", value);
this->add_command_(payload, EzoCommandType::EZO_T);
}
@@ -215,7 +221,9 @@ void EZOSensor::set_calibration_point_high(float value) {
}
void EZOSensor::set_calibration_generic(float value) {
std::string payload = str_sprintf("Cal,%0.2f", value);
// exact 16 bytes: "Cal," (4) + float with "%0.2f" (up to 11 chars, e.g. "-9999999.99") + null (1) = 16
char payload[16];
snprintf(payload, sizeof(payload), "Cal,%0.2f", value);
this->add_command_(payload, EzoCommandType::EZO_CALIBRATION, 900);
}
@@ -223,13 +231,11 @@ void EZOSensor::clear_calibration() { this->add_command_("Cal,clear", EzoCommand
void EZOSensor::get_led_state() { this->add_command_("L,?", EzoCommandType::EZO_LED); }
void EZOSensor::set_led_state(bool on) {
std::string to_send = "L,";
to_send += on ? "1" : "0";
this->add_command_(to_send, EzoCommandType::EZO_LED);
}
void EZOSensor::set_led_state(bool on) { this->add_command_(on ? "L,1" : "L,0", EzoCommandType::EZO_LED); }
void EZOSensor::send_custom(const std::string &to_send) { this->add_command_(to_send, EzoCommandType::EZO_CUSTOM); }
void EZOSensor::send_custom(const std::string &to_send) {
this->add_command_(to_send.c_str(), EzoCommandType::EZO_CUSTOM);
}
} // namespace ezo
} // namespace esphome

View File

@@ -92,7 +92,7 @@ class EZOSensor : public sensor::Sensor, public PollingComponent, public i2c::I2
std::deque<std::unique_ptr<EzoCommand>> commands_;
int new_address_;
void add_command_(const std::string &command, EzoCommandType command_type, uint16_t delay_ms = 300);
void add_command_(const char *command, EzoCommandType command_type, uint16_t delay_ms = 300);
void set_calibration_point_(EzoCalibrationType type, float value);

View File

@@ -318,90 +318,93 @@ void EzoPMP::send_next_command_() {
switch (this->next_command_) {
// Read Commands
case EZO_PMP_COMMAND_READ_DOSING: // Page 54
command_buffer_length = sprintf((char *) command_buffer, "D,?");
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "D,?");
break;
case EZO_PMP_COMMAND_READ_SINGLE_REPORT: // Single Report (page 53)
command_buffer_length = sprintf((char *) command_buffer, "R");
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "R");
break;
case EZO_PMP_COMMAND_READ_MAX_FLOW_RATE:
command_buffer_length = sprintf((char *) command_buffer, "DC,?");
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "DC,?");
break;
case EZO_PMP_COMMAND_READ_PAUSE_STATUS:
command_buffer_length = sprintf((char *) command_buffer, "P,?");
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "P,?");
break;
case EZO_PMP_COMMAND_READ_TOTAL_VOLUME_DOSED:
command_buffer_length = sprintf((char *) command_buffer, "TV,?");
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "TV,?");
break;
case EZO_PMP_COMMAND_READ_ABSOLUTE_TOTAL_VOLUME_DOSED:
command_buffer_length = sprintf((char *) command_buffer, "ATV,?");
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "ATV,?");
break;
case EZO_PMP_COMMAND_READ_CALIBRATION_STATUS:
command_buffer_length = sprintf((char *) command_buffer, "Cal,?");
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "Cal,?");
break;
case EZO_PMP_COMMAND_READ_PUMP_VOLTAGE:
command_buffer_length = sprintf((char *) command_buffer, "PV,?");
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "PV,?");
break;
// Non-Read Commands
case EZO_PMP_COMMAND_FIND: // Find (page 52)
command_buffer_length = sprintf((char *) command_buffer, "Find");
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "Find");
wait_time_for_command = 60000; // This command will block all updates for a minute
break;
case EZO_PMP_COMMAND_DOSE_CONTINUOUSLY: // Continuous Dispensing (page 54)
command_buffer_length = sprintf((char *) command_buffer, "D,*");
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "D,*");
break;
case EZO_PMP_COMMAND_CLEAR_TOTAL_VOLUME_DOSED: // Clear Total Volume Dosed (page 64)
command_buffer_length = sprintf((char *) command_buffer, "Clear");
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "Clear");
break;
case EZO_PMP_COMMAND_CLEAR_CALIBRATION: // Clear Calibration (page 65)
command_buffer_length = sprintf((char *) command_buffer, "Cal,clear");
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "Cal,clear");
break;
case EZO_PMP_COMMAND_PAUSE_DOSING: // Pause (page 61)
command_buffer_length = sprintf((char *) command_buffer, "P");
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "P");
break;
case EZO_PMP_COMMAND_STOP_DOSING: // Stop (page 62)
command_buffer_length = sprintf((char *) command_buffer, "X");
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "X");
break;
// Non-Read commands with parameters
case EZO_PMP_COMMAND_DOSE_VOLUME: // Volume Dispensing (page 55)
command_buffer_length = sprintf((char *) command_buffer, "D,%0.1f", this->next_command_volume_);
command_buffer_length =
snprintf((char *) command_buffer, sizeof(command_buffer), "D,%0.1f", this->next_command_volume_);
break;
case EZO_PMP_COMMAND_DOSE_VOLUME_OVER_TIME: // Dose over time (page 56)
command_buffer_length =
sprintf((char *) command_buffer, "D,%0.1f,%i", this->next_command_volume_, this->next_command_duration_);
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "D,%0.1f,%i",
this->next_command_volume_, this->next_command_duration_);
break;
case EZO_PMP_COMMAND_DOSE_WITH_CONSTANT_FLOW_RATE: // Constant Flow Rate (page 57)
command_buffer_length =
sprintf((char *) command_buffer, "DC,%0.1f,%i", this->next_command_volume_, this->next_command_duration_);
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "DC,%0.1f,%i",
this->next_command_volume_, this->next_command_duration_);
break;
case EZO_PMP_COMMAND_SET_CALIBRATION_VOLUME: // Set Calibration Volume (page 65)
command_buffer_length = sprintf((char *) command_buffer, "Cal,%0.2f", this->next_command_volume_);
command_buffer_length =
snprintf((char *) command_buffer, sizeof(command_buffer), "Cal,%0.2f", this->next_command_volume_);
break;
case EZO_PMP_COMMAND_CHANGE_I2C_ADDRESS: // Change I2C Address (page 73)
command_buffer_length = sprintf((char *) command_buffer, "I2C,%i", this->next_command_duration_);
command_buffer_length =
snprintf((char *) command_buffer, sizeof(command_buffer), "I2C,%i", this->next_command_duration_);
break;
case EZO_PMP_COMMAND_EXEC_ARBITRARY_COMMAND_ADDRESS: // Run an arbitrary command
command_buffer_length = sprintf((char *) command_buffer, this->arbitrary_command_, this->next_command_duration_);
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "%s", this->arbitrary_command_);
ESP_LOGI(TAG, "Sending arbitrary command: %s", (char *) command_buffer);
break;

View File

@@ -77,7 +77,7 @@ FanSpeedSetTrigger = fan_ns.class_(
"FanSpeedSetTrigger", automation.Trigger.template(cg.int_)
)
FanPresetSetTrigger = fan_ns.class_(
"FanPresetSetTrigger", automation.Trigger.template(cg.std_string)
"FanPresetSetTrigger", automation.Trigger.template(cg.StringRef)
)
FanIsOnCondition = fan_ns.class_("FanIsOnCondition", automation.Condition.template())
@@ -287,7 +287,7 @@ async def setup_fan_core_(var, config):
await automation.build_automation(trigger, [(cg.int_, "x")], conf)
for conf in config.get(CONF_ON_PRESET_SET, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(cg.std_string, "x")], conf)
await automation.build_automation(trigger, [(cg.StringRef, "x")], conf)
async def register_fan(var, config):

View File

@@ -208,7 +208,7 @@ class FanSpeedSetTrigger : public Trigger<int> {
int last_speed_;
};
class FanPresetSetTrigger : public Trigger<std::string> {
class FanPresetSetTrigger : public Trigger<StringRef> {
public:
FanPresetSetTrigger(Fan *state) {
state->add_on_state_callback([this, state]() {
@@ -216,7 +216,7 @@ class FanPresetSetTrigger : public Trigger<std::string> {
auto should_trigger = preset_mode != this->last_preset_mode_;
this->last_preset_mode_ = preset_mode;
if (should_trigger) {
this->trigger(std::string(preset_mode));
this->trigger(preset_mode);
}
});
this->last_preset_mode_ = state->get_preset_mode();

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

@@ -163,9 +163,10 @@ bool GDK101Component::read_fw_version_(uint8_t *data) {
return false;
}
const std::string fw_version_str = str_sprintf("%d.%d", data[0], data[1]);
this->fw_version_text_sensor_->publish_state(fw_version_str);
// max 8: "255.255" (7 chars) + null
char buf[8];
snprintf(buf, sizeof(buf), "%d.%d", data[0], data[1]);
this->fw_version_text_sensor_->publish_state(buf);
}
#endif // USE_TEXT_SENSOR
return true;

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

@@ -97,7 +97,7 @@ void HomeassistantNumber::control(float value) {
entity_value.key = VALUE_KEY;
// Stack buffer - no heap allocation; %g produces shortest representation
char value_buf[16];
snprintf(value_buf, sizeof(value_buf), "%g", value);
buf_append_printf(value_buf, sizeof(value_buf), 0, "%g", value);
entity_value.value = StringRef(value_buf);
api::global_api_server->send_homeassistant_action(resp);

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

@@ -42,8 +42,8 @@ ErrorCode I2CDevice::read_register16(uint16_t a_register, uint8_t *data, size_t
}
ErrorCode I2CDevice::write_register(uint8_t a_register, const uint8_t *data, size_t len) const {
SmallBufferWithHeapFallback<17> buffer_alloc; // Most I2C writes are <= 16 bytes
uint8_t *buffer = buffer_alloc.get(len + 1);
SmallBufferWithHeapFallback<17> buffer_alloc(len + 1); // Most I2C writes are <= 16 bytes
uint8_t *buffer = buffer_alloc.get();
buffer[0] = a_register;
std::copy(data, data + len, buffer + 1);
@@ -51,8 +51,8 @@ ErrorCode I2CDevice::write_register(uint8_t a_register, const uint8_t *data, siz
}
ErrorCode I2CDevice::write_register16(uint16_t a_register, const uint8_t *data, size_t len) const {
SmallBufferWithHeapFallback<18> buffer_alloc; // Most I2C writes are <= 16 bytes + 2 for register
uint8_t *buffer = buffer_alloc.get(len + 2);
SmallBufferWithHeapFallback<18> buffer_alloc(len + 2); // Most I2C writes are <= 16 bytes + 2 for register
uint8_t *buffer = buffer_alloc.get();
buffer[0] = a_register >> 8;
buffer[1] = a_register;

View File

@@ -11,22 +11,6 @@
namespace esphome {
namespace i2c {
/// @brief Helper class for efficient buffer allocation - uses stack for small sizes, heap for large
template<size_t STACK_SIZE> class SmallBufferWithHeapFallback {
public:
uint8_t *get(size_t size) {
if (size <= STACK_SIZE) {
return this->stack_buffer_;
}
this->heap_buffer_ = std::unique_ptr<uint8_t[]>(new uint8_t[size]);
return this->heap_buffer_.get();
}
private:
uint8_t stack_buffer_[STACK_SIZE];
std::unique_ptr<uint8_t[]> heap_buffer_;
};
/// @brief Error codes returned by I2CBus and I2CDevice methods
enum ErrorCode {
NO_ERROR = 0, ///< No error found during execution of method
@@ -92,8 +76,8 @@ class I2CBus {
total_len += read_buffers[i].len;
}
SmallBufferWithHeapFallback<128> buffer_alloc; // Most I2C reads are small
uint8_t *buffer = buffer_alloc.get(total_len);
SmallBufferWithHeapFallback<128> buffer_alloc(total_len); // Most I2C reads are small
uint8_t *buffer = buffer_alloc.get();
auto err = this->write_readv(address, nullptr, 0, buffer, total_len);
if (err != ERROR_OK)
@@ -116,8 +100,8 @@ class I2CBus {
total_len += write_buffers[i].len;
}
SmallBufferWithHeapFallback<128> buffer_alloc; // Most I2C writes are small
uint8_t *buffer = buffer_alloc.get(total_len);
SmallBufferWithHeapFallback<128> buffer_alloc(total_len); // Most I2C writes are small
uint8_t *buffer = buffer_alloc.get();
size_t pos = 0;
for (size_t i = 0; i != count; i++) {

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

@@ -162,7 +162,7 @@ void MAX6956GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this-
bool MAX6956GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; }
void MAX6956GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); }
size_t MAX6956GPIOPin::dump_summary(char *buffer, size_t len) const {
return snprintf(buffer, len, "%u via Max6956", this->pin_);
return buf_append_printf(buffer, len, 0, "%u via Max6956", this->pin_);
}
} // namespace max6956

View File

@@ -100,7 +100,7 @@ void MCP23016GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this
bool MCP23016GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; }
void MCP23016GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); }
size_t MCP23016GPIOPin::dump_summary(char *buffer, size_t len) const {
return snprintf(buffer, len, "%u via MCP23016", this->pin_);
return buf_append_printf(buffer, len, 0, "%u via MCP23016", this->pin_);
}
} // namespace mcp23016

View File

@@ -17,7 +17,7 @@ template<uint8_t N> void MCP23XXXGPIOPin<N>::digital_write(bool value) {
this->parent_->digital_write(this->pin_, value != this->inverted_);
}
template<uint8_t N> size_t MCP23XXXGPIOPin<N>::dump_summary(char *buffer, size_t len) const {
return snprintf(buffer, len, "%u via MCP23XXX", this->pin_);
return buf_append_printf(buffer, len, 0, "%u via MCP23XXX", this->pin_);
}
template class MCP23XXXGPIOPin<8>;

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

@@ -154,7 +154,7 @@ void MPR121GPIOPin::digital_write(bool value) {
}
size_t MPR121GPIOPin::dump_summary(char *buffer, size_t len) const {
return snprintf(buffer, len, "ELE%u on MPR121", this->pin_);
return buf_append_printf(buffer, len, 0, "ELE%u on MPR121", this->pin_);
}
} // namespace mpr121

View File

@@ -635,7 +635,8 @@ void MQTTClientComponent::set_log_message_template(MQTTMessage &&message) { this
const MQTTDiscoveryInfo &MQTTClientComponent::get_discovery_info() const { return this->discovery_info_; }
void MQTTClientComponent::set_topic_prefix(const std::string &topic_prefix, const std::string &check_topic_prefix) {
if (App.is_name_add_mac_suffix_enabled() && (topic_prefix == check_topic_prefix)) {
this->topic_prefix_ = str_sanitize(App.get_name());
char buf[ESPHOME_DEVICE_NAME_MAX_LEN + 1];
this->topic_prefix_ = str_sanitize_to(buf, App.get_name().c_str());
} else {
this->topic_prefix_ = topic_prefix;
}

View File

@@ -48,7 +48,8 @@ void MQTTComponent::set_subscribe_qos(uint8_t qos) { this->subscribe_qos_ = qos;
void MQTTComponent::set_retain(bool retain) { this->retain_ = retain; }
std::string MQTTComponent::get_discovery_topic_(const MQTTDiscoveryInfo &discovery_info) const {
std::string sanitized_name = str_sanitize(App.get_name());
char sanitized_name[ESPHOME_DEVICE_NAME_MAX_LEN + 1];
str_sanitize_to(sanitized_name, App.get_name().c_str());
const char *comp_type = this->component_type();
char object_id_buf[OBJECT_ID_MAX_LEN];
StringRef object_id = this->get_default_object_id_to_(object_id_buf);
@@ -60,7 +61,7 @@ std::string MQTTComponent::get_discovery_topic_(const MQTTDiscoveryInfo &discove
p = append_char(p, '/');
p = append_str(p, comp_type, strlen(comp_type));
p = append_char(p, '/');
p = append_str(p, sanitized_name.data(), sanitized_name.size());
p = append_str(p, sanitized_name, strlen(sanitized_name));
p = append_char(p, '/');
p = append_str(p, object_id.c_str(), object_id.size());
p = append_str(p, "/config", 7);
@@ -189,8 +190,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

@@ -1,6 +1,7 @@
#include "nextion.h"
#include <cinttypes>
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/util.h"
@@ -1283,8 +1284,9 @@ void Nextion::check_pending_waveform_() {
size_t buffer_to_send = component->get_wave_buffer_size() < 255 ? component->get_wave_buffer_size()
: 255; // ADDT command can only send 255
std::string command = "addt " + to_string(component->get_component_id()) + "," +
to_string(component->get_wave_channel_id()) + "," + to_string(buffer_to_send);
char command[24]; // "addt " + uint8 + "," + uint8 + "," + uint8 + null = max 17 chars
buf_append_printf(command, sizeof(command), 0, "addt %u,%u,%zu", component->get_component_id(),
component->get_wave_channel_id(), buffer_to_send);
if (!this->send_command_(command)) {
delete nb; // NOLINT(cppcoreguidelines-owning-memory)
this->waveform_queue_.pop_front();

View File

@@ -34,7 +34,7 @@ int Nextion::upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start) {
}
char range_header[32];
sprintf(range_header, "bytes=%" PRIu32 "-%" PRIu32, range_start, range_end);
buf_append_printf(range_header, sizeof(range_header), 0, "bytes=%" PRIu32 "-%" PRIu32, range_start, range_end);
ESP_LOGV(TAG, "Range: %s", range_header);
http_client.addHeader("Range", range_header);
int code = http_client.GET();

View File

@@ -36,7 +36,7 @@ int Nextion::upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &r
}
char range_header[32];
sprintf(range_header, "bytes=%" PRIu32 "-%" PRIu32, range_start, range_end);
buf_append_printf(range_header, sizeof(range_header), 0, "bytes=%" PRIu32 "-%" PRIu32, range_start, range_end);
ESP_LOGV(TAG, "Range: %s", range_header);
esp_http_client_set_header(http_client, "Range", range_header);
ESP_LOGV(TAG, "Open HTTP");

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

@@ -181,7 +181,7 @@ void PCA6416AGPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this
bool PCA6416AGPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; }
void PCA6416AGPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); }
size_t PCA6416AGPIOPin::dump_summary(char *buffer, size_t len) const {
return snprintf(buffer, len, "%u via PCA6416A", this->pin_);
return buf_append_printf(buffer, len, 0, "%u via PCA6416A", this->pin_);
}
} // namespace pca6416a

View File

@@ -130,7 +130,7 @@ void PCA9554GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this-
bool PCA9554GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; }
void PCA9554GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); }
size_t PCA9554GPIOPin::dump_summary(char *buffer, size_t len) const {
return snprintf(buffer, len, "%u via PCA9554", this->pin_);
return buf_append_printf(buffer, len, 0, "%u via PCA9554", this->pin_);
}
} // namespace pca9554

View File

@@ -107,7 +107,7 @@ void PCF8574GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this-
bool PCF8574GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; }
void PCF8574GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); }
size_t PCF8574GPIOPin::dump_summary(char *buffer, size_t len) const {
return snprintf(buffer, len, "%u via PCF8574", this->pin_);
return buf_append_printf(buffer, len, 0, "%u via PCF8574", this->pin_);
}
} // namespace pcf8574

View File

@@ -165,7 +165,7 @@ void PI4IOE5V6408GPIOPin::digital_write(bool value) {
this->parent_->digital_write(this->pin_, value != this->inverted_);
}
size_t PI4IOE5V6408GPIOPin::dump_summary(char *buffer, size_t len) const {
return snprintf(buffer, len, "%u via PI4IOE5V6408", this->pin_);
return buf_append_printf(buffer, len, 0, "%u via PI4IOE5V6408", this->pin_);
}
} // namespace pi4ioe5v6408

View File

@@ -8,8 +8,8 @@ namespace pipsolar {
static const char *const TAG = "pipsolar.output";
void PipsolarOutput::write_state(float state) {
char tmp[10];
sprintf(tmp, this->set_command_.c_str(), state);
char tmp[16];
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

@@ -1,4 +1,5 @@
#include "rc522_spi.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
// Based on:
@@ -70,7 +71,7 @@ void RC522Spi::pcd_read_register(PcdRegister reg, ///< The register to read fro
index++;
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
sprintf(cstrb, " %x", values[0]);
buf_append_printf(cstrb, sizeof(cstrb), 0, " %x", values[0]);
buf.append(cstrb);
#endif
}
@@ -78,7 +79,7 @@ void RC522Spi::pcd_read_register(PcdRegister reg, ///< The register to read fro
values[index] = transfer_byte(address); // Read value and tell that we want to read the same address again.
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
sprintf(cstrb, " %x", values[index]);
buf_append_printf(cstrb, sizeof(cstrb), 0, " %x", values[index]);
buf.append(cstrb);
#endif
@@ -88,7 +89,7 @@ void RC522Spi::pcd_read_register(PcdRegister reg, ///< The register to read fro
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
buf = buf + " ";
sprintf(cstrb, "%x", values[index]);
buf_append_printf(cstrb, sizeof(cstrb), 0, "%x", values[index]);
buf.append(cstrb);
ESP_LOGVV(TAG, "read_register_array_(%x, %d, , %d) -> %s", reg, count, rx_align, buf.c_str());
@@ -127,7 +128,7 @@ void RC522Spi::pcd_write_register(PcdRegister reg, ///< The register to write t
transfer_byte(values[index]);
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
sprintf(cstrb, " %x", values[index]);
buf_append_printf(cstrb, sizeof(cstrb), 0, " %x", values[index]);
buf.append(cstrb);
#endif
}

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
@@ -30,7 +33,7 @@ SelectPtr = Select.operator("ptr")
# Triggers
SelectStateTrigger = select_ns.class_(
"SelectStateTrigger",
automation.Trigger.template(cg.std_string, cg.size_t),
automation.Trigger.template(cg.StringRef, cg.size_t),
)
# Actions
@@ -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 = {
@@ -94,7 +100,7 @@ async def setup_select_core_(var, config, *, options: list[str]):
for conf in config.get(CONF_ON_VALUE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger, [(cg.std_string, "x"), (cg.size_t, "i")], conf
trigger, [(cg.StringRef, "x"), (cg.size_t, "i")], conf
)
if (mqtt_id := config.get(CONF_MQTT_ID)) is not None:
@@ -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,

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