Compare commits

...

61 Commits

Author SHA1 Message Date
J. Nick Koston
2bacca21c4 tweak 2026-01-22 22:23:10 -10:00
J. Nick Koston
425db688e5 [wifi] Avoid heap allocation when building AP SSID 2026-01-22 22:10:08 -10:00
J. Nick Koston
be5bfe24da [wifi] Avoid heap allocation when building AP SSID 2026-01-22 22:05:50 -10:00
Keith Burzinski
cfb61bc50a [ir_rf_proxy] Remove unnecessary headers, add tests (#13464) 2026-01-22 20:35:37 -06:00
Jonathan Swoboda
547c985672 Merge branch 'release' into dev 2026-01-22 18:19:32 -05:00
Jonathan Swoboda
44e624d7a7 Merge pull request #13459 from esphome/bump-2026.1.1
2026.1.1
2026-01-22 18:19:18 -05:00
J. Nick Koston
5779e3e6e4 [atm90e32] Fix dump_summary deprecation warning and remove stored cs_summary_ (#13465) 2026-01-22 12:54:01 -10:00
J. Nick Koston
3184717607 [rpi_dpi_rgb] Fix dump_summary deprecation warning (#13461) 2026-01-22 12:53:38 -10:00
J. Nick Koston
e8972c65c8 [mipi_rgb] Fix dump_summary deprecation warning (#13463) 2026-01-22 12:53:15 -10:00
J. Nick Koston
71cda05073 [st7701s] Fix dump_summary deprecation warning (#13462) 2026-01-22 12:42:28 -10:00
Clyde Stubbs
3dbebb728d [sensor] Clamp filter handles non-finite values better (#13457) 2026-01-22 22:34:29 +00:00
Jonathan Swoboda
f938de16af Bump version to 2026.1.1 2026-01-22 16:30:52 -05:00
J. Nick Koston
ec791063b3 [time] Always call time sync callbacks even when time unchanged (#13456) 2026-01-22 16:30:52 -05:00
Jonathan Swoboda
fb984cd052 [aqi] Remove unit_of_measurement to fix Home Assistant warning (#13448)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 16:30:52 -05:00
Jonathan Swoboda
85181779d1 [fingerprint_grow] Use buffer-based dump_summary to fix deprecation warnings (#13447)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 16:30:52 -05:00
J. Nick Koston
95b23702e4 [wifi] Fix stale error_from_callback_ causing immediate connection failures (#13450) 2026-01-22 16:30:52 -05:00
J. Nick Koston
95eebcd74f [api] Limit Nagle batching for log messages to reduce LWIP buffer pressure (#13439) 2026-01-22 16:30:52 -05:00
Rene Guca
3c3d5c2fca [dht] Increase delay for DHT22 and RHT03 (#13446) 2026-01-22 16:30:52 -05:00
J. Nick Koston
811ac81320 [http_request] Fix OTA failures on ESP8266/Arduino by making read semantics consistent (#13435)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-22 16:30:52 -05:00
J. Nick Koston
f01bd68a4b [spi] Fix display init failure by marking displays as write-only for half-duplex mode (#13431) 2026-01-22 16:30:52 -05:00
J. Nick Koston
5433c0f707 [wifi] Fix bk72xx manual_ip preventing API connection (#13426) 2026-01-22 16:30:52 -05:00
Jonathan Swoboda
b06cce9eeb [esp32] Add warning for experimental 400MHz on ESP32-P4 (#13433)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 16:30:52 -05:00
Jonathan Swoboda
65bcfee035 [http_request] Fix verify_ssl: false not working on ESP32 (#13422)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 16:30:52 -05:00
Copilot
9261b9ecaa [lvgl] Validate LVGL dropdown symbols require Unicode codepoint ≥ 0x100 (#13394)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-01-22 16:30:52 -05:00
J. Nick Koston
6725e6c01e [wifi] Process scan results one at a time to avoid heap allocation (#13400) 2026-01-22 16:30:52 -05:00
J. Nick Koston
effbcece49 [time] Always call time sync callbacks even when time unchanged (#13456) 2026-01-22 21:27:04 +00:00
Jonathan Swoboda
98a926f37f [heatpumpir] Fix ambiguous millis() call with HeatpumpIR library (#13458)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 16:22:33 -05:00
dependabot[bot]
110c173eac Update wheel requirement from <0.46,>=0.43 to >=0.43,<0.47 (#13451)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-22 11:16:53 -10:00
dependabot[bot]
6008abae62 Bump actions/setup-python from 6.1.0 to 6.2.0 in /.github/actions/restore-python (#13453)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-22 11:16:40 -10:00
dependabot[bot]
04e102f344 Bump actions/setup-python from 6.1.0 to 6.2.0 (#13454)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-22 11:16:27 -10:00
dependabot[bot]
bb67b1ca1e Bump actions/checkout from 6.0.1 to 6.0.2 (#13452)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-22 11:16:15 -10:00
esphomebot
6d7956a062 Update webserver local assets to 20260122-204614 (#13455)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-01-22 11:15:42 -10:00
Jonathan Swoboda
afbbdd1492 [aqi] Remove unit_of_measurement to fix Home Assistant warning (#13448)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 16:10:55 -05:00
Jonathan Swoboda
b06568c132 [fingerprint_grow] Use buffer-based dump_summary to fix deprecation warnings (#13447)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 16:07:41 -05:00
J. Nick Koston
3c5fc638d5 [wifi] Fix stale error_from_callback_ causing immediate connection failures (#13450) 2026-01-22 10:42:14 -10:00
J. Nick Koston
ddb762f8f5 [api] Limit Nagle batching for log messages to reduce LWIP buffer pressure (#13439) 2026-01-22 08:09:14 -10:00
H. Árkosi Róbert
4ac7fe84b4 [bthome_mithermometer] add encrypted beacon support (#13428) 2026-01-23 03:14:14 +11:00
Sven Kocksch
d6a41ed51e [mipi_dsi] Add M5Stack Tab5 (Rev2/V2) DriverChip (#12074)
Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
2026-01-23 02:31:38 +11:00
Rene Guca
8d1379a275 [dht] Increase delay for DHT22 and RHT03 (#13446) 2026-01-22 07:54:10 -05:00
J. Nick Koston
5bbf9153ca [http_request] Fix OTA failures on ESP8266/Arduino by making read semantics consistent (#13435)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-21 19:48:32 -10:00
J. Nick Koston
a1c4d56268 [alarm_control_panel] Reduce heap allocations in arm/disarm methods (#13358) 2026-01-21 18:37:13 -10:00
J. Nick Koston
a9ce3df04c [esp8266] Use SmallBufferWithHeapFallback in preferences (#13397) 2026-01-21 18:36:12 -10:00
J. Nick Koston
99aa83564e [mqtt] Reduce heap allocations in hot paths (#13362)
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-21 18:35:59 -10:00
J. Nick Koston
aa5092bdc2 [mqtt] Use stack buffers for discovery message formatting (#13216)
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-21 18:35:43 -10:00
Edward Firmo
645832a070 [nextion] Add configurable startup and queue timeout constants (#11098)
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@koston.org>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick+github@koston.org>
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
2026-01-21 20:10:12 -06:00
Jonathan Swoboda
19c1d3aee7 [esp32] Bump Arduino to 3.3.6, platform to 55.03.36 (#13438)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 20:41:59 -05:00
J. Nick Koston
ce5ec7a78f [spi] Fix display init failure by marking displays as write-only for half-duplex mode (#13431) 2026-01-21 14:04:07 -10:00
J. Nick Koston
ebf589560d [wifi] Fix bk72xx manual_ip preventing API connection (#13426) 2026-01-21 14:03:49 -10:00
Jonathan Swoboda
8dd1aec606 [esp32] Add warning for experimental 400MHz on ESP32-P4 (#13433)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 17:17:11 -05:00
Joakim Plate
9d967b01c8 Expose sockaddr to string formatter (#12351) 2026-01-21 10:32:39 -10:00
tomaszduda23
11e0d536e4 [debug] Print reg0 value from config if mismatched on nrf52 (#11867)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-01-21 20:15:51 +00:00
dependabot[bot]
673f46f761 Bump peter-evans/create-pull-request from 8.0.0 to 8.1.0 (#13430)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-21 09:37:18 -10:00
dependabot[bot]
4abae8d445 Bump setuptools from 80.9.0 to 80.10.1 (#13429)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-21 09:37:04 -10:00
Jonathan Swoboda
e62368e058 [heatpumpir] Add ESP-IDF support, bump to 1.0.40 (#13042) 2026-01-21 13:19:36 -05:00
Jonathan Swoboda
5345c96ff3 [http_request] Fix verify_ssl: false not working on ESP32 (#13422)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 13:18:37 -05:00
tomaszduda23
333ace25c9 [adc] Fix indent (#11933) 2026-01-21 12:41:56 -05:00
Dawid
6014bba3d1 [zephyr] Small build fixes for the logger/gpio subsystems (#13242)
Co-authored-by: dawret <dawret@dawret.me>
2026-01-21 12:37:10 -05:00
maikeljkwak
5f2394ef80 [hc8, mhz19] Moving constant CONF_WARMUP_TIME to const.py (#13392)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-01-21 12:34:52 -05:00
Copilot
29555c0ddc [lvgl] Validate LVGL dropdown symbols require Unicode codepoint ≥ 0x100 (#13394)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-01-21 12:32:55 -05:00
Kevin Ahrendt
37eaf10f75 [audio] Bump esp-audio-libs to 2.0.3 (#13346) 2026-01-21 07:40:41 -05:00
J. Nick Koston
0b60fd0c8c [core] Avoid heap allocation in str_equals_case_insensitive with string literals (#13312) 2026-01-20 21:49:14 -10:00
130 changed files with 10306 additions and 9463 deletions

View File

@@ -1 +1 @@
d15ae81646ac0ee76b2586716fe697f187281523ee6db566aed26542a9f98d1a
15dc295268b2dcf75942f42759b3ddec64eba89f75525698eb39c95a7f4b14ce

View File

@@ -17,7 +17,7 @@ runs:
steps:
- name: Set up Python ${{ inputs.python-version }}
id: python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ inputs.python-version }}
- name: Restore Python virtual environment

View File

@@ -22,7 +22,7 @@ jobs:
if: github.event.action != 'labeled' || github.event.sender.type != 'Bot'
steps:
- name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Generate a token
id: generate-token

View File

@@ -21,9 +21,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.11"

View File

@@ -21,10 +21,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.11"

View File

@@ -43,9 +43,9 @@ jobs:
- "docker"
# - "lint"
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.11"
- name: Set up Docker Buildx

View File

@@ -49,7 +49,7 @@ jobs:
- name: Check out code from base repository
if: steps.pr.outputs.skip != 'true'
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Always check out from the base repository (esphome/esphome), never from forks
# Use the PR's target branch to ensure we run trusted code from the main repo

View File

@@ -36,13 +36,13 @@ jobs:
cache-key: ${{ steps.cache-key.outputs.key }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Generate cache-key
id: cache-key
run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment
@@ -70,7 +70,7 @@ jobs:
if: needs.determine-jobs.outputs.python-linters == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -91,7 +91,7 @@ jobs:
- common
steps:
- name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -132,7 +132,7 @@ jobs:
- common
steps:
- name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Restore Python
id: restore-python
uses: ./.github/actions/restore-python
@@ -183,7 +183,7 @@ jobs:
component-test-batches: ${{ steps.determine.outputs.component-test-batches }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Fetch enough history to find the merge base
fetch-depth: 2
@@ -237,10 +237,10 @@ jobs:
if: needs.determine-jobs.outputs.integration-tests == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Python 3.13
id: python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.13"
- name: Restore Python virtual environment
@@ -273,7 +273,7 @@ jobs:
if: github.event_name == 'pull_request' && (needs.determine-jobs.outputs.cpp-unit-tests-run-all == 'true' || needs.determine-jobs.outputs.cpp-unit-tests-components != '[]')
steps:
- name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Restore Python
uses: ./.github/actions/restore-python
@@ -321,7 +321,7 @@ jobs:
steps:
- name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
@@ -400,7 +400,7 @@ jobs:
GH_TOKEN: ${{ github.token }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
@@ -489,7 +489,7 @@ jobs:
steps:
- name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
@@ -577,7 +577,7 @@ jobs:
version: 1.0
- name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -662,7 +662,7 @@ jobs:
if: github.event_name == 'pull_request' && !startsWith(github.base_ref, 'beta') && !startsWith(github.base_ref, 'release')
steps:
- name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -688,7 +688,7 @@ jobs:
skip: ${{ steps.check-script.outputs.skip }}
steps:
- name: Check out target branch
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.base_ref }}
@@ -840,7 +840,7 @@ jobs:
flash_usage: ${{ steps.extract.outputs.flash_usage }}
steps:
- name: Check out PR branch
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -908,7 +908,7 @@ jobs:
GH_TOKEN: ${{ github.token }}
steps:
- name: Check out code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:

View File

@@ -54,7 +54,7 @@ jobs:
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL

View File

@@ -20,7 +20,7 @@ jobs:
branch_build: ${{ steps.tag.outputs.branch_build }}
deploy_env: ${{ steps.tag.outputs.deploy_env }}
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Get tag
id: tag
# yamllint disable rule:line-length
@@ -60,9 +60,9 @@ jobs:
contents: read
id-token: write
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.x"
- name: Build
@@ -92,9 +92,9 @@ jobs:
os: "ubuntu-24.04-arm"
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.11"
@@ -168,7 +168,7 @@ jobs:
- ghcr
- dockerhub
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Download digests
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0

View File

@@ -13,16 +13,16 @@ jobs:
if: github.repository == 'esphome/esphome'
steps:
- name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Checkout Home Assistant
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
repository: home-assistant/core
path: lib/home-assistant
- name: Setup Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: 3.13
@@ -41,7 +41,7 @@ jobs:
python script/run-in-env.py pre-commit run --all-files
- name: Commit changes
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
commit-message: "Synchronise Device Classes from Home Assistant"
committer: esphomebot <esphome@openhomefoundation.org>

View File

@@ -160,21 +160,21 @@ async def to_code(config):
zephyr_add_user("io-channels", f"<&adc {channel_id}>")
zephyr_add_overlay(
f"""
&adc {{
#address-cells = <1>;
#size-cells = <0>;
&adc {{
#address-cells = <1>;
#size-cells = <0>;
channel@{channel_id} {{
reg = <{channel_id}>;
zephyr,gain = "{gain}";
zephyr,reference = "ADC_REF_INTERNAL";
zephyr,acquisition-time = <ADC_ACQ_TIME_DEFAULT>;
zephyr,input-positive = <NRF_SAADC_{pin_number}>;
zephyr,resolution = <14>;
zephyr,oversampling = <8>;
}};
}};
"""
channel@{channel_id} {{
reg = <{channel_id}>;
zephyr,gain = "{gain}";
zephyr,reference = "ADC_REF_INTERNAL";
zephyr,acquisition-time = <ADC_ACQ_TIME_DEFAULT>;
zephyr,input-positive = <NRF_SAADC_{pin_number}>;
zephyr,resolution = <14>;
zephyr,oversampling = <8>;
}};
}};
"""
)

View File

@@ -67,52 +67,29 @@ void AlarmControlPanel::add_on_ready_callback(std::function<void()> &&callback)
this->ready_callback_.add(std::move(callback));
}
void AlarmControlPanel::arm_away(optional<std::string> code) {
void AlarmControlPanel::arm_with_code_(AlarmControlPanelCall &(AlarmControlPanelCall::*arm_method)(),
const char *code) {
auto call = this->make_call();
call.arm_away();
if (code.has_value())
call.set_code(code.value());
(call.*arm_method)();
if (code != nullptr)
call.set_code(code);
call.perform();
}
void AlarmControlPanel::arm_home(optional<std::string> code) {
auto call = this->make_call();
call.arm_home();
if (code.has_value())
call.set_code(code.value());
call.perform();
void AlarmControlPanel::arm_away(const char *code) { this->arm_with_code_(&AlarmControlPanelCall::arm_away, code); }
void AlarmControlPanel::arm_home(const char *code) { this->arm_with_code_(&AlarmControlPanelCall::arm_home, code); }
void AlarmControlPanel::arm_night(const char *code) { this->arm_with_code_(&AlarmControlPanelCall::arm_night, code); }
void AlarmControlPanel::arm_vacation(const char *code) {
this->arm_with_code_(&AlarmControlPanelCall::arm_vacation, code);
}
void AlarmControlPanel::arm_night(optional<std::string> code) {
auto call = this->make_call();
call.arm_night();
if (code.has_value())
call.set_code(code.value());
call.perform();
void AlarmControlPanel::arm_custom_bypass(const char *code) {
this->arm_with_code_(&AlarmControlPanelCall::arm_custom_bypass, code);
}
void AlarmControlPanel::arm_vacation(optional<std::string> code) {
auto call = this->make_call();
call.arm_vacation();
if (code.has_value())
call.set_code(code.value());
call.perform();
}
void AlarmControlPanel::arm_custom_bypass(optional<std::string> code) {
auto call = this->make_call();
call.arm_custom_bypass();
if (code.has_value())
call.set_code(code.value());
call.perform();
}
void AlarmControlPanel::disarm(optional<std::string> code) {
auto call = this->make_call();
call.disarm();
if (code.has_value())
call.set_code(code.value());
call.perform();
}
void AlarmControlPanel::disarm(const char *code) { this->arm_with_code_(&AlarmControlPanelCall::disarm, code); }
} // namespace esphome::alarm_control_panel

View File

@@ -76,37 +76,53 @@ class AlarmControlPanel : public EntityBase {
*
* @param code The code
*/
void arm_away(optional<std::string> code = nullopt);
void arm_away(const char *code = nullptr);
void arm_away(const optional<std::string> &code) {
this->arm_away(code.has_value() ? code.value().c_str() : nullptr);
}
/** arm the alarm in home mode
*
* @param code The code
*/
void arm_home(optional<std::string> code = nullopt);
void arm_home(const char *code = nullptr);
void arm_home(const optional<std::string> &code) {
this->arm_home(code.has_value() ? code.value().c_str() : nullptr);
}
/** arm the alarm in night mode
*
* @param code The code
*/
void arm_night(optional<std::string> code = nullopt);
void arm_night(const char *code = nullptr);
void arm_night(const optional<std::string> &code) {
this->arm_night(code.has_value() ? code.value().c_str() : nullptr);
}
/** arm the alarm in vacation mode
*
* @param code The code
*/
void arm_vacation(optional<std::string> code = nullopt);
void arm_vacation(const char *code = nullptr);
void arm_vacation(const optional<std::string> &code) {
this->arm_vacation(code.has_value() ? code.value().c_str() : nullptr);
}
/** arm the alarm in custom bypass mode
*
* @param code The code
*/
void arm_custom_bypass(optional<std::string> code = nullopt);
void arm_custom_bypass(const char *code = nullptr);
void arm_custom_bypass(const optional<std::string> &code) {
this->arm_custom_bypass(code.has_value() ? code.value().c_str() : nullptr);
}
/** disarm the alarm
*
* @param code The code
*/
void disarm(optional<std::string> code = nullopt);
void disarm(const char *code = nullptr);
void disarm(const optional<std::string> &code) { this->disarm(code.has_value() ? code.value().c_str() : nullptr); }
/** Get the state
*
@@ -118,6 +134,8 @@ class AlarmControlPanel : public EntityBase {
protected:
friend AlarmControlPanelCall;
// Helper to reduce code duplication for arm/disarm methods
void arm_with_code_(AlarmControlPanelCall &(AlarmControlPanelCall::*arm_method)(), const char *code);
// in order to store last panel state in flash
ESPPreferenceObject pref_;
// current state

View File

@@ -10,8 +10,10 @@ static const char *const TAG = "alarm_control_panel";
AlarmControlPanelCall::AlarmControlPanelCall(AlarmControlPanel *parent) : parent_(parent) {}
AlarmControlPanelCall &AlarmControlPanelCall::set_code(const std::string &code) {
this->code_ = code;
AlarmControlPanelCall &AlarmControlPanelCall::set_code(const char *code) {
if (code != nullptr) {
this->code_ = std::string(code);
}
return *this;
}

View File

@@ -14,7 +14,8 @@ class AlarmControlPanelCall {
public:
AlarmControlPanelCall(AlarmControlPanel *parent);
AlarmControlPanelCall &set_code(const std::string &code);
AlarmControlPanelCall &set_code(const char *code);
AlarmControlPanelCall &set_code(const std::string &code) { return this->set_code(code.c_str()); }
AlarmControlPanelCall &arm_away();
AlarmControlPanelCall &arm_home();
AlarmControlPanelCall &arm_night();

View File

@@ -66,15 +66,7 @@ template<typename... Ts> class ArmAwayAction : public Action<Ts...> {
TEMPLATABLE_VALUE(std::string, code)
void play(const Ts &...x) override {
auto call = this->alarm_control_panel_->make_call();
auto code = this->code_.optional_value(x...);
if (code.has_value()) {
call.set_code(code.value());
}
call.arm_away();
call.perform();
}
void play(const Ts &...x) override { this->alarm_control_panel_->arm_away(this->code_.optional_value(x...)); }
protected:
AlarmControlPanel *alarm_control_panel_;
@@ -86,15 +78,7 @@ template<typename... Ts> class ArmHomeAction : public Action<Ts...> {
TEMPLATABLE_VALUE(std::string, code)
void play(const Ts &...x) override {
auto call = this->alarm_control_panel_->make_call();
auto code = this->code_.optional_value(x...);
if (code.has_value()) {
call.set_code(code.value());
}
call.arm_home();
call.perform();
}
void play(const Ts &...x) override { this->alarm_control_panel_->arm_home(this->code_.optional_value(x...)); }
protected:
AlarmControlPanel *alarm_control_panel_;
@@ -106,15 +90,7 @@ template<typename... Ts> class ArmNightAction : public Action<Ts...> {
TEMPLATABLE_VALUE(std::string, code)
void play(const Ts &...x) override {
auto call = this->alarm_control_panel_->make_call();
auto code = this->code_.optional_value(x...);
if (code.has_value()) {
call.set_code(code.value());
}
call.arm_night();
call.perform();
}
void play(const Ts &...x) override { this->alarm_control_panel_->arm_night(this->code_.optional_value(x...)); }
protected:
AlarmControlPanel *alarm_control_panel_;

View File

@@ -1844,23 +1844,8 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
return false;
}
// Toggle Nagle's algorithm based on message type to prevent log messages from
// filling the TCP send buffer and crowding out important state updates.
//
// This honors the `no_delay` proto option - SubscribeLogsResponse is the only
// message with `option (no_delay) = false;` in api.proto, indicating it should
// allow Nagle coalescing. This option existed since 2019 but was never implemented.
//
// - Log messages: Enable Nagle (NODELAY=false) so small log packets coalesce
// into fewer, larger packets. They flush naturally via TCP delayed ACK timer
// (~200ms), buffer filling, or when a state update triggers a flush.
//
// - All other messages (state updates, responses): Disable Nagle (NODELAY=true)
// for immediate delivery. These are time-sensitive and should not be delayed.
//
// This must be done proactively BEFORE the buffer fills up - checking buffer
// state here would be too late since we'd already be in a degraded state.
this->helper_->set_nodelay(!is_log_message);
// Set TCP_NODELAY based on message type - see set_nodelay_for_message() for details
this->helper_->set_nodelay_for_message(is_log_message);
APIError err = this->helper_->write_protobuf_packet(message_type, buffer);
if (err == APIError::WOULD_BLOCK)

View File

@@ -120,26 +120,39 @@ class APIFrameHelper {
}
return APIError::OK;
}
/// Toggle TCP_NODELAY socket option to control Nagle's algorithm.
///
/// This is used to allow log messages to coalesce (Nagle enabled) while keeping
/// state updates low-latency (NODELAY enabled). Without this, many small log
/// packets fill the TCP send buffer, crowding out important state updates.
///
/// State is tracked to minimize setsockopt() overhead - on lwip_raw (ESP8266/RP2040)
/// this is just a boolean assignment; on other platforms it's a lightweight syscall.
///
/// @param enable true to enable NODELAY (disable Nagle), false to enable Nagle
/// @return true if successful or already in desired state
bool set_nodelay(bool enable) {
if (this->nodelay_enabled_ == enable)
return true;
int val = enable ? 1 : 0;
int err = this->socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &val, sizeof(int));
if (err == 0) {
this->nodelay_enabled_ = enable;
// Manage TCP_NODELAY (Nagle's algorithm) based on message type.
//
// For non-log messages (sensor data, state updates): Always disable Nagle
// (NODELAY on) for immediate delivery - these are time-sensitive.
//
// For log messages: Use Nagle to coalesce multiple small log packets into
// fewer larger packets, reducing WiFi overhead. However, we limit batching
// to 3 messages to avoid excessive LWIP buffer pressure on memory-constrained
// devices like ESP8266. LWIP's TCP_OVERSIZE option coalesces the data into
// shared pbufs, but holding data too long waiting for Nagle's timer causes
// buffer exhaustion and dropped messages.
//
// Flow: Log 1 (Nagle on) -> Log 2 (Nagle on) -> Log 3 (NODELAY, flush all)
//
void set_nodelay_for_message(bool is_log_message) {
if (!is_log_message) {
if (this->nodelay_state_ != NODELAY_ON) {
this->set_nodelay_raw_(true);
this->nodelay_state_ = NODELAY_ON;
}
return;
}
// Log messages 1-3: state transitions -1 -> 1 -> 2 -> -1 (flush on 3rd)
if (this->nodelay_state_ == NODELAY_ON) {
this->set_nodelay_raw_(false);
this->nodelay_state_ = 1;
} else if (this->nodelay_state_ >= LOG_NAGLE_COUNT) {
this->set_nodelay_raw_(true);
this->nodelay_state_ = NODELAY_ON;
} else {
this->nodelay_state_++;
}
return err == 0;
}
virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0;
// Write multiple protobuf messages in a single operation
@@ -229,10 +242,18 @@ class APIFrameHelper {
uint8_t tx_buf_head_{0};
uint8_t tx_buf_tail_{0};
uint8_t tx_buf_count_{0};
// Tracks TCP_NODELAY state to minimize setsockopt() calls. Initialized to true
// since init_common_() enables NODELAY. Used by set_nodelay() to allow log
// messages to coalesce while keeping state updates low-latency.
bool nodelay_enabled_{true};
// Nagle batching state for log messages. NODELAY_ON (-1) means NODELAY is enabled
// (immediate send). Values 1-2 count log messages in the current Nagle batch.
// After LOG_NAGLE_COUNT logs, we switch to NODELAY to flush and reset.
static constexpr int8_t NODELAY_ON = -1;
static constexpr int8_t LOG_NAGLE_COUNT = 2;
int8_t nodelay_state_{NODELAY_ON};
// Internal helper to set TCP_NODELAY socket option
void set_nodelay_raw_(bool enable) {
int val = enable ? 1 : 0;
this->socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &val, sizeof(int));
}
// Common initialization for both plaintext and noise protocols
APIError init_common_();

View File

@@ -13,14 +13,11 @@ from . import AQI_CALCULATION_TYPE, CONF_CALCULATION_TYPE, aqi_ns
CODEOWNERS = ["@jasstrong"]
DEPENDENCIES = ["sensor"]
UNIT_INDEX = "index"
AQISensor = aqi_ns.class_("AQISensor", sensor.Sensor, cg.Component)
CONFIG_SCHEMA = (
sensor.sensor_schema(
AQISensor,
unit_of_measurement=UNIT_INDEX,
accuracy_decimals=0,
device_class=DEVICE_CLASS_AQI,
state_class=STATE_CLASS_MEASUREMENT,

View File

@@ -108,10 +108,14 @@ void ATM90E32Component::update() {
#endif
}
void ATM90E32Component::get_cs_summary_(std::span<char, GPIO_SUMMARY_MAX_LEN> buffer) {
this->cs_->dump_summary(buffer.data(), buffer.size());
}
void ATM90E32Component::setup() {
this->spi_setup();
this->cs_summary_ = this->cs_->dump_summary();
const char *cs = this->cs_summary_.c_str();
char cs[GPIO_SUMMARY_MAX_LEN];
this->get_cs_summary_(cs);
uint16_t mmode0 = 0x87; // 3P4W 50Hz
uint16_t high_thresh = 0;
@@ -159,13 +163,13 @@ void ATM90E32Component::setup() {
if (this->enable_offset_calibration_) {
// Initialize flash storage for offset calibrations
uint32_t o_hash = fnv1_hash("_offset_calibration_");
o_hash = fnv1_hash_extend(o_hash, this->cs_summary_);
o_hash = fnv1_hash_extend(o_hash, cs);
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("_power_offset_calibration_");
po_hash = fnv1_hash_extend(po_hash, this->cs_summary_);
po_hash = fnv1_hash_extend(po_hash, cs);
this->power_offset_pref_ = global_preferences->make_preference<PowerOffsetCalibration[3]>(po_hash, true);
this->restore_power_offset_calibrations_();
} else {
@@ -186,7 +190,7 @@ void ATM90E32Component::setup() {
if (this->enable_gain_calibration_) {
// Initialize flash storage for gain calibration
uint32_t g_hash = fnv1_hash("_gain_calibration_");
g_hash = fnv1_hash_extend(g_hash, this->cs_summary_);
g_hash = fnv1_hash_extend(g_hash, cs);
this->gain_calibration_pref_ = global_preferences->make_preference<GainCalibration[3]>(g_hash, true);
this->restore_gain_calibrations_();
@@ -217,7 +221,8 @@ void ATM90E32Component::setup() {
}
void ATM90E32Component::log_calibration_status_() {
const char *cs = this->cs_summary_.c_str();
char cs[GPIO_SUMMARY_MAX_LEN];
this->get_cs_summary_(cs);
bool offset_mismatch = false;
bool power_mismatch = false;
@@ -568,7 +573,8 @@ float ATM90E32Component::get_chip_temperature_() {
}
void ATM90E32Component::run_gain_calibrations() {
const char *cs = this->cs_summary_.c_str();
char cs[GPIO_SUMMARY_MAX_LEN];
this->get_cs_summary_(cs);
if (!this->enable_gain_calibration_) {
ESP_LOGW(TAG, "[CALIBRATION][%s] Gain calibration is disabled! Enable it first with enable_gain_calibration: true",
cs);
@@ -668,7 +674,8 @@ void ATM90E32Component::run_gain_calibrations() {
}
void ATM90E32Component::save_gain_calibration_to_memory_() {
const char *cs = this->cs_summary_.c_str();
char cs[GPIO_SUMMARY_MAX_LEN];
this->get_cs_summary_(cs);
bool success = this->gain_calibration_pref_.save(&this->gain_phase_);
global_preferences->sync();
if (success) {
@@ -681,7 +688,8 @@ void ATM90E32Component::save_gain_calibration_to_memory_() {
}
void ATM90E32Component::save_offset_calibration_to_memory_() {
const char *cs = this->cs_summary_.c_str();
char cs[GPIO_SUMMARY_MAX_LEN];
this->get_cs_summary_(cs);
bool success = this->offset_pref_.save(&this->offset_phase_);
global_preferences->sync();
if (success) {
@@ -697,7 +705,8 @@ void ATM90E32Component::save_offset_calibration_to_memory_() {
}
void ATM90E32Component::save_power_offset_calibration_to_memory_() {
const char *cs = this->cs_summary_.c_str();
char cs[GPIO_SUMMARY_MAX_LEN];
this->get_cs_summary_(cs);
bool success = this->power_offset_pref_.save(&this->power_offset_phase_);
global_preferences->sync();
if (success) {
@@ -713,7 +722,8 @@ void ATM90E32Component::save_power_offset_calibration_to_memory_() {
}
void ATM90E32Component::run_offset_calibrations() {
const char *cs = this->cs_summary_.c_str();
char cs[GPIO_SUMMARY_MAX_LEN];
this->get_cs_summary_(cs);
if (!this->enable_offset_calibration_) {
ESP_LOGW(TAG,
"[CALIBRATION][%s] Offset calibration is disabled! Enable it first with enable_offset_calibration: true",
@@ -743,7 +753,8 @@ void ATM90E32Component::run_offset_calibrations() {
}
void ATM90E32Component::run_power_offset_calibrations() {
const char *cs = this->cs_summary_.c_str();
char cs[GPIO_SUMMARY_MAX_LEN];
this->get_cs_summary_(cs);
if (!this->enable_offset_calibration_) {
ESP_LOGW(
TAG,
@@ -816,7 +827,8 @@ void ATM90E32Component::write_power_offsets_to_registers_(uint8_t phase, int16_t
}
void ATM90E32Component::restore_gain_calibrations_() {
const char *cs = this->cs_summary_.c_str();
char cs[GPIO_SUMMARY_MAX_LEN];
this->get_cs_summary_(cs);
for (uint8_t i = 0; i < 3; ++i) {
this->config_gain_phase_[i].voltage_gain = this->phase_[i].voltage_gain_;
this->config_gain_phase_[i].current_gain = this->phase_[i].ct_gain_;
@@ -870,7 +882,8 @@ void ATM90E32Component::restore_gain_calibrations_() {
}
void ATM90E32Component::restore_offset_calibrations_() {
const char *cs = this->cs_summary_.c_str();
char cs[GPIO_SUMMARY_MAX_LEN];
this->get_cs_summary_(cs);
for (uint8_t i = 0; i < 3; ++i)
this->config_offset_phase_[i] = this->offset_phase_[i];
@@ -912,7 +925,8 @@ void ATM90E32Component::restore_offset_calibrations_() {
}
void ATM90E32Component::restore_power_offset_calibrations_() {
const char *cs = this->cs_summary_.c_str();
char cs[GPIO_SUMMARY_MAX_LEN];
this->get_cs_summary_(cs);
for (uint8_t i = 0; i < 3; ++i)
this->config_power_offset_phase_[i] = this->power_offset_phase_[i];
@@ -954,7 +968,8 @@ void ATM90E32Component::restore_power_offset_calibrations_() {
}
void ATM90E32Component::clear_gain_calibrations() {
const char *cs = this->cs_summary_.c_str();
char cs[GPIO_SUMMARY_MAX_LEN];
this->get_cs_summary_(cs);
if (!this->using_saved_calibrations_) {
ESP_LOGI(TAG, "[CALIBRATION][%s] No stored gain calibrations to clear. Current values:", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] ----------------------------------------------------------", cs);
@@ -1003,7 +1018,8 @@ void ATM90E32Component::clear_gain_calibrations() {
}
void ATM90E32Component::clear_offset_calibrations() {
const char *cs = this->cs_summary_.c_str();
char cs[GPIO_SUMMARY_MAX_LEN];
this->get_cs_summary_(cs);
if (!this->restored_offset_calibration_) {
ESP_LOGI(TAG, "[CALIBRATION][%s] No stored offset calibrations to clear. Current values:", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs);
@@ -1045,7 +1061,8 @@ void ATM90E32Component::clear_offset_calibrations() {
}
void ATM90E32Component::clear_power_offset_calibrations() {
const char *cs = this->cs_summary_.c_str();
char cs[GPIO_SUMMARY_MAX_LEN];
this->get_cs_summary_(cs);
if (!this->restored_power_offset_calibration_) {
ESP_LOGI(TAG, "[CALIBRATION][%s] No stored power offsets to clear. Current values:", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs);
@@ -1120,7 +1137,8 @@ int16_t ATM90E32Component::calibrate_power_offset(uint8_t phase, bool reactive)
}
bool ATM90E32Component::verify_gain_writes_() {
const char *cs = this->cs_summary_.c_str();
char cs[GPIO_SUMMARY_MAX_LEN];
this->get_cs_summary_(cs);
bool success = true;
for (uint8_t phase = 0; phase < 3; phase++) {
uint16_t read_voltage = this->read16_(voltage_gain_registers[phase]);

View File

@@ -1,11 +1,13 @@
#pragma once
#include <span>
#include <unordered_map>
#include "atm90e32_reg.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/spi/spi.h"
#include "esphome/core/application.h"
#include "esphome/core/component.h"
#include "esphome/core/gpio.h"
#include "esphome/core/helpers.h"
#include "esphome/core/preferences.h"
@@ -182,6 +184,7 @@ class ATM90E32Component : public PollingComponent,
bool verify_gain_writes_();
bool validate_spi_read_(uint16_t expected, const char *context = nullptr);
void log_calibration_status_();
void get_cs_summary_(std::span<char, GPIO_SUMMARY_MAX_LEN> buffer);
struct ATM90E32Phase {
uint16_t voltage_gain_{0};
@@ -247,7 +250,6 @@ class ATM90E32Component : public PollingComponent,
ESPPreferenceObject offset_pref_;
ESPPreferenceObject power_offset_pref_;
ESPPreferenceObject gain_calibration_pref_;
std::string cs_summary_;
sensor::Sensor *freq_sensor_{nullptr};
#ifdef USE_TEXT_SENSOR

View File

@@ -1,4 +1,5 @@
import esphome.codegen as cg
from esphome.components.esp32 import add_idf_component
import esphome.config_validation as cv
from esphome.const import CONF_BITS_PER_SAMPLE, CONF_NUM_CHANNELS, CONF_SAMPLE_RATE
import esphome.final_validate as fv
@@ -165,4 +166,7 @@ def final_validate_audio_schema(
async def to_code(config):
cg.add_library("esphome/esp-audio-libs", "2.0.1")
add_idf_component(
name="esphome/esp-audio-libs",
ref="2.0.3",
)

View File

@@ -300,7 +300,7 @@ FileDecoderState AudioDecoder::decode_mp3_() {
// Advance read pointer to match the offset for the syncword
this->input_transfer_buffer_->decrease_buffer_length(offset);
uint8_t *buffer_start = this->input_transfer_buffer_->get_buffer_start();
const uint8_t *buffer_start = this->input_transfer_buffer_->get_buffer_start();
buffer_length = (int) this->input_transfer_buffer_->available();
int err = esp_audio_libs::helix_decoder::MP3Decode(this->mp3_decoder_, &buffer_start, &buffer_length,

View File

@@ -1,7 +1,8 @@
import esphome.codegen as cg
from esphome.components import esp32_ble_tracker
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_MAC_ADDRESS
from esphome.const import CONF_BINDKEY, CONF_ID, CONF_MAC_ADDRESS
from esphome.core import HexInt
CODEOWNERS = ["@nagyrobi"]
DEPENDENCIES = ["esp32_ble_tracker"]
@@ -22,6 +23,7 @@ def bthome_mithermometer_base_schema(extra_schema=None):
{
cv.GenerateID(CONF_ID): cv.declare_id(BTHomeMiThermometer),
cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
cv.Optional(CONF_BINDKEY): cv.bind_key,
}
)
.extend(BLE_DEVICE_SCHEMA)
@@ -34,3 +36,9 @@ async def setup_bthome_mithermometer(var, config):
await cg.register_component(var, config)
await esp32_ble_tracker.register_ble_device(var, config)
cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
if bindkey := config.get(CONF_BINDKEY):
bindkey_bytes = [
HexInt(int(bindkey[index : index + 2], 16))
for index in range(0, len(bindkey), 2)
]
cg.add(var.set_bindkey(cg.ArrayInitializer(*bindkey_bytes)))

View File

@@ -3,15 +3,23 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <algorithm>
#include <array>
#include <cstring>
#include <span>
#ifdef USE_ESP32
#include "mbedtls/ccm.h"
namespace esphome {
namespace bthome_mithermometer {
static const char *const TAG = "bthome_mithermometer";
static constexpr size_t BTHOME_BINDKEY_SIZE = 16;
static constexpr size_t BTHOME_NONCE_SIZE = 13;
static constexpr size_t BTHOME_MIC_SIZE = 4;
static constexpr size_t BTHOME_COUNTER_SIZE = 4;
static const char *format_mac_address(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buffer, uint64_t address) {
std::array<uint8_t, MAC_ADDRESS_SIZE> mac{};
@@ -130,6 +138,10 @@ void BTHomeMiThermometer::dump_config() {
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
ESP_LOGCONFIG(TAG, "BTHome MiThermometer");
ESP_LOGCONFIG(TAG, " MAC Address: %s", format_mac_address(addr_buf, this->address_));
if (this->has_bindkey_) {
char bindkey_hex[format_hex_pretty_size(BTHOME_BINDKEY_SIZE)];
ESP_LOGCONFIG(TAG, " Bindkey: %s", format_hex_pretty_to(bindkey_hex, this->bindkey_, BTHOME_BINDKEY_SIZE, '.'));
}
LOG_SENSOR(" ", "Temperature", this->temperature_);
LOG_SENSOR(" ", "Humidity", this->humidity_);
LOG_SENSOR(" ", "Battery Level", this->battery_level_);
@@ -150,6 +162,60 @@ bool BTHomeMiThermometer::parse_device(const esp32_ble_tracker::ESPBTDevice &dev
return matched;
}
void BTHomeMiThermometer::set_bindkey(std::initializer_list<uint8_t> bindkey) {
if (bindkey.size() != sizeof(this->bindkey_)) {
ESP_LOGW(TAG, "BTHome bindkey size mismatch: %zu", bindkey.size());
return;
}
std::copy(bindkey.begin(), bindkey.end(), this->bindkey_);
this->has_bindkey_ = true;
}
bool BTHomeMiThermometer::decrypt_bthome_payload_(const std::vector<uint8_t> &data, uint64_t source_address,
std::vector<uint8_t> &payload) const {
if (data.size() <= 1 + BTHOME_COUNTER_SIZE + BTHOME_MIC_SIZE) {
ESP_LOGVV(TAG, "Encrypted BTHome payload too short: %zu", data.size());
return false;
}
const size_t ciphertext_size = data.size() - 1 - BTHOME_COUNTER_SIZE - BTHOME_MIC_SIZE;
payload.resize(ciphertext_size);
std::array<uint8_t, MAC_ADDRESS_SIZE> mac{};
for (size_t i = 0; i < MAC_ADDRESS_SIZE; i++) {
mac[i] = (source_address >> ((MAC_ADDRESS_SIZE - 1 - i) * 8)) & 0xFF;
}
std::array<uint8_t, BTHOME_NONCE_SIZE> nonce{};
memcpy(nonce.data(), mac.data(), mac.size());
nonce[6] = 0xD2;
nonce[7] = 0xFC;
nonce[8] = data[0];
memcpy(nonce.data() + 9, &data[data.size() - BTHOME_COUNTER_SIZE - BTHOME_MIC_SIZE], BTHOME_COUNTER_SIZE);
const uint8_t *ciphertext = data.data() + 1;
const uint8_t *mic = data.data() + data.size() - BTHOME_MIC_SIZE;
mbedtls_ccm_context ctx;
mbedtls_ccm_init(&ctx);
int ret = mbedtls_ccm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, this->bindkey_, BTHOME_BINDKEY_SIZE * 8);
if (ret) {
ESP_LOGVV(TAG, "mbedtls_ccm_setkey() failed.");
mbedtls_ccm_free(&ctx);
return false;
}
ret = mbedtls_ccm_auth_decrypt(&ctx, ciphertext_size, nonce.data(), nonce.size(), nullptr, 0, ciphertext,
payload.data(), mic, BTHOME_MIC_SIZE);
mbedtls_ccm_free(&ctx);
if (ret) {
ESP_LOGVV(TAG, "BTHome decryption failed (ret=%d).", ret);
return false;
}
return true;
}
bool BTHomeMiThermometer::handle_service_data_(const esp32_ble_tracker::ServiceData &service_data,
const esp32_ble_tracker::ESPBTDevice &device) {
if (!service_data.uuid.contains(0xD2, 0xFC)) {
@@ -173,51 +239,88 @@ bool BTHomeMiThermometer::handle_service_data_(const esp32_ble_tracker::ServiceD
return false;
}
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
if (is_encrypted) {
ESP_LOGV(TAG, "Ignoring encrypted BTHome frame from %s", device.address_str_to(addr_buf));
uint64_t source_address = device.address_uint64();
bool address_matches = source_address == this->address_;
if (!is_encrypted && mac_included && data.size() >= 7) {
uint64_t advertised_address = 0;
for (int i = 5; i >= 0; i--) {
advertised_address = (advertised_address << 8) | data[1 + i];
}
address_matches = address_matches || advertised_address == this->address_;
}
if (is_encrypted && !this->has_bindkey_) {
if (address_matches) {
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
ESP_LOGE(TAG, "Encrypted BTHome frame received but no bindkey configured for %s",
device.address_str_to(addr_buf));
}
return false;
}
size_t payload_index = 1;
uint64_t source_address = device.address_uint64();
if (!is_encrypted && this->has_bindkey_) {
if (address_matches) {
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
ESP_LOGE(TAG, "Unencrypted BTHome frame received with bindkey configured for %s",
device.address_str_to(addr_buf));
}
return false;
}
std::vector<uint8_t> decrypted_payload;
const uint8_t *payload = nullptr;
size_t payload_size = 0;
if (is_encrypted) {
if (!this->decrypt_bthome_payload_(data, source_address, decrypted_payload)) {
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
ESP_LOGVV(TAG, "Failed to decrypt BTHome frame from %s", device.address_str_to(addr_buf));
return false;
}
payload = decrypted_payload.data();
payload_size = decrypted_payload.size();
} else {
payload = data.data() + 1;
payload_size = data.size() - 1;
}
if (mac_included) {
if (data.size() < 7) {
if (payload_size < 6) {
ESP_LOGVV(TAG, "BTHome payload missing MAC address");
return false;
}
source_address = 0;
for (int i = 5; i >= 0; i--) {
source_address = (source_address << 8) | data[1 + i];
source_address = (source_address << 8) | payload[i];
}
payload_index = 7;
payload += 6;
payload_size -= 6;
}
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
if (source_address != this->address_) {
ESP_LOGVV(TAG, "BTHome frame from unexpected device %s", format_mac_address(addr_buf, source_address));
return false;
}
if (payload_index >= data.size()) {
if (payload_size == 0) {
ESP_LOGVV(TAG, "BTHome payload empty after header");
return false;
}
bool reported = false;
size_t offset = payload_index;
size_t offset = 0;
uint8_t last_type = 0;
while (offset < data.size()) {
const uint8_t obj_type = data[offset++];
while (offset < payload_size) {
const uint8_t obj_type = payload[offset++];
size_t value_length = 0;
bool has_length_byte = obj_type == 0x53; // text objects include explicit length
if (has_length_byte) {
if (offset >= data.size()) {
if (offset >= payload_size) {
break;
}
value_length = data[offset++];
value_length = payload[offset++];
} else {
if (!get_bthome_value_length(obj_type, value_length)) {
ESP_LOGVV(TAG, "Unknown BTHome object 0x%02X", obj_type);
@@ -229,12 +332,12 @@ bool BTHomeMiThermometer::handle_service_data_(const esp32_ble_tracker::ServiceD
break;
}
if (offset + value_length > data.size()) {
if (offset + value_length > payload_size) {
ESP_LOGVV(TAG, "BTHome object length exceeds payload");
break;
}
const uint8_t *value = &data[offset];
const uint8_t *value = &payload[offset];
offset += value_length;
if (obj_type < last_type) {

View File

@@ -5,6 +5,8 @@
#include "esphome/core/component.h"
#include <cstdint>
#include <initializer_list>
#include <vector>
#ifdef USE_ESP32
@@ -14,6 +16,7 @@ namespace bthome_mithermometer {
class BTHomeMiThermometer : public esp32_ble_tracker::ESPBTDeviceListener, public Component {
public:
void set_address(uint64_t address) { this->address_ = address; }
void set_bindkey(std::initializer_list<uint8_t> bindkey);
void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; }
void set_humidity(sensor::Sensor *humidity) { this->humidity_ = humidity; }
@@ -27,9 +30,13 @@ class BTHomeMiThermometer : public esp32_ble_tracker::ESPBTDeviceListener, publi
protected:
bool handle_service_data_(const esp32_ble_tracker::ServiceData &service_data,
const esp32_ble_tracker::ESPBTDevice &device);
bool decrypt_bthome_payload_(const std::vector<uint8_t> &data, uint64_t source_address,
std::vector<uint8_t> &payload) const;
uint64_t address_{0};
optional<uint8_t> last_packet_id_{};
bool has_bindkey_{false};
uint8_t bindkey_[16];
sensor::Sensor *temperature_{nullptr};
sensor::Sensor *humidity_{nullptr};

View File

@@ -132,6 +132,26 @@ void DebugComponent::log_partition_info_() {
flash_area_foreach(fa_cb, nullptr);
}
static const char *regout0_to_str(uint32_t value) {
switch (value) {
case (UICR_REGOUT0_VOUT_DEFAULT):
return "1.8V (default)";
case (UICR_REGOUT0_VOUT_1V8):
return "1.8V";
case (UICR_REGOUT0_VOUT_2V1):
return "2.1V";
case (UICR_REGOUT0_VOUT_2V4):
return "2.4V";
case (UICR_REGOUT0_VOUT_2V7):
return "2.7V";
case (UICR_REGOUT0_VOUT_3V0):
return "3.0V";
case (UICR_REGOUT0_VOUT_3V3):
return "3.3V";
}
return "???V";
}
size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE> buffer, size_t pos) {
constexpr size_t size = DEVICE_INFO_BUFFER_SIZE;
char *buf = buffer.data();
@@ -145,34 +165,14 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
// Regulator stage 0
if (nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_HIGH) {
const char *reg0_type = nrf_power_dcdcen_vddh_get(NRF_POWER) ? "DC/DC" : "LDO";
const char *reg0_voltage;
switch (NRF_UICR->REGOUT0 & UICR_REGOUT0_VOUT_Msk) {
case (UICR_REGOUT0_VOUT_DEFAULT << UICR_REGOUT0_VOUT_Pos):
reg0_voltage = "1.8V (default)";
break;
case (UICR_REGOUT0_VOUT_1V8 << UICR_REGOUT0_VOUT_Pos):
reg0_voltage = "1.8V";
break;
case (UICR_REGOUT0_VOUT_2V1 << UICR_REGOUT0_VOUT_Pos):
reg0_voltage = "2.1V";
break;
case (UICR_REGOUT0_VOUT_2V4 << UICR_REGOUT0_VOUT_Pos):
reg0_voltage = "2.4V";
break;
case (UICR_REGOUT0_VOUT_2V7 << UICR_REGOUT0_VOUT_Pos):
reg0_voltage = "2.7V";
break;
case (UICR_REGOUT0_VOUT_3V0 << UICR_REGOUT0_VOUT_Pos):
reg0_voltage = "3.0V";
break;
case (UICR_REGOUT0_VOUT_3V3 << UICR_REGOUT0_VOUT_Pos):
reg0_voltage = "3.3V";
break;
default:
reg0_voltage = "???V";
}
const char *reg0_voltage = regout0_to_str((NRF_UICR->REGOUT0 & UICR_REGOUT0_VOUT_Msk) >> UICR_REGOUT0_VOUT_Pos);
ESP_LOGD(TAG, "Regulator stage 0: %s, %s", reg0_type, reg0_voltage);
pos = buf_append_printf(buf, size, pos, "|Regulator stage 0: %s, %s", reg0_type, reg0_voltage);
#ifdef USE_NRF52_REG0_VOUT
if ((NRF_UICR->REGOUT0 & UICR_REGOUT0_VOUT_Msk) >> UICR_REGOUT0_VOUT_Pos != USE_NRF52_REG0_VOUT) {
ESP_LOGE(TAG, "Regulator stage 0: expected %s", regout0_to_str(USE_NRF52_REG0_VOUT));
}
#endif
} else {
ESP_LOGD(TAG, "Regulator stage 0: disabled");
pos = buf_append_printf(buf, size, pos, "|Regulator stage 0: disabled");

View File

@@ -89,10 +89,8 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r
delayMicroseconds(500);
} else if (this->model_ == DHT_MODEL_DHT22_TYPE2) {
delayMicroseconds(2000);
} else if (this->model_ == DHT_MODEL_AM2120 || this->model_ == DHT_MODEL_AM2302) {
delayMicroseconds(1000);
} else {
delayMicroseconds(800);
delayMicroseconds(1000);
}
#ifdef USE_ESP32

View File

@@ -190,7 +190,7 @@ async def to_code(config):
# Rotation is handled by setting the transform
display_config = {k: v for k, v in config.items() if k != CONF_ROTATION}
await display.register_display(var, display_config)
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
cg.add(var.set_dc_pin(dc))

View File

@@ -182,6 +182,12 @@ def set_core_data(config):
path=[CONF_CPU_FREQUENCY],
)
if variant == VARIANT_ESP32P4 and cpu_frequency == "400MHZ":
_LOGGER.warning(
"400MHz on ESP32-P4 is experimental and may not boot. "
"Consider using 360MHz instead. See https://github.com/esphome/esphome/issues/13425"
)
CORE.data[KEY_ESP32] = {}
CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_ESP32
conf = config[CONF_FRAMEWORK]
@@ -342,7 +348,12 @@ def add_extra_build_file(filename: str, path: Path) -> bool:
def _format_framework_arduino_version(ver: cv.Version) -> str:
# format the given arduino (https://github.com/espressif/arduino-esp32/releases) version to
# a PIO pioarduino/framework-arduinoespressif32 value
return f"pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/{str(ver)}/esp32-{str(ver)}.zip"
# 3.3.6+ changed filename from esp32-{ver}.zip to esp32-core-{ver}.tar.xz
if ver >= cv.Version(3, 3, 6):
filename = f"esp32-core-{ver}.tar.xz"
else:
filename = f"esp32-{ver}.zip"
return f"pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/{ver}/{filename}"
def _format_framework_espidf_version(ver: cv.Version, release: str) -> str:
@@ -377,11 +388,12 @@ def _is_framework_url(source: str) -> bool:
# The default/recommended arduino framework version
# - https://github.com/espressif/arduino-esp32/releases
ARDUINO_FRAMEWORK_VERSION_LOOKUP = {
"recommended": cv.Version(3, 3, 5),
"latest": cv.Version(3, 3, 5),
"dev": cv.Version(3, 3, 5),
"recommended": cv.Version(3, 3, 6),
"latest": cv.Version(3, 3, 6),
"dev": cv.Version(3, 3, 6),
}
ARDUINO_PLATFORM_VERSION_LOOKUP = {
cv.Version(3, 3, 6): cv.Version(55, 3, 36),
cv.Version(3, 3, 5): cv.Version(55, 3, 35),
cv.Version(3, 3, 4): cv.Version(55, 3, 31, "2"),
cv.Version(3, 3, 3): cv.Version(55, 3, 31, "2"),
@@ -399,6 +411,7 @@ ARDUINO_PLATFORM_VERSION_LOOKUP = {
# These versions correspond to pioarduino/esp-idf releases
# See: https://github.com/pioarduino/esp-idf/releases
ARDUINO_IDF_VERSION_LOOKUP = {
cv.Version(3, 3, 6): cv.Version(5, 5, 2),
cv.Version(3, 3, 5): cv.Version(5, 5, 2),
cv.Version(3, 3, 4): cv.Version(5, 5, 1),
cv.Version(3, 3, 3): cv.Version(5, 5, 1),
@@ -421,7 +434,7 @@ ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
"dev": cv.Version(5, 5, 2),
}
ESP_IDF_PLATFORM_VERSION_LOOKUP = {
cv.Version(5, 5, 2): cv.Version(55, 3, 35),
cv.Version(5, 5, 2): cv.Version(55, 3, 36),
cv.Version(5, 5, 1): cv.Version(55, 3, 31, "2"),
cv.Version(5, 5, 0): cv.Version(55, 3, 31, "2"),
cv.Version(5, 4, 3): cv.Version(55, 3, 32),
@@ -438,9 +451,9 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = {
# The platform-espressif32 version
# - https://github.com/pioarduino/platform-espressif32/releases
PLATFORM_VERSION_LOOKUP = {
"recommended": cv.Version(55, 3, 35),
"latest": cv.Version(55, 3, 35),
"dev": cv.Version(55, 3, 35),
"recommended": cv.Version(55, 3, 36),
"latest": cv.Version(55, 3, 36),
"dev": cv.Version(55, 3, 36),
}

View File

@@ -11,6 +11,7 @@
#include <esp_ota_ops.h>
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
#include "esphome/components/http_request/http_request.h"
#include "esphome/components/json/json_util.h"
#include "esphome/components/network/util.h"
#endif
@@ -184,15 +185,23 @@ bool Esp32HostedUpdate::fetch_manifest_() {
}
// Read manifest JSON into string (manifest is small, ~1KB max)
// NOTE: HttpContainer::read() has non-BSD socket semantics - see http_request.h
// Use http_read_loop_result() helper instead of checking return values directly
std::string json_str;
json_str.reserve(container->content_length);
uint8_t buf[256];
uint32_t last_data_time = millis();
const uint32_t read_timeout = this->http_request_parent_->get_timeout();
while (container->get_bytes_read() < container->content_length) {
int read = container->read(buf, sizeof(buf));
if (read > 0) {
json_str.append(reinterpret_cast<char *>(buf), read);
}
int read_or_error = container->read(buf, sizeof(buf));
App.feed_wdt();
yield();
auto result = http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout);
if (result == http_request::HttpReadLoopResult::RETRY)
continue;
if (result != http_request::HttpReadLoopResult::DATA)
break; // ERROR or TIMEOUT
json_str.append(reinterpret_cast<char *>(buf), read_or_error);
}
container->end();
@@ -297,32 +306,38 @@ bool Esp32HostedUpdate::stream_firmware_to_coprocessor_() {
}
// Stream firmware to coprocessor while computing SHA256
// NOTE: HttpContainer::read() has non-BSD socket semantics - see http_request.h
// Use http_read_loop_result() helper instead of checking return values directly
sha256::SHA256 hasher;
hasher.init();
uint8_t buffer[CHUNK_SIZE];
uint32_t last_data_time = millis();
const uint32_t read_timeout = this->http_request_parent_->get_timeout();
while (container->get_bytes_read() < total_size) {
int read = container->read(buffer, sizeof(buffer));
int read_or_error = container->read(buffer, sizeof(buffer));
// Feed watchdog and give other tasks a chance to run
App.feed_wdt();
yield();
// Exit loop if no data available (stream closed or end of data)
if (read <= 0) {
if (read < 0) {
ESP_LOGE(TAG, "Stream closed with error");
esp_hosted_slave_ota_end(); // NOLINT
container->end();
this->status_set_error(LOG_STR("Download failed"));
return false;
auto result = http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout);
if (result == http_request::HttpReadLoopResult::RETRY)
continue;
if (result != http_request::HttpReadLoopResult::DATA) {
if (result == http_request::HttpReadLoopResult::TIMEOUT) {
ESP_LOGE(TAG, "Timeout reading firmware data");
} else {
ESP_LOGE(TAG, "Error reading firmware data: %d", read_or_error);
}
// read == 0: no more data available, exit loop
break;
esp_hosted_slave_ota_end(); // NOLINT
container->end();
this->status_set_error(LOG_STR("Download failed"));
return false;
}
hasher.add(buffer, read);
err = esp_hosted_slave_ota_write(buffer, read); // NOLINT
hasher.add(buffer, read_or_error);
err = esp_hosted_slave_ota_write(buffer, read_or_error); // NOLINT
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to write OTA data: %s", esp_err_to_name(err));
esp_hosted_slave_ota_end(); // NOLINT

View File

@@ -12,7 +12,6 @@ extern "C" {
#include "preferences.h"
#include <cstring>
#include <memory>
namespace esphome::esp8266 {
@@ -143,16 +142,8 @@ class ESP8266PreferenceBackend : public ESPPreferenceBackend {
return false;
const size_t buffer_size = static_cast<size_t>(this->length_words) + 1;
uint32_t stack_buffer[PREF_BUFFER_WORDS];
std::unique_ptr<uint32_t[]> heap_buffer;
uint32_t *buffer;
if (buffer_size <= PREF_BUFFER_WORDS) {
buffer = stack_buffer;
} else {
heap_buffer = make_unique<uint32_t[]>(buffer_size);
buffer = heap_buffer.get();
}
SmallBufferWithHeapFallback<PREF_BUFFER_WORDS, uint32_t> buffer_alloc(buffer_size);
uint32_t *buffer = buffer_alloc.get();
memset(buffer, 0, buffer_size * sizeof(uint32_t));
memcpy(buffer, data, len);
@@ -167,16 +158,8 @@ class ESP8266PreferenceBackend : public ESPPreferenceBackend {
return false;
const size_t buffer_size = static_cast<size_t>(this->length_words) + 1;
uint32_t stack_buffer[PREF_BUFFER_WORDS];
std::unique_ptr<uint32_t[]> heap_buffer;
uint32_t *buffer;
if (buffer_size <= PREF_BUFFER_WORDS) {
buffer = stack_buffer;
} else {
heap_buffer = make_unique<uint32_t[]>(buffer_size);
buffer = heap_buffer.get();
}
SmallBufferWithHeapFallback<PREF_BUFFER_WORDS, uint32_t> buffer_alloc(buffer_size);
uint32_t *buffer = buffer_alloc.get();
bool ret = this->in_flash ? load_from_flash(this->offset, buffer, buffer_size)
: load_from_rtc(this->offset, buffer, buffer_size);

View File

@@ -1,4 +1,5 @@
#include "fingerprint_grow.h"
#include "esphome/core/gpio.h"
#include "esphome/core/log.h"
#include <cinttypes>
@@ -532,14 +533,21 @@ void FingerprintGrowComponent::sensor_sleep_() {
}
void FingerprintGrowComponent::dump_config() {
char sensing_pin_buf[GPIO_SUMMARY_MAX_LEN];
char power_pin_buf[GPIO_SUMMARY_MAX_LEN];
if (this->has_sensing_pin_) {
this->sensing_pin_->dump_summary(sensing_pin_buf, sizeof(sensing_pin_buf));
}
if (this->has_power_pin_) {
this->sensor_power_pin_->dump_summary(power_pin_buf, sizeof(power_pin_buf));
}
ESP_LOGCONFIG(TAG,
"GROW_FINGERPRINT_READER:\n"
" System Identifier Code: 0x%.4X\n"
" Touch Sensing Pin: %s\n"
" Sensor Power Pin: %s",
this->system_identifier_code_,
this->has_sensing_pin_ ? this->sensing_pin_->dump_summary().c_str() : "None",
this->has_power_pin_ ? this->sensor_power_pin_->dump_summary().c_str() : "None");
this->system_identifier_code_, this->has_sensing_pin_ ? sensing_pin_buf : "None",
this->has_power_pin_ ? power_pin_buf : "None");
if (this->idle_period_to_sleep_ms_ < UINT32_MAX) {
ESP_LOGCONFIG(TAG, " Idle Period to Sleep: %" PRIu32 " ms", this->idle_period_to_sleep_ms_);
} else {

View File

@@ -6,6 +6,7 @@ from esphome.const import (
CONF_BASELINE,
CONF_CO2,
CONF_ID,
CONF_WARMUP_TIME,
DEVICE_CLASS_CARBON_DIOXIDE,
ICON_MOLECULE_CO2,
STATE_CLASS_MEASUREMENT,
@@ -14,8 +15,6 @@ from esphome.const import (
DEPENDENCIES = ["uart"]
CONF_WARMUP_TIME = "warmup_time"
hc8_ns = cg.esphome_ns.namespace("hc8")
HC8Component = hc8_ns.class_("HC8Component", cg.PollingComponent, uart.UARTDevice)
HC8CalibrateAction = hc8_ns.class_("HC8CalibrateAction", automation.Action)

View File

@@ -107,7 +107,7 @@ CONFIG_SCHEMA = cv.All(
cv.Required(CONF_MAX_TEMPERATURE): cv.temperature,
}
),
cv.only_with_arduino,
cv.Any(cv.only_with_arduino, cv.only_on_esp32),
)
@@ -126,6 +126,6 @@ async def to_code(config):
cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE]))
cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE]))
cg.add_library("tonia/HeatpumpIR", "1.0.37")
cg.add_library("tonia/HeatpumpIR", "1.0.40")
if CORE.is_libretiny or CORE.is_esp32:
CORE.add_platformio_option("lib_ignore", ["IRremoteESP8266"])

View File

@@ -1,15 +1,46 @@
#include "heatpumpir.h"
#ifdef USE_ARDUINO
#if defined(USE_ARDUINO) || defined(USE_ESP32)
#include <map>
#include "ir_sender_esphome.h"
#include "HeatpumpIRFactory.h"
#include <IRSender.h>
#include <HeatpumpIRFactory.h>
#include "esphome/components/remote_base/remote_base.h"
#include "esphome/core/log.h"
namespace esphome {
namespace heatpumpir {
// IRSenderESPHome - bridge between ESPHome's remote_transmitter and HeatpumpIR library
// Defined here (not in a header) to isolate HeatpumpIR's headers from the rest of ESPHome,
// as they define conflicting symbols like millis() in the global namespace.
class IRSenderESPHome : public IRSender {
public:
IRSenderESPHome(remote_base::RemoteTransmitterBase *transmitter) : IRSender(0), transmit_(transmitter->transmit()) {}
void setFrequency(int frequency) override { // NOLINT(readability-identifier-naming)
auto *data = this->transmit_.get_data();
data->set_carrier_frequency(1000 * frequency);
}
void space(int space_length) override {
if (space_length) {
auto *data = this->transmit_.get_data();
data->space(space_length);
} else {
this->transmit_.perform();
}
}
void mark(int mark_length) override {
auto *data = this->transmit_.get_data();
data->mark(mark_length);
}
protected:
remote_base::RemoteTransmitterBase::TransmitCall transmit_;
};
static const char *const TAG = "heatpumpir.climate";
const std::map<Protocol, std::function<HeatpumpIR *()>> PROTOCOL_CONSTRUCTOR_MAP = {

View File

@@ -1,6 +1,6 @@
#pragma once
#ifdef USE_ARDUINO
#if defined(USE_ARDUINO) || defined(USE_ESP32)
#include "esphome/components/climate_ir/climate_ir.h"

View File

@@ -1,32 +0,0 @@
#include "ir_sender_esphome.h"
#ifdef USE_ARDUINO
namespace esphome {
namespace heatpumpir {
void IRSenderESPHome::setFrequency(int frequency) { // NOLINT(readability-identifier-naming)
auto *data = transmit_.get_data();
data->set_carrier_frequency(1000 * frequency);
}
// Send an IR 'mark' symbol, i.e. transmitter ON
void IRSenderESPHome::mark(int mark_length) {
auto *data = transmit_.get_data();
data->mark(mark_length);
}
// Send an IR 'space' symbol, i.e. transmitter OFF
void IRSenderESPHome::space(int space_length) {
if (space_length) {
auto *data = transmit_.get_data();
data->space(space_length);
} else {
transmit_.perform();
}
}
} // namespace heatpumpir
} // namespace esphome
#endif

View File

@@ -1,25 +0,0 @@
#pragma once
#ifdef USE_ARDUINO
#include "esphome/components/remote_base/remote_base.h"
#include <IRSender.h> // arduino-heatpump library
namespace esphome {
namespace heatpumpir {
class IRSenderESPHome : public IRSender {
public:
IRSenderESPHome(remote_base::RemoteTransmitterBase *transmitter) : IRSender(0), transmit_(transmitter->transmit()){};
void setFrequency(int frequency) override; // NOLINT(readability-identifier-naming)
void space(int space_length) override;
void mark(int mark_length) override;
protected:
remote_base::RemoteTransmitterBase::TransmitCall transmit_;
};
} // namespace heatpumpir
} // namespace esphome
#endif

View File

@@ -157,6 +157,7 @@ async def to_code(config):
if CORE.is_esp32:
cg.add(var.set_buffer_size_rx(config[CONF_BUFFER_SIZE_RX]))
cg.add(var.set_buffer_size_tx(config[CONF_BUFFER_SIZE_TX]))
cg.add(var.set_verify_ssl(config[CONF_VERIFY_SSL]))
if config.get(CONF_VERIFY_SSL):
esp32.add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True)

View File

@@ -79,6 +79,81 @@ inline bool is_redirect(int const status) {
*/
inline bool is_success(int const status) { return status >= HTTP_STATUS_OK && status < HTTP_STATUS_MULTIPLE_CHOICES; }
/*
* HTTP Container Read Semantics
* =============================
*
* IMPORTANT: These semantics differ from standard BSD sockets!
*
* BSD socket read() returns:
* > 0: bytes read
* == 0: connection closed (EOF)
* < 0: error (check errno)
*
* HttpContainer::read() returns:
* > 0: bytes read successfully
* == 0: no data available yet OR all content read
* (caller should check bytes_read vs content_length)
* < 0: error or connection closed (caller should EXIT)
* HTTP_ERROR_CONNECTION_CLOSED (-1) = connection closed prematurely
* other negative values = platform-specific errors
*
* Platform behaviors:
* - ESP-IDF: blocking reads, 0 only returned when all content read
* - Arduino: non-blocking, 0 means "no data yet" or "all content read"
*
* Use the helper functions below instead of checking return values directly:
* - http_read_loop_result(): for manual loops with per-chunk processing
* - http_read_fully(): for simple "read N bytes into buffer" operations
*/
/// Error code returned by HttpContainer::read() when connection closed prematurely
/// NOTE: Unlike BSD sockets where 0 means EOF, here 0 means "no data yet, retry"
static constexpr int HTTP_ERROR_CONNECTION_CLOSED = -1;
/// Status of a read operation
enum class HttpReadStatus : uint8_t {
OK, ///< Read completed successfully
ERROR, ///< Read error occurred
TIMEOUT, ///< Timeout waiting for data
};
/// Result of an HTTP read operation
struct HttpReadResult {
HttpReadStatus status; ///< Status of the read operation
int error_code; ///< Error code from read() on failure, 0 on success
};
/// Result of processing a non-blocking read with timeout (for manual loops)
enum class HttpReadLoopResult : uint8_t {
DATA, ///< Data was read, process it
RETRY, ///< No data yet, already delayed, caller should continue loop
ERROR, ///< Read error, caller should exit loop
TIMEOUT, ///< Timeout waiting for data, caller should exit loop
};
/// Process a read result with timeout tracking and delay handling
/// @param bytes_read_or_error Return value from read() - positive for bytes read, negative for error
/// @param last_data_time Time of last successful read, updated when data received
/// @param timeout_ms Maximum time to wait for data
/// @return DATA if data received, RETRY if should continue loop, ERROR/TIMEOUT if should exit
inline HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_t &last_data_time,
uint32_t timeout_ms) {
if (bytes_read_or_error > 0) {
last_data_time = millis();
return HttpReadLoopResult::DATA;
}
if (bytes_read_or_error < 0) {
return HttpReadLoopResult::ERROR;
}
// bytes_read_or_error == 0: no data available yet
if (millis() - last_data_time >= timeout_ms) {
return HttpReadLoopResult::TIMEOUT;
}
delay(1); // Small delay to prevent tight spinning
return HttpReadLoopResult::RETRY;
}
class HttpRequestComponent;
class HttpContainer : public Parented<HttpRequestComponent> {
@@ -88,6 +163,33 @@ class HttpContainer : public Parented<HttpRequestComponent> {
int status_code;
uint32_t duration_ms;
/**
* @brief Read data from the HTTP response body.
*
* WARNING: These semantics differ from BSD sockets!
* BSD sockets: 0 = EOF (connection closed)
* This method: 0 = no data yet OR all content read, negative = error/closed
*
* @param buf Buffer to read data into
* @param max_len Maximum number of bytes to read
* @return
* - > 0: Number of bytes read successfully
* - 0: No data available yet OR all content read
* (check get_bytes_read() >= content_length to distinguish)
* - HTTP_ERROR_CONNECTION_CLOSED (-1): Connection closed prematurely
* - < -1: Other error (platform-specific error code)
*
* Platform notes:
* - ESP-IDF: blocking read, 0 only when all content read
* - Arduino: non-blocking, 0 can mean "no data yet" or "all content read"
*
* Use get_bytes_read() and content_length to track progress.
* When get_bytes_read() >= content_length, all data has been received.
*
* IMPORTANT: Do not use raw return values directly. Use these helpers:
* - http_read_loop_result(): for loops with per-chunk processing
* - http_read_fully(): for simple "read N bytes" operations
*/
virtual int read(uint8_t *buf, size_t max_len) = 0;
virtual void end() = 0;
@@ -110,6 +212,38 @@ class HttpContainer : public Parented<HttpRequestComponent> {
std::map<std::string, std::list<std::string>> response_headers_{};
};
/// Read data from HTTP container into buffer with timeout handling
/// Handles feed_wdt, yield, and timeout checking internally
/// @param container The HTTP container to read from
/// @param buffer Buffer to read into
/// @param total_size Total bytes to read
/// @param chunk_size Maximum bytes per read call
/// @param timeout_ms Read timeout in milliseconds
/// @return HttpReadResult with status and error_code on failure
inline HttpReadResult http_read_fully(HttpContainer *container, uint8_t *buffer, size_t total_size, size_t chunk_size,
uint32_t timeout_ms) {
size_t read_index = 0;
uint32_t last_data_time = millis();
while (read_index < total_size) {
int read_bytes_or_error = container->read(buffer + read_index, std::min(chunk_size, total_size - read_index));
App.feed_wdt();
yield();
auto result = http_read_loop_result(read_bytes_or_error, last_data_time, timeout_ms);
if (result == HttpReadLoopResult::RETRY)
continue;
if (result == HttpReadLoopResult::ERROR)
return {HttpReadStatus::ERROR, read_bytes_or_error};
if (result == HttpReadLoopResult::TIMEOUT)
return {HttpReadStatus::TIMEOUT, 0};
read_index += read_bytes_or_error;
}
return {HttpReadStatus::OK, 0};
}
class HttpRequestResponseTrigger : public Trigger<std::shared_ptr<HttpContainer>, std::string &> {
public:
void process(const std::shared_ptr<HttpContainer> &container, std::string &response_body) {
@@ -124,6 +258,7 @@ class HttpRequestComponent : public Component {
void set_useragent(const char *useragent) { this->useragent_ = useragent; }
void set_timeout(uint32_t timeout) { this->timeout_ = timeout; }
uint32_t get_timeout() const { return this->timeout_; }
void set_watchdog_timeout(uint32_t watchdog_timeout) { this->watchdog_timeout_ = watchdog_timeout; }
uint32_t get_watchdog_timeout() const { return this->watchdog_timeout_; }
void set_follow_redirects(bool follow_redirects) { this->follow_redirects_ = follow_redirects; }
@@ -249,15 +384,21 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
RAMAllocator<uint8_t> allocator;
uint8_t *buf = allocator.allocate(max_length);
if (buf != nullptr) {
// NOTE: HttpContainer::read() has non-BSD socket semantics - see top of this file
// Use http_read_loop_result() helper instead of checking return values directly
size_t read_index = 0;
uint32_t last_data_time = millis();
const uint32_t read_timeout = this->parent_->get_timeout();
while (container->get_bytes_read() < max_length) {
int read = container->read(buf + read_index, std::min<size_t>(max_length - read_index, 512));
if (read <= 0) {
break;
}
int read_or_error = container->read(buf + read_index, std::min<size_t>(max_length - read_index, 512));
App.feed_wdt();
yield();
read_index += read;
auto result = http_read_loop_result(read_or_error, last_data_time, read_timeout);
if (result == HttpReadLoopResult::RETRY)
continue;
if (result != HttpReadLoopResult::DATA)
break; // ERROR or TIMEOUT
read_index += read_or_error;
}
response_body.reserve(read_index);
response_body.assign((char *) buf, read_index);

View File

@@ -139,6 +139,23 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur
return container;
}
// Arduino HTTP read implementation
//
// WARNING: Return values differ from BSD sockets! See http_request.h for full documentation.
//
// Arduino's WiFiClient is inherently non-blocking - available() returns 0 when
// no data is ready. We use connected() to distinguish "no data yet" from
// "connection closed".
//
// WiFiClient behavior:
// available() > 0: data ready to read
// available() == 0 && connected(): no data yet, still connected
// available() == 0 && !connected(): connection closed
//
// We normalize to HttpContainer::read() contract (NOT BSD socket semantics!):
// > 0: bytes read
// 0: no data yet, retry <-- NOTE: 0 means retry, NOT EOF!
// < 0: error/connection closed <-- connection closed returns -1, not 0
int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
const uint32_t start = millis();
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
@@ -146,7 +163,7 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
WiFiClient *stream_ptr = this->client_.getStreamPtr();
if (stream_ptr == nullptr) {
ESP_LOGE(TAG, "Stream pointer vanished!");
return -1;
return HTTP_ERROR_CONNECTION_CLOSED;
}
int available_data = stream_ptr->available();
@@ -154,7 +171,15 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
if (bufsize == 0) {
this->duration_ms += (millis() - start);
return 0;
// Check if we've read all expected content
if (this->bytes_read_ >= this->content_length) {
return 0; // All content read successfully
}
// No data available - check if connection is still open
if (!stream_ptr->connected()) {
return HTTP_ERROR_CONNECTION_CLOSED; // Connection closed prematurely
}
return 0; // No data yet, caller should retry
}
App.feed_wdt();

View File

@@ -89,7 +89,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
config.max_redirection_count = this->redirect_limit_;
config.auth_type = HTTP_AUTH_TYPE_BASIC;
#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
if (secure) {
if (secure && this->verify_ssl_) {
config.crt_bundle_attach = esp_crt_bundle_attach;
}
#endif
@@ -209,26 +209,57 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
return container;
}
// ESP-IDF HTTP read implementation (blocking mode)
//
// WARNING: Return values differ from BSD sockets! See http_request.h for full documentation.
//
// esp_http_client_read() in blocking mode returns:
// > 0: bytes read
// 0: connection closed (end of stream)
// < 0: error
//
// We normalize to HttpContainer::read() contract:
// > 0: bytes read
// 0: no data yet / all content read (caller should check bytes_read vs content_length)
// < 0: error/connection closed
int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
const uint32_t start = millis();
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
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;
// Check if we've already read all expected content
if (this->bytes_read_ >= this->content_length) {
return 0; // All content read successfully
}
this->feed_wdt();
int read_len_or_error = esp_http_client_read(this->client_, (char *) buf, max_len);
this->feed_wdt();
this->duration_ms += (millis() - start);
return read_len;
if (read_len_or_error > 0) {
this->bytes_read_ += read_len_or_error;
return read_len_or_error;
}
// Connection closed by server before all content received
if (read_len_or_error == 0) {
return HTTP_ERROR_CONNECTION_CLOSED;
}
// Negative value - error, return the actual error code for debugging
return read_len_or_error;
}
void HttpContainerIDF::end() {
if (this->client_ == nullptr) {
return; // Already cleaned up
}
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
esp_http_client_close(this->client_);
esp_http_client_cleanup(this->client_);
this->client_ = nullptr;
}
void HttpContainerIDF::feed_wdt() {

View File

@@ -34,6 +34,7 @@ class HttpRequestIDF : public HttpRequestComponent {
void set_buffer_size_rx(uint16_t buffer_size_rx) { this->buffer_size_rx_ = buffer_size_rx; }
void set_buffer_size_tx(uint16_t buffer_size_tx) { this->buffer_size_tx_ = buffer_size_tx; }
void set_verify_ssl(bool verify_ssl) { this->verify_ssl_ = verify_ssl; }
protected:
std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, const std::string &body,
@@ -42,6 +43,7 @@ class HttpRequestIDF : public HttpRequestComponent {
// if zero ESP-IDF will use DEFAULT_HTTP_BUF_SIZE
uint16_t buffer_size_rx_{};
uint16_t buffer_size_tx_{};
bool verify_ssl_{true};
/// @brief Monitors the http client events to gather response headers
static esp_err_t http_event_handler(esp_http_client_event_t *evt);

View File

@@ -115,39 +115,47 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
return error_code;
}
// NOTE: HttpContainer::read() has non-BSD socket semantics - see http_request.h
// Use http_read_loop_result() helper instead of checking return values directly
uint32_t last_data_time = millis();
const uint32_t read_timeout = this->parent_->get_timeout();
while (container->get_bytes_read() < container->content_length) {
// read a maximum of chunk_size bytes into buf. (real read size returned)
int bufsize = container->read(buf, OtaHttpRequestComponent::HTTP_RECV_BUFFER);
ESP_LOGVV(TAG, "bytes_read_ = %u, body_length_ = %u, bufsize = %i", container->get_bytes_read(),
container->content_length, bufsize);
// read a maximum of chunk_size bytes into buf. (real read size returned, or negative error code)
int bufsize_or_error = container->read(buf, OtaHttpRequestComponent::HTTP_RECV_BUFFER);
ESP_LOGVV(TAG, "bytes_read_ = %u, body_length_ = %u, bufsize_or_error = %i", container->get_bytes_read(),
container->content_length, bufsize_or_error);
// feed watchdog and give other tasks a chance to run
App.feed_wdt();
yield();
// Exit loop if no data available (stream closed or end of data)
if (bufsize <= 0) {
if (bufsize < 0) {
ESP_LOGE(TAG, "Stream closed with error");
this->cleanup_(std::move(backend), container);
return OTA_CONNECTION_ERROR;
auto result = http_read_loop_result(bufsize_or_error, last_data_time, read_timeout);
if (result == HttpReadLoopResult::RETRY)
continue;
if (result != HttpReadLoopResult::DATA) {
if (result == HttpReadLoopResult::TIMEOUT) {
ESP_LOGE(TAG, "Timeout reading data");
} else {
ESP_LOGE(TAG, "Error reading data: %d", bufsize_or_error);
}
// bufsize == 0: no more data available, exit loop
break;
this->cleanup_(std::move(backend), container);
return OTA_CONNECTION_ERROR;
}
if (bufsize <= OtaHttpRequestComponent::HTTP_RECV_BUFFER) {
// At this point bufsize_or_error > 0, so it's a valid size
if (bufsize_or_error <= OtaHttpRequestComponent::HTTP_RECV_BUFFER) {
// add read bytes to MD5
md5_receive.add(buf, bufsize);
md5_receive.add(buf, bufsize_or_error);
// write bytes to OTA backend
this->update_started_ = true;
error_code = backend->write(buf, bufsize);
error_code = backend->write(buf, bufsize_or_error);
if (error_code != ota::OTA_RESPONSE_OK) {
// error code explanation available at
// https://github.com/esphome/esphome/blob/dev/esphome/components/ota/ota_backend.h
ESP_LOGE(TAG, "Error code (%02X) writing binary data to flash at offset %d and size %d", error_code,
container->get_bytes_read() - bufsize, container->content_length);
container->get_bytes_read() - bufsize_or_error, container->content_length);
this->cleanup_(std::move(backend), container);
return error_code;
}
@@ -244,19 +252,19 @@ bool OtaHttpRequestComponent::http_get_md5_() {
}
this->md5_expected_.resize(MD5_SIZE);
int read_len = 0;
while (container->get_bytes_read() < MD5_SIZE) {
read_len = container->read((uint8_t *) this->md5_expected_.data(), MD5_SIZE);
if (read_len <= 0) {
break;
}
App.feed_wdt();
yield();
}
auto result = http_read_fully(container.get(), (uint8_t *) this->md5_expected_.data(), MD5_SIZE, MD5_SIZE,
this->parent_->get_timeout());
container->end();
ESP_LOGV(TAG, "Read len: %u, MD5 expected: %u", read_len, MD5_SIZE);
return read_len == MD5_SIZE;
if (result.status != HttpReadStatus::OK) {
if (result.status == HttpReadStatus::TIMEOUT) {
ESP_LOGE(TAG, "Timeout reading MD5");
} else {
ESP_LOGE(TAG, "Error reading MD5: %d", result.error_code);
}
return false;
}
return true;
}
bool OtaHttpRequestComponent::validate_url_(const std::string &url) {

View File

@@ -11,7 +11,12 @@ namespace http_request {
// The update function runs in a task only on ESP32s.
#ifdef USE_ESP32
#define UPDATE_RETURN vTaskDelete(nullptr) // Delete the current update task
// vTaskDelete doesn't return, but clang-tidy doesn't know that
#define UPDATE_RETURN \
do { \
vTaskDelete(nullptr); \
__builtin_unreachable(); \
} while (0)
#else
#define UPDATE_RETURN return
#endif
@@ -70,19 +75,21 @@ void HttpRequestUpdate::update_task(void *params) {
UPDATE_RETURN;
}
size_t read_index = 0;
while (container->get_bytes_read() < container->content_length) {
int read_bytes = container->read(data + read_index, MAX_READ_SIZE);
yield();
if (read_bytes <= 0) {
// Network error or connection closed - break to avoid infinite loop
break;
auto read_result = http_read_fully(container.get(), data, container->content_length, MAX_READ_SIZE,
this_update->request_parent_->get_timeout());
if (read_result.status != HttpReadStatus::OK) {
if (read_result.status == HttpReadStatus::TIMEOUT) {
ESP_LOGE(TAG, "Timeout reading manifest");
} else {
ESP_LOGE(TAG, "Error reading manifest: %d", read_result.error_code);
}
read_index += read_bytes;
// Defer to main loop to avoid race condition on component_state_ read-modify-write
this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to read manifest")); });
allocator.deallocate(data, container->content_length);
container->end();
UPDATE_RETURN;
}
size_t read_index = container->get_bytes_read();
bool valid = false;
{ // Ensures the response string falls out of scope and deallocates before the task ends

View File

@@ -223,7 +223,7 @@ async def to_code(config):
var = cg.Pvariable(config[CONF_ID], rhs)
await display.register_display(var, config)
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
cg.add(var.set_dc_pin(dc))
if init_sequences := config.get(CONF_INIT_SEQUENCE):

View File

@@ -5,8 +5,6 @@
// Once the API is considered stable, this warning will be removed.
#include "esphome/components/infrared/infrared.h"
#include "esphome/components/remote_transmitter/remote_transmitter.h"
#include "esphome/components/remote_receiver/remote_receiver.h"
namespace esphome::ir_rf_proxy {

View File

@@ -1,3 +1,4 @@
from esphome import codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_OPTIONS
@@ -24,6 +25,34 @@ from .label import CONF_LABEL
CONF_DROPDOWN = "dropdown"
CONF_DROPDOWN_LIST = "dropdown_list"
# Example valid dropdown symbol (left arrow) for error messages
EXAMPLE_DROPDOWN_SYMBOL = "\U00002190" # ←
def dropdown_symbol_validator(value):
"""
Validate that the dropdown symbol is a single Unicode character
with a codepoint of 0x100 (256) or greater.
This is required because LVGL uses codepoints below 0x100 for internal symbols.
"""
value = cv.string(value)
# len(value) counts Unicode code points, not grapheme clusters or bytes
if len(value) != 1:
raise cv.Invalid(
f"Dropdown symbol must be a single character, got '{value}' with length {len(value)}"
)
codepoint = ord(value)
if codepoint < 0x100:
# Format the example symbol as a Unicode escape for the error message
example_escape = f"\\U{ord(EXAMPLE_DROPDOWN_SYMBOL):08X}"
raise cv.Invalid(
f"Dropdown symbol must have a Unicode codepoint of 0x100 (256) or greater. "
f"'{value}' has codepoint {codepoint} (0x{codepoint:X}). "
f"Use a character like '{example_escape}' ({EXAMPLE_DROPDOWN_SYMBOL}) or other Unicode symbols with codepoint >= 0x100."
)
return value
lv_dropdown_t = LvSelect("LvDropdownType", parents=(LvCompound,))
lv_dropdown_list_t = LvType("lv_dropdown_list_t")
@@ -33,7 +62,7 @@ dropdown_list_spec = WidgetType(
DROPDOWN_BASE_SCHEMA = cv.Schema(
{
cv.Optional(CONF_SYMBOL): lv_text,
cv.Optional(CONF_SYMBOL): dropdown_symbol_validator,
cv.Exclusive(CONF_SELECTED_INDEX, CONF_SELECTED_TEXT): lv_int,
cv.Exclusive(CONF_SELECTED_TEXT, CONF_SELECTED_TEXT): lv_text,
cv.Optional(CONF_DROPDOWN_LIST): part_schema(dropdown_list_spec.parts),
@@ -70,7 +99,7 @@ class DropdownType(WidgetType):
if options := config.get(CONF_OPTIONS):
lv_add(w.var.set_options(options))
if symbol := config.get(CONF_SYMBOL):
lv.dropdown_set_symbol(w.var.obj, await lv_text.process(symbol))
lv.dropdown_set_symbol(w.var.obj, cg.safe_exp(symbol))
if (selected := config.get(CONF_SELECTED_INDEX)) is not None:
value = await lv_int.process(selected)
lv_add(w.var.set_selected_index(value, literal("LV_ANIM_OFF")))

View File

@@ -29,7 +29,7 @@ CONFIG_SCHEMA = (
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
await display.register_display(var, config)
cg.add(var.set_num_chips(config[CONF_NUM_CHIPS]))

View File

@@ -86,7 +86,7 @@ CONFIG_SCHEMA = (
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
await display.register_display(var, config)
cg.add(var.set_num_chips(config[CONF_NUM_CHIPS]))

View File

@@ -7,6 +7,7 @@ from esphome.const import (
CONF_CO2,
CONF_ID,
CONF_TEMPERATURE,
CONF_WARMUP_TIME,
DEVICE_CLASS_CARBON_DIOXIDE,
DEVICE_CLASS_TEMPERATURE,
ICON_MOLECULE_CO2,
@@ -18,7 +19,6 @@ from esphome.const import (
DEPENDENCIES = ["uart"]
CONF_AUTOMATIC_BASELINE_CALIBRATION = "automatic_baseline_calibration"
CONF_WARMUP_TIME = "warmup_time"
CONF_DETECTION_RANGE = "detection_range"
mhz19_ns = cg.esphome_ns.namespace("mhz19")

View File

@@ -55,3 +55,44 @@ DriverChip(
(0x35,), (0xFE,),
],
)
DriverChip(
"M5STACK-TAB5-V2",
height=1280,
width=720,
hsync_back_porch=40,
hsync_pulse_width=2,
hsync_front_porch=40,
vsync_back_porch=8,
vsync_pulse_width=2,
vsync_front_porch=220,
pclk_frequency="80MHz",
lane_bit_rate="960Mbps",
swap_xy=cv.UNDEFINED,
color_order="RGB",
initsequence=[
(0x60, 0x71, 0x23, 0xa2),
(0x60, 0x71, 0x23, 0xa3),
(0x60, 0x71, 0x23, 0xa4),
(0xA4, 0x31),
(0xD7, 0x10, 0x0A, 0x10, 0x2A, 0x80, 0x80),
(0x90, 0x71, 0x23, 0x5A, 0x20, 0x24, 0x09, 0x09),
(0xA3, 0x80, 0x01, 0x88, 0x30, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, 0x00, 0x00, 0x1E, 0x5C, 0x1E, 0x80, 0x00, 0x4F, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, 0x00, 0x00, 0x1E, 0x5C, 0x1E, 0x80, 0x00, 0x6F, 0x58, 0x00, 0x00, 0x00, 0xFF),
(0xA6, 0x03, 0x00, 0x24, 0x55, 0x36, 0x00, 0x39, 0x00, 0x6E, 0x6E, 0x91, 0xFF, 0x00, 0x24, 0x55, 0x38, 0x00, 0x37, 0x00, 0x6E, 0x6E, 0x91, 0xFF, 0x00, 0x24, 0x11, 0x00, 0x00, 0x00, 0x00, 0x6E, 0x6E, 0x91, 0xFF, 0x00, 0xEC, 0x11, 0x00, 0x03, 0x00, 0x03, 0x6E, 0x6E, 0xFF, 0xFF, 0x00, 0x08, 0x80, 0x08, 0x80, 0x06, 0x00, 0x00, 0x00, 0x00),
(0xA7, 0x19, 0x19, 0x80, 0x64, 0x40, 0x07, 0x16, 0x40, 0x00, 0x44, 0x03, 0x6E, 0x6E, 0x91, 0xFF, 0x08, 0x80, 0x64, 0x40, 0x25, 0x34, 0x40, 0x00, 0x02, 0x01, 0x6E, 0x6E, 0x91, 0xFF, 0x08, 0x80, 0x64, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x6E, 0x6E, 0x91, 0xFF, 0x08, 0x80, 0x64, 0x40, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x6E, 0x6E, 0x84, 0xFF, 0x08, 0x80, 0x44),
(0xAC, 0x03, 0x19, 0x19, 0x18, 0x18, 0x06, 0x13, 0x13, 0x11, 0x11, 0x08, 0x08, 0x0A, 0x0A, 0x1C, 0x1C, 0x07, 0x07, 0x00, 0x00, 0x02, 0x02, 0x01, 0x19, 0x19, 0x18, 0x18, 0x06, 0x12, 0x12, 0x10, 0x10, 0x09, 0x09, 0x0B, 0x0B, 0x1C, 0x1C, 0x07, 0x07, 0x03, 0x03, 0x01, 0x01),
(0xAD, 0xF0, 0x00, 0x46, 0x00, 0x03, 0x50, 0x50, 0xFF, 0xFF, 0xF0, 0x40, 0x06, 0x01, 0x07, 0x42, 0x42, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF),
(0xAE, 0xFE, 0x3F, 0x3F, 0xFE, 0x3F, 0x3F, 0x00),
(0xB2, 0x15, 0x19, 0x05, 0x23, 0x49, 0xAF, 0x03, 0x2E, 0x5C, 0xD2, 0xFF, 0x10, 0x20, 0xFD, 0x20, 0xC0, 0x00),
(0xE8, 0x20, 0x6F, 0x04, 0x97, 0x97, 0x3E, 0x04, 0xDC, 0xDC, 0x3E, 0x06, 0xFA, 0x26, 0x3E),
(0x75, 0x03, 0x04),
(0xE7, 0x3B, 0x00, 0x00, 0x7C, 0xA1, 0x8C, 0x20, 0x1A, 0xF0, 0xB1, 0x50, 0x00, 0x50, 0xB1, 0x50, 0xB1, 0x50, 0xD8, 0x00, 0x55, 0x00, 0xB1, 0x00, 0x45, 0xC9, 0x6A, 0xFF, 0x5A, 0xD8, 0x18, 0x88, 0x15, 0xB1, 0x01, 0x01, 0x77),
(0xEA, 0x13, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x2C),
(0xB0, 0x22, 0x43, 0x11, 0x61, 0x25, 0x43, 0x43),
(0xb7, 0x00, 0x00, 0x73, 0x73),
(0xBF, 0xA6, 0xAA),
(0xA9, 0x00, 0x00, 0x73, 0xFF, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03),
(0xC8, 0x00, 0x00, 0x10, 0x1F, 0x36, 0x00, 0x5D, 0x04, 0x9D, 0x05, 0x10, 0xF2, 0x06, 0x60, 0x03, 0x11, 0xAD, 0x00, 0xEF, 0x01, 0x22, 0x2E, 0x0E, 0x74, 0x08, 0x32, 0xDC, 0x09, 0x33, 0x0F, 0xF3, 0x77, 0x0D, 0xB0, 0xDC, 0x03, 0xFF),
(0xC9, 0x00, 0x00, 0x10, 0x1F, 0x36, 0x00, 0x5D, 0x04, 0x9D, 0x05, 0x10, 0xF2, 0x06, 0x60, 0x03, 0x11, 0xAD, 0x00, 0xEF, 0x01, 0x22, 0x2E, 0x0E, 0x74, 0x08, 0x32, 0xDC, 0x09, 0x33, 0x0F, 0xF3, 0x77, 0x0D, 0xB0, 0xDC, 0x03, 0xFF),
],
)

View File

@@ -260,7 +260,7 @@ async def to_code(config):
cg.add(var.set_enable_pins(enable))
if CONF_SPI_ID in config:
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
sequence, madctl = model.get_sequence(config)
cg.add(var.set_init_sequence(sequence))
cg.add(var.set_madctl(madctl))

View File

@@ -1,9 +1,11 @@
#ifdef USE_ESP32_VARIANT_ESP32S3
#include "mipi_rgb.h"
#include "esphome/core/gpio.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include "esp_lcd_panel_rgb.h"
#include <span>
namespace esphome {
namespace mipi_rgb {
@@ -343,19 +345,27 @@ int MipiRgb::get_height() {
}
}
static std::string get_pin_name(GPIOPin *pin) {
static const char *get_pin_name(GPIOPin *pin, std::span<char, GPIO_SUMMARY_MAX_LEN> buffer) {
if (pin == nullptr)
return "None";
return pin->dump_summary();
pin->dump_summary(buffer.data(), buffer.size());
return buffer.data();
}
void MipiRgb::dump_pins_(uint8_t start, uint8_t end, const char *name, uint8_t offset) {
char pin_summary[GPIO_SUMMARY_MAX_LEN];
for (uint8_t i = start; i != end; i++) {
ESP_LOGCONFIG(TAG, " %s pin %d: %s", name, offset++, this->data_pins_[i]->dump_summary().c_str());
this->data_pins_[i]->dump_summary(pin_summary, sizeof(pin_summary));
ESP_LOGCONFIG(TAG, " %s pin %d: %s", name, offset++, pin_summary);
}
}
void MipiRgb::dump_config() {
char reset_buf[GPIO_SUMMARY_MAX_LEN];
char de_buf[GPIO_SUMMARY_MAX_LEN];
char pclk_buf[GPIO_SUMMARY_MAX_LEN];
char hsync_buf[GPIO_SUMMARY_MAX_LEN];
char vsync_buf[GPIO_SUMMARY_MAX_LEN];
ESP_LOGCONFIG(TAG,
"MIPI_RGB LCD"
"\n Model: %s"
@@ -379,9 +389,9 @@ void MipiRgb::dump_config() {
this->model_, this->width_, this->height_, this->rotation_, YESNO(this->pclk_inverted_),
this->hsync_pulse_width_, this->hsync_back_porch_, this->hsync_front_porch_, this->vsync_pulse_width_,
this->vsync_back_porch_, this->vsync_front_porch_, YESNO(this->invert_colors_),
(unsigned) (this->pclk_frequency_ / 1000000), get_pin_name(this->reset_pin_).c_str(),
get_pin_name(this->de_pin_).c_str(), get_pin_name(this->pclk_pin_).c_str(),
get_pin_name(this->hsync_pin_).c_str(), get_pin_name(this->vsync_pin_).c_str());
(unsigned) (this->pclk_frequency_ / 1000000), get_pin_name(this->reset_pin_, reset_buf),
get_pin_name(this->de_pin_, de_buf), get_pin_name(this->pclk_pin_, pclk_buf),
get_pin_name(this->hsync_pin_, hsync_buf), get_pin_name(this->vsync_pin_, vsync_buf));
this->dump_pins_(8, 13, "Blue", 0);
this->dump_pins_(13, 16, "Green", 0);

View File

@@ -443,6 +443,4 @@ async def to_code(config):
)
cg.add(var.set_writer(lambda_))
await display.register_display(var, config)
await spi.register_spi_device(var, config)
# Displays are write-only, set the SPI device to write-only as well
cg.add(var.set_write_only(True))
await spi.register_spi_device(var, config, write_only=True)

View File

@@ -18,7 +18,7 @@ bool CustomMQTTDevice::publish(const std::string &topic, float value, int8_t num
}
bool CustomMQTTDevice::publish(const std::string &topic, int value) {
char buffer[24];
int len = snprintf(buffer, sizeof(buffer), "%d", value);
size_t len = buf_append_printf(buffer, sizeof(buffer), 0, "%d", value);
return global_mqtt_client->publish(topic, buffer, len);
}
bool CustomMQTTDevice::publish_json(const std::string &topic, const json::json_build_t &f, uint8_t qos, bool retain) {

View File

@@ -43,7 +43,7 @@ void MQTTAlarmControlPanelComponent::setup() {
void MQTTAlarmControlPanelComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT alarm_control_panel '%s':", this->alarm_control_panel_->get_name().c_str());
LOG_MQTT_COMPONENT(true, true)
LOG_MQTT_COMPONENT(true, true);
ESP_LOGCONFIG(TAG,
" Supported Features: %" PRIu32 "\n"
" Requires Code to Disarm: %s\n"

View File

@@ -19,7 +19,7 @@ void MQTTBinarySensorComponent::setup() {
void MQTTBinarySensorComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT Binary Sensor '%s':", this->binary_sensor_->get_name().c_str());
LOG_MQTT_COMPONENT(true, false)
LOG_MQTT_COMPONENT(true, false);
}
MQTTBinarySensorComponent::MQTTBinarySensorComponent(binary_sensor::BinarySensor *binary_sensor)
: binary_sensor_(binary_sensor) {

View File

@@ -98,7 +98,17 @@ void MQTTClientComponent::send_device_info_() {
uint8_t index = 0;
for (auto &ip : network::get_ip_addresses()) {
if (ip.is_set()) {
root["ip" + (index == 0 ? "" : esphome::to_string(index))] = ip.str();
char key[8]; // "ip" + up to 3 digits + null
char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
if (index == 0) {
key[0] = 'i';
key[1] = 'p';
key[2] = '\0';
} else {
buf_append_printf(key, sizeof(key), 0, "ip%u", index);
}
ip.str_to(ip_buf);
root[key] = ip_buf;
index++;
}
}

View File

@@ -27,20 +27,23 @@ inline char *append_char(char *p, char c) {
// Max lengths for stack-based topic building.
// These limits are enforced at Python config validation time in mqtt/__init__.py
// using cv.Length() validators for topic_prefix and discovery_prefix.
// MQTT_COMPONENT_TYPE_MAX_LEN and MQTT_SUFFIX_MAX_LEN are defined in mqtt_component.h.
// MQTT_COMPONENT_TYPE_MAX_LEN, MQTT_SUFFIX_MAX_LEN, and MQTT_DEFAULT_TOPIC_MAX_LEN are in mqtt_component.h.
// ESPHOME_DEVICE_NAME_MAX_LEN and OBJECT_ID_MAX_LEN are defined in entity_base.h.
// This ensures the stack buffers below are always large enough.
static constexpr size_t TOPIC_PREFIX_MAX_LEN = 64; // Validated in Python: cv.Length(max=64)
static constexpr size_t DISCOVERY_PREFIX_MAX_LEN = 64; // Validated in Python: cv.Length(max=64)
// Stack buffer sizes - safe because all inputs are length-validated at config time
// Format: prefix + "/" + type + "/" + object_id + "/" + suffix + null
static constexpr size_t DEFAULT_TOPIC_MAX_LEN =
TOPIC_PREFIX_MAX_LEN + 1 + MQTT_COMPONENT_TYPE_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 1 + MQTT_SUFFIX_MAX_LEN + 1;
// Format: prefix + "/" + type + "/" + name + "/" + object_id + "/config" + null
static constexpr size_t DISCOVERY_TOPIC_MAX_LEN = DISCOVERY_PREFIX_MAX_LEN + 1 + MQTT_COMPONENT_TYPE_MAX_LEN + 1 +
ESPHOME_DEVICE_NAME_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 7 + 1;
// Function implementation of LOG_MQTT_COMPONENT macro to reduce code size
void log_mqtt_component(const char *tag, MQTTComponent *obj, bool state_topic, bool command_topic) {
char buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
if (state_topic)
ESP_LOGCONFIG(tag, " State Topic: '%s'", obj->get_state_topic_to_(buf).c_str());
if (command_topic)
ESP_LOGCONFIG(tag, " Command Topic: '%s'", obj->get_command_topic_to_(buf).c_str());
}
void MQTTComponent::set_qos(uint8_t qos) { this->qos_ = qos; }
void MQTTComponent::set_subscribe_qos(uint8_t qos) { this->subscribe_qos_ = qos; }
@@ -69,19 +72,18 @@ std::string MQTTComponent::get_discovery_topic_(const MQTTDiscoveryInfo &discove
return std::string(buf, p - buf);
}
std::string MQTTComponent::get_default_topic_for_(const std::string &suffix) const {
StringRef MQTTComponent::get_default_topic_for_to_(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf, const char *suffix,
size_t suffix_len) const {
const std::string &topic_prefix = global_mqtt_client->get_topic_prefix();
if (topic_prefix.empty()) {
// If the topic_prefix is null, the default topic should be null
return "";
return StringRef(); // Empty topic_prefix means no default topic
}
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);
char buf[DEFAULT_TOPIC_MAX_LEN];
char *p = buf;
char *p = buf.data();
p = append_str(p, topic_prefix.data(), topic_prefix.size());
p = append_char(p, '/');
@@ -89,21 +91,44 @@ std::string MQTTComponent::get_default_topic_for_(const std::string &suffix) con
p = append_char(p, '/');
p = append_str(p, object_id.c_str(), object_id.size());
p = append_char(p, '/');
p = append_str(p, suffix.data(), suffix.size());
p = append_str(p, suffix, suffix_len);
*p = '\0';
return std::string(buf, p - buf);
return StringRef(buf.data(), p - buf.data());
}
std::string MQTTComponent::get_default_topic_for_(const std::string &suffix) const {
char buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
StringRef ref = this->get_default_topic_for_to_(buf, suffix.data(), suffix.size());
return std::string(ref.c_str(), ref.size());
}
StringRef MQTTComponent::get_state_topic_to_(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf) const {
if (this->custom_state_topic_.has_value()) {
// Returns ref to existing data for static/value, uses buf only for lambda case
return this->custom_state_topic_.ref_or_copy_to(buf.data(), buf.size());
}
return this->get_default_topic_for_to_(buf, "state", 5);
}
StringRef MQTTComponent::get_command_topic_to_(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf) const {
if (this->custom_command_topic_.has_value()) {
// Returns ref to existing data for static/value, uses buf only for lambda case
return this->custom_command_topic_.ref_or_copy_to(buf.data(), buf.size());
}
return this->get_default_topic_for_to_(buf, "command", 7);
}
std::string MQTTComponent::get_state_topic_() const {
if (this->custom_state_topic_.has_value())
return this->custom_state_topic_.value();
return this->get_default_topic_for_("state");
char buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
StringRef ref = this->get_state_topic_to_(buf);
return std::string(ref.c_str(), ref.size());
}
std::string MQTTComponent::get_command_topic_() const {
if (this->custom_command_topic_.has_value())
return this->custom_command_topic_.value();
return this->get_default_topic_for_("command");
char buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
StringRef ref = this->get_command_topic_to_(buf);
return std::string(ref.c_str(), ref.size());
}
bool MQTTComponent::publish(const std::string &topic, const std::string &payload) {
@@ -168,10 +193,14 @@ bool MQTTComponent::send_discovery_() {
break;
}
if (config.state_topic)
root[MQTT_STATE_TOPIC] = this->get_state_topic_();
if (config.command_topic)
root[MQTT_COMMAND_TOPIC] = this->get_command_topic_();
if (config.state_topic) {
char state_topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
root[MQTT_STATE_TOPIC] = this->get_state_topic_to_(state_topic_buf);
}
if (config.command_topic) {
char command_topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
root[MQTT_COMMAND_TOPIC] = this->get_command_topic_to_(command_topic_buf);
}
if (this->command_retain_)
root[MQTT_COMMAND_RETAIN] = true;
@@ -190,27 +219,37 @@ 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];
snprintf(friendly_name_hash, sizeof(friendly_name_hash), "%08" PRIx32, fnv1_hash(this->friendly_name_()));
buf_append_printf(friendly_name_hash, sizeof(friendly_name_hash), 0, "%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];
char mac_buf[MAC_ADDRESS_BUFFER_SIZE];
get_mac_address_into_buffer(mac_buf);
snprintf(unique_id, sizeof(unique_id), "%s-%s-%s", mac_buf, this->component_type(), friendly_name_hash);
buf_append_printf(unique_id, sizeof(unique_id), 0, "%s-%s-%s", mac_buf, this->component_type(),
friendly_name_hash);
root[MQTT_UNIQUE_ID] = unique_id;
} else {
// default to almost-unique ID. It's a hack but the only way to get that
// gorgeous device registry view.
root[MQTT_UNIQUE_ID] = "ESP" + std::string(this->component_type()) + object_id.c_str();
// "ESP" (3) + component_type (max 20) + object_id (max 128) + null
char unique_id_buf[3 + MQTT_COMPONENT_TYPE_MAX_LEN + OBJECT_ID_MAX_LEN + 1];
buf_append_printf(unique_id_buf, sizeof(unique_id_buf), 0, "ESP%s%s", this->component_type(),
object_id.c_str());
root[MQTT_UNIQUE_ID] = unique_id_buf;
}
const std::string &node_name = App.get_name();
if (discovery_info.object_id_generator == MQTT_DEVICE_NAME_OBJECT_ID_GENERATOR)
root[MQTT_OBJECT_ID] = node_name + "_" + object_id.c_str();
if (discovery_info.object_id_generator == MQTT_DEVICE_NAME_OBJECT_ID_GENERATOR) {
// node_name (max 31) + "_" (1) + object_id (max 128) + null
char object_id_full[ESPHOME_DEVICE_NAME_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 1];
buf_append_printf(object_id_full, sizeof(object_id_full), 0, "%s_%s", node_name.c_str(), object_id.c_str());
root[MQTT_OBJECT_ID] = object_id_full;
}
const std::string &friendly_name_ref = App.get_friendly_name();
const std::string &node_friendly_name = friendly_name_ref.empty() ? node_name : friendly_name_ref;
std::string node_area = App.get_area();
const char *node_area = App.get_area();
JsonObject device_info = root[MQTT_DEVICE].to<JsonObject>();
char mac[MAC_ADDRESS_BUFFER_SIZE];
@@ -221,18 +260,29 @@ bool MQTTComponent::send_discovery_() {
device_info[MQTT_DEVICE_SW_VERSION] = ESPHOME_PROJECT_VERSION " (ESPHome " ESPHOME_VERSION ")";
const char *model = std::strchr(ESPHOME_PROJECT_NAME, '.');
device_info[MQTT_DEVICE_MODEL] = model == nullptr ? ESPHOME_BOARD : model + 1;
device_info[MQTT_DEVICE_MANUFACTURER] =
model == nullptr ? ESPHOME_PROJECT_NAME : std::string(ESPHOME_PROJECT_NAME, model - ESPHOME_PROJECT_NAME);
if (model == nullptr) {
device_info[MQTT_DEVICE_MANUFACTURER] = ESPHOME_PROJECT_NAME;
} else {
// Extract manufacturer (part before '.') using stack buffer to avoid heap allocation
// memcpy is used instead of strncpy since we know the exact length and strncpy
// would still require manual null-termination
char manufacturer[sizeof(ESPHOME_PROJECT_NAME)];
size_t len = model - ESPHOME_PROJECT_NAME;
memcpy(manufacturer, ESPHOME_PROJECT_NAME, len);
manufacturer[len] = '\0';
device_info[MQTT_DEVICE_MANUFACTURER] = manufacturer;
}
#else
static const char ver_fmt[] PROGMEM = ESPHOME_VERSION " (config hash 0x%08" PRIx32 ")";
// Buffer sized for format string expansion: ~4 bytes net growth from format specifier to 8 hex digits, plus
// safety margin
char version_buf[sizeof(ver_fmt) + 8];
#ifdef USE_ESP8266
char fmt_buf[sizeof(ver_fmt)];
strcpy_P(fmt_buf, ver_fmt);
const char *fmt = fmt_buf;
snprintf_P(version_buf, sizeof(version_buf), ver_fmt, App.get_config_hash());
#else
const char *fmt = ver_fmt;
snprintf(version_buf, sizeof(version_buf), ver_fmt, App.get_config_hash());
#endif
device_info[MQTT_DEVICE_SW_VERSION] = str_sprintf(fmt, App.get_config_hash());
device_info[MQTT_DEVICE_SW_VERSION] = version_buf;
device_info[MQTT_DEVICE_MODEL] = ESPHOME_BOARD;
#if defined(USE_ESP8266) || defined(USE_ESP32)
device_info[MQTT_DEVICE_MANUFACTURER] = "Espressif";
@@ -246,7 +296,7 @@ bool MQTTComponent::send_discovery_() {
device_info[MQTT_DEVICE_MANUFACTURER] = "Host";
#endif
#endif
if (!node_area.empty()) {
if (node_area[0] != '\0') {
device_info[MQTT_DEVICE_SUGGESTED_AREA] = node_area;
}
@@ -288,7 +338,9 @@ void MQTTComponent::set_availability(std::string topic, std::string payload_avai
}
void MQTTComponent::disable_availability() { this->set_availability("", "", ""); }
void MQTTComponent::call_setup() {
if (this->is_internal())
// Cache is_internal result once during setup - topics don't change after this
this->is_internal_ = this->compute_is_internal_();
if (this->is_internal_)
return;
this->setup();
@@ -340,26 +392,28 @@ StringRef MQTTComponent::get_default_object_id_to_(std::span<char, OBJECT_ID_MAX
}
StringRef MQTTComponent::get_icon_ref_() const { return this->get_entity()->get_icon_ref(); }
bool MQTTComponent::is_disabled_by_default_() const { return this->get_entity()->is_disabled_by_default(); }
bool MQTTComponent::is_internal() {
bool MQTTComponent::compute_is_internal_() {
if (this->custom_state_topic_.has_value()) {
// If the custom state_topic is null, return true as it is internal and should not publish
// If the custom state_topic is empty, return true as it is internal and should not publish
// else, return false, as it is explicitly set to a topic, so it is not internal and should publish
return this->get_state_topic_().empty();
// Using is_empty() avoids heap allocation for non-lambda cases
return this->custom_state_topic_.is_empty();
}
if (this->custom_command_topic_.has_value()) {
// If the custom command_topic is null, return true as it is internal and should not publish
// If the custom command_topic is empty, return true as it is internal and should not publish
// else, return false, as it is explicitly set to a topic, so it is not internal and should publish
return this->get_command_topic_().empty();
// Using is_empty() avoids heap allocation for non-lambda cases
return this->custom_command_topic_.is_empty();
}
// No custom topics have been set
if (this->get_default_topic_for_("").empty()) {
// If the default topic prefix is null, then the component, by default, is internal and should not publish
// No custom topics have been set - check topic_prefix directly to avoid allocation
if (global_mqtt_client->get_topic_prefix().empty()) {
// If the default topic prefix is empty, then the component, by default, is internal and should not publish
return true;
}
// Use ESPHome's component internal state if topic_prefix is not null with no custom state_topic or command_topic
// Use ESPHome's component internal state if topic_prefix is not empty with no custom state_topic or command_topic
return this->get_entity()->is_internal();
}

View File

@@ -20,17 +20,22 @@ struct SendDiscoveryConfig {
bool command_topic{true}; ///< If the command topic should be included. Default to true.
};
// Max lengths for stack-based topic building (must match mqtt_component.cpp)
// Max lengths for stack-based topic building.
// These limits are enforced at Python config validation time in mqtt/__init__.py
// using cv.Length() validators for topic_prefix and discovery_prefix.
// This ensures the stack buffers are always large enough.
static constexpr size_t MQTT_COMPONENT_TYPE_MAX_LEN = 20;
static constexpr size_t MQTT_SUFFIX_MAX_LEN = 32;
static constexpr size_t MQTT_TOPIC_PREFIX_MAX_LEN = 64; // Validated in Python: cv.Length(max=64)
// Stack buffer size - safe because all inputs are length-validated at config time
// Format: prefix + "/" + type + "/" + object_id + "/" + suffix + null
static constexpr size_t MQTT_DEFAULT_TOPIC_MAX_LEN =
MQTT_TOPIC_PREFIX_MAX_LEN + 1 + MQTT_COMPONENT_TYPE_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 1 + MQTT_SUFFIX_MAX_LEN + 1;
#define LOG_MQTT_COMPONENT(state_topic, command_topic) \
if (state_topic) { \
ESP_LOGCONFIG(TAG, " State Topic: '%s'", this->get_state_topic_().c_str()); \
} \
if (command_topic) { \
ESP_LOGCONFIG(TAG, " Command Topic: '%s'", this->get_command_topic_().c_str()); \
}
class MQTTComponent; // Forward declaration
void log_mqtt_component(const char *tag, MQTTComponent *obj, bool state_topic, bool command_topic);
#define LOG_MQTT_COMPONENT(state_topic, command_topic) log_mqtt_component(TAG, this, state_topic, command_topic)
// Macro to define component_type() with compile-time length verification
// Usage: MQTT_COMPONENT_TYPE(MQTTSensorComponent, "sensor")
@@ -74,6 +79,8 @@ static constexpr size_t MQTT_SUFFIX_MAX_LEN = 32;
* a clean separation.
*/
class MQTTComponent : public Component {
friend void log_mqtt_component(const char *tag, MQTTComponent *obj, bool state_topic, bool command_topic);
public:
/// Constructs a MQTTComponent.
explicit MQTTComponent();
@@ -88,7 +95,8 @@ class MQTTComponent : public Component {
virtual bool send_initial_state() = 0;
virtual bool is_internal();
/// Returns cached is_internal result (computed once during setup).
bool is_internal() const { return this->is_internal_; }
/// Set QOS for state messages.
void set_qos(uint8_t qos);
@@ -179,7 +187,16 @@ class MQTTComponent : public Component {
/// Helper method to get the discovery topic for this component.
std::string get_discovery_topic_(const MQTTDiscoveryInfo &discovery_info) const;
/** Get this components state/command/... topic.
/** Get this components state/command/... topic into a buffer.
*
* @param buf The buffer to write to (must be exactly MQTT_DEFAULT_TOPIC_MAX_LEN).
* @param suffix The suffix/key such as "state" or "command".
* @return StringRef pointing to the buffer with the topic.
*/
StringRef get_default_topic_for_to_(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf, const char *suffix,
size_t suffix_len) const;
/** Get this components state/command/... topic (allocates std::string).
*
* @param suffix The suffix/key such as "state" or "command".
* @return The full topic.
@@ -200,10 +217,20 @@ class MQTTComponent : public Component {
/// Get whether the underlying Entity is disabled by default
bool is_disabled_by_default_() const;
/// Get the MQTT topic that new states will be shared to.
/// Get the MQTT state topic into a buffer (no heap allocation for non-lambda custom topics).
/// @param buf Buffer of exactly MQTT_DEFAULT_TOPIC_MAX_LEN bytes.
/// @return StringRef pointing to the topic in the buffer.
StringRef get_state_topic_to_(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf) const;
/// Get the MQTT command topic into a buffer (no heap allocation for non-lambda custom topics).
/// @param buf Buffer of exactly MQTT_DEFAULT_TOPIC_MAX_LEN bytes.
/// @return StringRef pointing to the topic in the buffer.
StringRef get_command_topic_to_(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf) const;
/// Get the MQTT topic that new states will be shared to (allocates std::string).
std::string get_state_topic_() const;
/// Get the MQTT topic for listening to commands.
/// Get the MQTT topic for listening to commands (allocates std::string).
std::string get_command_topic_() const;
bool is_connected_() const;
@@ -221,12 +248,18 @@ class MQTTComponent : public Component {
std::unique_ptr<Availability> availability_;
bool command_retain_{false};
bool retain_{true};
uint8_t qos_{0};
uint8_t subscribe_qos_{0};
bool discovery_enabled_{true};
bool resend_state_{false};
// Packed bitfields - QoS values are 0-2, bools are flags
uint8_t qos_ : 2 {0};
uint8_t subscribe_qos_ : 2 {0};
bool command_retain_ : 1 {false};
bool retain_ : 1 {true};
bool discovery_enabled_ : 1 {true};
bool resend_state_ : 1 {false};
bool is_internal_ : 1 {false}; ///< Cached result of compute_is_internal_(), set during setup
/// Compute is_internal status based on topics and entity state.
/// Called once during setup to cache the result.
bool compute_is_internal_();
};
} // namespace esphome::mqtt

View File

@@ -51,7 +51,7 @@ void MQTTCoverComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT cover '%s':", this->cover_->get_name().c_str());
auto traits = this->cover_->get_traits();
bool has_command_topic = traits.get_supports_position() || !traits.get_supports_tilt();
LOG_MQTT_COMPONENT(true, has_command_topic)
LOG_MQTT_COMPONENT(true, has_command_topic);
if (traits.get_supports_position()) {
ESP_LOGCONFIG(TAG,
" Position State Topic: '%s'\n"

View File

@@ -36,7 +36,7 @@ void MQTTDateComponent::setup() {
void MQTTDateComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT Date '%s':", this->date_->get_name().c_str());
LOG_MQTT_COMPONENT(true, true)
LOG_MQTT_COMPONENT(true, true);
}
MQTT_COMPONENT_TYPE(MQTTDateComponent, "date")

View File

@@ -47,7 +47,7 @@ void MQTTDateTimeComponent::setup() {
void MQTTDateTimeComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT DateTime '%s':", this->datetime_->get_name().c_str());
LOG_MQTT_COMPONENT(true, true)
LOG_MQTT_COMPONENT(true, true);
}
MQTT_COMPONENT_TYPE(MQTTDateTimeComponent, "datetime")

View File

@@ -175,7 +175,7 @@ bool MQTTFanComponent::publish_state() {
auto traits = this->state_->get_traits();
if (traits.supports_speed()) {
char buf[12];
int len = snprintf(buf, sizeof(buf), "%d", this->state_->speed);
size_t len = buf_append_printf(buf, sizeof(buf), 0, "%d", this->state_->speed);
bool success = this->publish(this->get_speed_level_state_topic(), buf, len);
failed = failed || !success;
}

View File

@@ -90,7 +90,7 @@ void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscovery
bool MQTTJSONLightComponent::send_initial_state() { return this->publish_state_(); }
void MQTTJSONLightComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT Light '%s':", this->state_->get_name().c_str());
LOG_MQTT_COMPONENT(true, true)
LOG_MQTT_COMPONENT(true, true);
}
} // namespace esphome::mqtt

View File

@@ -30,7 +30,7 @@ void MQTTNumberComponent::setup() {
void MQTTNumberComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT Number '%s':", this->number_->get_name().c_str());
LOG_MQTT_COMPONENT(true, false)
LOG_MQTT_COMPONENT(true, false);
}
MQTT_COMPONENT_TYPE(MQTTNumberComponent, "number")
@@ -75,7 +75,7 @@ bool MQTTNumberComponent::send_initial_state() {
}
bool MQTTNumberComponent::publish_state(float value) {
char buffer[64];
snprintf(buffer, sizeof(buffer), "%f", value);
buf_append_printf(buffer, sizeof(buffer), 0, "%f", value);
return this->publish(this->get_state_topic_(), buffer);
}

View File

@@ -25,7 +25,7 @@ void MQTTSelectComponent::setup() {
void MQTTSelectComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT Select '%s':", this->select_->get_name().c_str());
LOG_MQTT_COMPONENT(true, false)
LOG_MQTT_COMPONENT(true, false);
}
MQTT_COMPONENT_TYPE(MQTTSelectComponent, "select")

View File

@@ -28,7 +28,7 @@ void MQTTSensorComponent::dump_config() {
if (this->get_expire_after() > 0) {
ESP_LOGCONFIG(TAG, " Expire After: %" PRIu32 "s", this->get_expire_after() / 1000);
}
LOG_MQTT_COMPONENT(true, false)
LOG_MQTT_COMPONENT(true, false);
}
MQTT_COMPONENT_TYPE(MQTTSensorComponent, "sensor")

View File

@@ -26,7 +26,7 @@ void MQTTTextComponent::setup() {
void MQTTTextComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT text '%s':", this->text_->get_name().c_str());
LOG_MQTT_COMPONENT(true, true)
LOG_MQTT_COMPONENT(true, true);
}
MQTT_COMPONENT_TYPE(MQTTTextComponent, "text")

View File

@@ -36,7 +36,7 @@ void MQTTTimeComponent::setup() {
void MQTTTimeComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT Time '%s':", this->time_->get_name().c_str());
LOG_MQTT_COMPONENT(true, true)
LOG_MQTT_COMPONENT(true, true);
}
MQTT_COMPONENT_TYPE(MQTTTimeComponent, "time")

View File

@@ -39,7 +39,7 @@ void MQTTValveComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT valve '%s':", this->valve_->get_name().c_str());
auto traits = this->valve_->get_traits();
bool has_command_topic = traits.get_supports_position();
LOG_MQTT_COMPONENT(true, has_command_topic)
LOG_MQTT_COMPONENT(true, has_command_topic);
if (traits.get_supports_position()) {
ESP_LOGCONFIG(TAG,
" Position State Topic: '%s'\n"

View File

@@ -16,6 +16,7 @@ CONF_EXIT_REPARSE_ON_START = "exit_reparse_on_start"
CONF_FONT_ID = "font_id"
CONF_FOREGROUND_PRESSED_COLOR = "foreground_pressed_color"
CONF_MAX_COMMANDS_PER_LOOP = "max_commands_per_loop"
CONF_MAX_QUEUE_AGE = "max_queue_age"
CONF_MAX_QUEUE_SIZE = "max_queue_size"
CONF_ON_BUFFER_OVERFLOW = "on_buffer_overflow"
CONF_ON_PAGE = "on_page"
@@ -25,6 +26,7 @@ CONF_ON_WAKE = "on_wake"
CONF_PRECISION = "precision"
CONF_SKIP_CONNECTION_HANDSHAKE = "skip_connection_handshake"
CONF_START_UP_PAGE = "start_up_page"
CONF_STARTUP_OVERRIDE_MS = "startup_override_ms"
CONF_TFT_URL = "tft_url"
CONF_TOUCH_SLEEP_TIMEOUT = "touch_sleep_timeout"
CONF_VARIABLE_NAME = "variable_name"

View File

@@ -23,6 +23,7 @@ from .base_component import (
CONF_DUMP_DEVICE_INFO,
CONF_EXIT_REPARSE_ON_START,
CONF_MAX_COMMANDS_PER_LOOP,
CONF_MAX_QUEUE_AGE,
CONF_MAX_QUEUE_SIZE,
CONF_ON_BUFFER_OVERFLOW,
CONF_ON_PAGE,
@@ -31,6 +32,7 @@ from .base_component import (
CONF_ON_WAKE,
CONF_SKIP_CONNECTION_HANDSHAKE,
CONF_START_UP_PAGE,
CONF_STARTUP_OVERRIDE_MS,
CONF_TFT_URL,
CONF_TOUCH_SLEEP_TIMEOUT,
CONF_WAKE_UP_PAGE,
@@ -65,6 +67,12 @@ CONFIG_SCHEMA = (
),
cv.Optional(CONF_DUMP_DEVICE_INFO, default=False): cv.boolean,
cv.Optional(CONF_EXIT_REPARSE_ON_START, default=False): cv.boolean,
cv.Optional(CONF_MAX_QUEUE_AGE, default="8000ms"): cv.All(
cv.positive_time_period_milliseconds,
cv.Range(
min=TimePeriod(milliseconds=0), max=TimePeriod(milliseconds=65535)
),
),
cv.Optional(CONF_MAX_COMMANDS_PER_LOOP): cv.uint16_t,
cv.Optional(CONF_MAX_QUEUE_SIZE): cv.positive_int,
cv.Optional(CONF_ON_BUFFER_OVERFLOW): automation.validate_automation(
@@ -100,6 +108,12 @@ CONFIG_SCHEMA = (
}
),
cv.Optional(CONF_SKIP_CONNECTION_HANDSHAKE, default=False): cv.boolean,
cv.Optional(CONF_STARTUP_OVERRIDE_MS, default="8000ms"): cv.All(
cv.positive_time_period_milliseconds,
cv.Range(
min=TimePeriod(milliseconds=0), max=TimePeriod(milliseconds=65535)
),
),
cv.Optional(CONF_START_UP_PAGE): cv.uint8_t,
cv.Optional(CONF_TFT_URL): cv.url,
cv.Optional(CONF_TOUCH_SLEEP_TIMEOUT): cv.Any(
@@ -138,6 +152,8 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await uart.register_uart_device(var, config)
cg.add(var.set_max_queue_age(config[CONF_MAX_QUEUE_AGE]))
if max_queue_size := config.get(CONF_MAX_QUEUE_SIZE):
cg.add_define("USE_NEXTION_MAX_QUEUE_SIZE")
cg.add(var.set_max_queue_size(max_queue_size))
@@ -146,6 +162,8 @@ async def to_code(config):
cg.add_define("USE_NEXTION_COMMAND_SPACING")
cg.add(var.set_command_spacing(command_spacing.total_milliseconds))
cg.add(var.set_startup_override_ms(config[CONF_STARTUP_OVERRIDE_MS]))
if CONF_BRIGHTNESS in config:
cg.add(var.set_brightness(config[CONF_BRIGHTNESS]))

View File

@@ -152,21 +152,25 @@ void Nextion::dump_config() {
#else // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE
ESP_LOGCONFIG(TAG,
#ifdef USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
" Device Model: %s\n"
" FW Version: %s\n"
" Serial Number: %s\n"
" Flash Size: %s\n"
" Device Model: %s\n"
" FW Version: %s\n"
" Serial Number: %s\n"
" Flash Size: %s\n"
" Max queue age: %u ms\n"
" Startup override: %u ms\n",
#endif // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
#ifdef USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START
" Exit reparse: YES\n"
" Exit reparse: YES\n"
#endif // USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START
" Wake On Touch: %s\n"
" Touch Timeout: %" PRIu16,
" Wake On Touch: %s\n"
" Touch Timeout: %" PRIu16,
#ifdef USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
this->device_model_.c_str(), this->firmware_version_.c_str(), this->serial_number_.c_str(),
this->flash_size_.c_str(),
this->flash_size_.c_str(), this->max_q_age_ms_,
this->startup_override_ms_
#endif // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
YESNO(this->connection_state_.auto_wake_on_touch_), this->touch_sleep_timeout_);
YESNO(this->connection_state_.auto_wake_on_touch_),
this->touch_sleep_timeout_);
#endif // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE
#ifdef USE_NEXTION_MAX_COMMANDS_PER_LOOP
@@ -174,21 +178,21 @@ void Nextion::dump_config() {
#endif // USE_NEXTION_MAX_COMMANDS_PER_LOOP
if (this->wake_up_page_ != 255) {
ESP_LOGCONFIG(TAG, " Wake Up Page: %u", this->wake_up_page_);
ESP_LOGCONFIG(TAG, " Wake Up Page: %u", this->wake_up_page_);
}
#ifdef USE_NEXTION_CONF_START_UP_PAGE
if (this->start_up_page_ != 255) {
ESP_LOGCONFIG(TAG, " Start Up Page: %u", this->start_up_page_);
ESP_LOGCONFIG(TAG, " Start Up Page: %u", this->start_up_page_);
}
#endif // USE_NEXTION_CONF_START_UP_PAGE
#ifdef USE_NEXTION_COMMAND_SPACING
ESP_LOGCONFIG(TAG, " Cmd spacing: %u ms", this->command_pacer_.get_spacing());
ESP_LOGCONFIG(TAG, " Cmd spacing: %u ms", this->command_pacer_.get_spacing());
#endif // USE_NEXTION_COMMAND_SPACING
#ifdef USE_NEXTION_MAX_QUEUE_SIZE
ESP_LOGCONFIG(TAG, " Max queue size: %zu", this->max_queue_size_);
ESP_LOGCONFIG(TAG, " Max queue size: %zu", this->max_queue_size_);
#endif
}
@@ -336,7 +340,8 @@ void Nextion::loop() {
if (this->started_ms_ == 0)
this->started_ms_ = App.get_loop_component_start_time();
if (this->started_ms_ + this->startup_override_ms_ < App.get_loop_component_start_time()) {
if (this->startup_override_ms_ > 0 &&
this->started_ms_ + this->startup_override_ms_ < App.get_loop_component_start_time()) {
ESP_LOGV(TAG, "Manual ready set");
this->connection_state_.nextion_reports_is_setup_ = true;
}
@@ -845,7 +850,8 @@ void Nextion::process_nextion_commands_() {
const uint32_t ms = App.get_loop_component_start_time();
if (!this->nextion_queue_.empty() && this->nextion_queue_.front()->queue_time + this->max_q_age_ms_ < ms) {
if (this->max_q_age_ms_ > 0 && !this->nextion_queue_.empty() &&
this->nextion_queue_.front()->queue_time + this->max_q_age_ms_ < ms) {
for (size_t i = 0; i < this->nextion_queue_.size(); i++) {
NextionComponentBase *component = this->nextion_queue_[i]->component;
if (this->nextion_queue_[i]->queue_time + this->max_q_age_ms_ < ms) {

View File

@@ -1309,6 +1309,30 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
*/
bool is_connected() { return this->connection_state_.is_connected_; }
/**
* @brief Set the maximum age for queue items
* @param age_ms Maximum age in milliseconds before queue items are removed
*/
inline void set_max_queue_age(uint16_t age_ms) { this->max_q_age_ms_ = age_ms; }
/**
* @brief Get the maximum age for queue items
* @return Maximum age in milliseconds
*/
inline uint16_t get_max_queue_age() const { return this->max_q_age_ms_; }
/**
* @brief Set the startup override timeout
* @param timeout_ms Time in milliseconds to wait before forcing setup complete
*/
inline void set_startup_override_ms(uint16_t timeout_ms) { this->startup_override_ms_ = timeout_ms; }
/**
* @brief Get the startup override timeout
* @return Startup override timeout in milliseconds
*/
inline uint16_t get_startup_override_ms() const { return this->startup_override_ms_; }
protected:
#ifdef USE_NEXTION_MAX_COMMANDS_PER_LOOP
uint16_t max_commands_per_loop_{1000};
@@ -1479,9 +1503,10 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
void reset_(bool reset_nextion = true);
std::string command_data_;
const uint16_t startup_override_ms_ = 8000;
const uint16_t max_q_age_ms_ = 8000;
uint32_t started_ms_ = 0;
uint16_t startup_override_ms_ = 8000; ///< Timeout before forcing setup complete
uint16_t max_q_age_ms_ = 8000; ///< Maximum age for queue items in ms
};
} // namespace nextion

View File

@@ -44,7 +44,7 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await display.register_display(var, config)
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
cg.add(var.set_dc_pin(dc))

View File

@@ -161,7 +161,7 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await display.register_display(var, config)
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
chip = DriverChip.chips[config[CONF_MODEL]]
if chip.initsequence:

View File

@@ -1,5 +1,6 @@
#ifdef USE_ESP32_VARIANT_ESP32S3
#include "rpi_dpi_rgb.h"
#include "esphome/core/gpio.h"
#include "esphome/core/log.h"
namespace esphome {
@@ -134,8 +135,11 @@ void RpiDpiRgb::dump_config() {
LOG_PIN(" Enable Pin: ", this->enable_pin_);
LOG_PIN(" Reset Pin: ", this->reset_pin_);
size_t data_pin_count = sizeof(this->data_pins_) / sizeof(this->data_pins_[0]);
for (size_t i = 0; i != data_pin_count; i++)
ESP_LOGCONFIG(TAG, " Data pin %d: %s", i, (this->data_pins_[i])->dump_summary().c_str());
char pin_summary[GPIO_SUMMARY_MAX_LEN];
for (size_t i = 0; i != data_pin_count; i++) {
this->data_pins_[i]->dump_summary(pin_summary, sizeof(pin_summary));
ESP_LOGCONFIG(TAG, " Data pin %d: %s", i, pin_summary);
}
}
void RpiDpiRgb::reset_display_() const {

View File

@@ -445,22 +445,18 @@ optional<float> CalibratePolynomialFilter::new_value(float value) {
ClampFilter::ClampFilter(float min, float max, bool ignore_out_of_range)
: min_(min), max_(max), ignore_out_of_range_(ignore_out_of_range) {}
optional<float> ClampFilter::new_value(float value) {
if (std::isfinite(value)) {
if (std::isfinite(this->min_) && value < this->min_) {
if (this->ignore_out_of_range_) {
return {};
} else {
return this->min_;
}
if (std::isfinite(this->min_) && !(value >= this->min_)) {
if (this->ignore_out_of_range_) {
return {};
}
return this->min_;
}
if (std::isfinite(this->max_) && value > this->max_) {
if (this->ignore_out_of_range_) {
return {};
} else {
return this->max_;
}
if (std::isfinite(this->max_) && !(value <= this->max_)) {
if (this->ignore_out_of_range_) {
return {};
}
return this->max_;
}
return value;
}

View File

@@ -46,15 +46,15 @@ static inline const char *esphome_inet_ntop6(const void *addr, char *buf, size_t
#endif
// Format sockaddr into caller-provided buffer, returns length written (excluding null)
static size_t format_sockaddr_to(const struct sockaddr_storage &storage, std::span<char, SOCKADDR_STR_LEN> buf) {
if (storage.ss_family == AF_INET) {
const auto *addr = reinterpret_cast<const struct sockaddr_in *>(&storage);
size_t format_sockaddr_to(const struct sockaddr *addr_ptr, socklen_t len, std::span<char, SOCKADDR_STR_LEN> buf) {
if (addr_ptr->sa_family == AF_INET && len >= sizeof(const struct sockaddr_in)) {
const auto *addr = reinterpret_cast<const struct sockaddr_in *>(addr_ptr);
if (esphome_inet_ntop4(&addr->sin_addr, buf.data(), buf.size()) != nullptr)
return strlen(buf.data());
}
#if USE_NETWORK_IPV6
else if (storage.ss_family == AF_INET6) {
const auto *addr = reinterpret_cast<const struct sockaddr_in6 *>(&storage);
else if (addr_ptr->sa_family == AF_INET6 && len >= sizeof(sockaddr_in6)) {
const auto *addr = reinterpret_cast<const struct sockaddr_in6 *>(addr_ptr);
#ifndef USE_SOCKET_IMPL_LWIP_TCP
// Format IPv4-mapped IPv6 addresses as regular IPv4 (not supported on ESP8266 raw TCP)
if (addr->sin6_addr.un.u32_addr[0] == 0 && addr->sin6_addr.un.u32_addr[1] == 0 &&
@@ -78,7 +78,7 @@ size_t Socket::getpeername_to(std::span<char, SOCKADDR_STR_LEN> buf) {
buf[0] = '\0';
return 0;
}
return format_sockaddr_to(storage, buf);
return format_sockaddr_to(reinterpret_cast<struct sockaddr *>(&storage), len, buf);
}
size_t Socket::getsockname_to(std::span<char, SOCKADDR_STR_LEN> buf) {
@@ -88,7 +88,7 @@ size_t Socket::getsockname_to(std::span<char, SOCKADDR_STR_LEN> buf) {
buf[0] = '\0';
return 0;
}
return format_sockaddr_to(storage, buf);
return format_sockaddr_to(reinterpret_cast<struct sockaddr *>(&storage), len, buf);
}
std::unique_ptr<Socket> socket_ip(int type, int protocol) {

View File

@@ -102,6 +102,9 @@ inline socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const st
/// Set a sockaddr to the any address and specified port for the IP version used by socket_ip().
socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t port);
/// Format sockaddr into caller-provided buffer, returns length written (excluding null)
size_t format_sockaddr_to(const struct sockaddr *addr_ptr, socklen_t len, std::span<char, SOCKADDR_STR_LEN> buf);
#if defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP)
/// Delay that can be woken early by socket activity.
/// On ESP8266, lwip callbacks set a flag and call esp_schedule() to wake the delay.

View File

@@ -39,6 +39,7 @@ from esphome.const import (
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
import esphome.final_validate as fv
from esphome.types import ConfigType
CODEOWNERS = ["@esphome/core", "@clydebarrow"]
spi_ns = cg.esphome_ns.namespace("spi")
@@ -448,9 +449,13 @@ def spi_device_schema(
)
async def register_spi_device(var, config):
async def register_spi_device(
var: cg.Pvariable, config: ConfigType, write_only: bool = False
) -> None:
parent = await cg.get_variable(config[CONF_SPI_ID])
cg.add(var.set_spi_parent(parent))
if write_only:
cg.add(var.set_write_only(True))
if cs_pin := config.get(CONF_CS_PIN):
pin = await cg.gpio_pin_expression(cs_pin)
cg.add(var.set_cs_pin(pin))

View File

@@ -195,8 +195,11 @@ class SPIDelegateHw : public SPIDelegate {
config.post_cb = nullptr;
if (this->bit_order_ == BIT_ORDER_LSB_FIRST)
config.flags |= SPI_DEVICE_BIT_LSBFIRST;
if (this->write_only_)
if (this->write_only_) {
config.flags |= SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_NO_DUMMY;
ESP_LOGD(TAG, "SPI device with CS pin %d using half-duplex mode (write-only)",
Utility::get_pin_no(this->cs_pin_));
}
esp_err_t const err = spi_bus_add_device(this->channel_, &config, &this->handle_);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Add device failed - err %X", err);

View File

@@ -32,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await ssd1306_base.setup_ssd1306(var, config)
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
cg.add(var.set_dc_pin(dc))

View File

@@ -32,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await ssd1322_base.setup_ssd1322(var, config)
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
cg.add(var.set_dc_pin(dc))

View File

@@ -32,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await ssd1325_base.setup_ssd1325(var, config)
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
cg.add(var.set_dc_pin(dc))

View File

@@ -32,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await ssd1327_base.setup_ssd1327(var, config)
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
cg.add(var.set_dc_pin(dc))

View File

@@ -32,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await ssd1331_base.setup_ssd1331(var, config)
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
cg.add(var.set_dc_pin(dc))

View File

@@ -32,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await ssd1351_base.setup_ssd1351(var, config)
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
cg.add(var.set_dc_pin(dc))

View File

@@ -32,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await st7567_base.setup_st7567(var, config)
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
cg.add(var.set_dc_pin(dc))

View File

@@ -173,7 +173,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await display.register_display(var, config)
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
sequence = []
for seq in config[CONF_INIT_SEQUENCE]:

View File

@@ -1,5 +1,6 @@
#ifdef USE_ESP32_VARIANT_ESP32S3
#include "st7701s.h"
#include "esphome/core/gpio.h"
#include "esphome/core/log.h"
namespace esphome {
@@ -183,8 +184,11 @@ void ST7701S::dump_config() {
LOG_PIN(" DE Pin: ", this->de_pin_);
LOG_PIN(" Reset Pin: ", this->reset_pin_);
size_t data_pin_count = sizeof(this->data_pins_) / sizeof(this->data_pins_[0]);
for (size_t i = 0; i != data_pin_count; i++)
ESP_LOGCONFIG(TAG, " Data pin %d: %s", i, (this->data_pins_[i])->dump_summary().c_str());
char pin_summary[GPIO_SUMMARY_MAX_LEN];
for (size_t i = 0; i != data_pin_count; i++) {
this->data_pins_[i]->dump_summary(pin_summary, sizeof(pin_summary));
ESP_LOGCONFIG(TAG, " Data pin %d: %s", i, pin_summary);
}
ESP_LOGCONFIG(TAG, " SPI Data rate: %dMHz", (unsigned) (this->data_rate_ / 1000000));
}

View File

@@ -99,7 +99,7 @@ async def to_code(config):
config[CONF_INVERT_COLORS],
)
await setup_st7735(var, config)
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
cg.add(var.set_dc_pin(dc))

View File

@@ -177,7 +177,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await display.register_display(var, config)
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
cg.add(var.set_model_str(config[CONF_MODEL]))

View File

@@ -28,7 +28,7 @@ CONFIG_SCHEMA = (
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
if CONF_LAMBDA in config:
lambda_ = await cg.process_lambda(

View File

@@ -40,6 +40,9 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) {
// Unsigned subtraction handles wraparound correctly, then cast to signed
int32_t diff = static_cast<int32_t>(epoch - static_cast<uint32_t>(current_time));
if (diff >= -1 && diff <= 1) {
// Time is already synchronized, but still call callbacks so components
// waiting for time sync (e.g., uptime timestamp sensor) can initialize
this->time_sync_callback_.call();
return;
}
}

View File

@@ -239,7 +239,7 @@ async def to_code(config):
raise NotImplementedError()
await display.register_display(var, config)
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
cg.add(var.set_dc_pin(dc))

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