Compare commits

...
Author SHA1 Message Date
Jesse Hills eb53ed5558 [api] Cover the PlatformIO toolchain in the managed component tests
The ESP-IDF framework can also be built with the PlatformIO toolchain, and the
managed components are used on both. Record why that choice is deliberately
toolchain-independent: wireguard splits on the same condition, and if the two
ever disagree one of them converts a second libsodium next to the managed one.

The test now sets the toolchain explicitly and asserts the PlatformIO one takes
the managed path too, so the condition cannot narrow without a test failing.
2026-08-18 07:24:25 +12:00
Jesse Hills 6d14778123 Merge remote-tracking branch 'origin/dev' into jesserockz-2026-584
# Conflicts:
#	esphome/components/api/__init__.py
#	platformio.ini
2026-08-18 07:11:48 +12:00
J. Nick KostonandGitHub a3af82867b [api] Bump noise-c to 0.1.18 (#18451) 2026-08-17 14:00:39 -05:00
J. Nick KostonandGitHub 27483a4101 [core] Retry gh CLI calls on transient network errors in CI scripts (#18292) 2026-08-17 13:15:31 -05:00
J. Nick KostonandGitHub 6d20ebc66b [socket] Shorten lwip TAG string (#18450) 2026-08-17 17:18:43 +00:00
J. Nick KostonandGitHub 031a038b49 [deep_sleep] Shorten bk72xx TAG string (#18445) 2026-08-17 11:56:37 -05:00
J. Nick KostonandGitHub f6c7434b2a [i2c] Shorten platform backend TAG strings (#18440) 2026-08-17 11:54:55 -05:00
J. Nick KostonandGitHub 3d9fecb562 [remote_receiver] Shorten esp32 TAG string (#18447) 2026-08-17 11:54:52 -05:00
J. Nick KostonandGitHub ebe93e2c68 [bluetooth_connection] Shorten platform TAG strings (#18449) 2026-08-17 11:54:35 -05:00
J. Nick KostonandGitHub 1f4fcead38 [nextion] Shorten upload TAG strings (#18448) 2026-08-17 11:54:29 -05:00
J. Nick KostonandGitHub 1f000ba668 [mqtt] Shorten esp32 backend TAG string (#18446) 2026-08-17 11:54:17 -05:00
J. Nick KostonandGitHub e0d28d7f5c [http_request] Shorten platform backend TAG strings (#18444) 2026-08-17 11:54:03 -05:00
J. Nick KostonandGitHub 47a58dd799 [internal_temperature] Shorten platform TAG strings (#18443) 2026-08-17 11:53:56 -05:00
J. Nick KostonandGitHub 37bea1c153 [spi] Shorten platform backend TAG strings (#18442) 2026-08-17 11:53:48 -05:00
J. Nick KostonandGitHub d1a7b8df8b [adc] Shorten platform TAG strings (#18441) 2026-08-17 11:53:42 -05:00
J. Nick KostonandGitHub c01f24553c [uart] Shorten platform backend TAG strings (#18439) 2026-08-17 11:53:29 -05:00
J. Nick KostonandGitHub 07e8b303b9 [ota] Shorten platform backend TAG strings (#18438) 2026-08-17 11:52:56 -05:00
esphome[bot]GitHubesphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
ebb0923362 Bump aioesphomeapi from 45.10.2 to 45.10.3 (#18433)
Co-authored-by: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
2026-08-16 22:34:00 -05:00
J. Nick KostonandGitHub e1c279718f [ld2420] Drop the setup priority override so setup runs after the UART bus (#18428) 2026-08-16 21:57:12 -05:00
J. Nick KostonandGitHub cf764740cf [api] Create the camera image reader lazily (#18421) 2026-08-16 21:56:58 -05:00
J. Nick KostonandGitHub 58d549ed4c [api] Move NoiseProtocolId off the connection object (#18420) 2026-08-16 21:56:37 -05:00
Jesse Hills 6b22d8068d Merge branch 'beta' into dev 2026-08-17 11:36:10 +12:00
Jesse HillsandGitHub 443d8f1f28 Merge pull request #18424 from esphome/bump-2026.8.0b4
2026.8.0b4
2026-08-17 11:33:27 +12:00
Jesse Hills 1ec21a2245 Bump version to 2026.8.0b4 2026-08-17 10:22:41 +12:00
J. Nick KostonandJesse Hills bb7d4c3630 [esp32] Split crash handler addr2line hint per core (#18418) 2026-08-17 10:22:39 +12:00
J. Nick KostonandJesse Hills f42fe9af29 [core] Skip redundant ESP8266 main loop wake posts from ISR context (#18416) 2026-08-17 10:22:39 +12:00
J. Nick KostonandJesse Hills 0bc2d71370 [bk72xx_ble] Fail early with a clear error on non BLE 5.x SoCs (#18406) 2026-08-17 10:22:39 +12:00
luar123andJesse Hills 594c12b3d9 [zigbee] bump esp-zigbee-sdk to 2.0.4 (#18415) 2026-08-17 10:22:39 +12:00
Jonathan SwobodaandJesse Hills bca72e9b6d [sensor] Pass NaN through the delta filter again (#18400) 2026-08-17 10:22:39 +12:00
J. Nick KostonandJesse Hills ce09504c92 [platformio] Skip ccache when the binary on PATH fails to run (#18407) 2026-08-17 10:22:39 +12:00
esphome[bot]andJesse Hills dda4566b9e Bump bundled esphome-device-builder to 1.11.0 (#18403) 2026-08-17 10:22:39 +12:00
Samuel SiebandJesse Hills 46a5665a66 [rotary_encoder] account for min and max value when resetting (#18197) 2026-08-17 10:22:39 +12:00
esphome[bot]andJesse Hills 9161f74bb1 Bump bundled esphome-device-builder to 1.10.0 (#18389) 2026-08-17 10:22:39 +12:00
J. Nick KostonandGitHub 3f01f9895f [esp32_hosted] Require ESP-IDF 5.3 or newer (#18417) 2026-08-16 12:44:49 -07:00
J. Nick KostonandGitHub 801a1817b5 [esp32] Split crash handler addr2line hint per core (#18418) 2026-08-16 12:44:00 -07:00
J. Nick KostonandGitHub 32c76ae828 [core] Skip redundant ESP8266 main loop wake posts from ISR context (#18416) 2026-08-16 12:43:27 -07:00
J. Nick KostonandGitHub c664f5fc95 [bk72xx_ble] Fail early with a clear error on non BLE 5.x SoCs (#18406) 2026-08-16 11:06:22 -07:00
luar123andGitHub 2bc4681fd6 [zigbee] bump esp-zigbee-sdk to 2.0.4 (#18415) 2026-08-16 13:33:25 -04:00
Jonathan SwobodaandGitHub 646501b0ef [sensor] Pass NaN through the delta filter again (#18400) 2026-08-16 12:36:51 -04:00
J. Nick KostonandGitHub de3e657d8b [platformio] Skip ccache when the binary on PATH fails to run (#18407) 2026-08-16 08:42:47 -07:00
esphome[bot]andGitHub 1add726892 Bump bundled esphome-device-builder to 1.11.0 (#18403) 2026-08-15 13:36:52 -07:00
Samuel SiebandGitHub 5a000cf5e4 [rotary_encoder] account for min and max value when resetting (#18197) 2026-08-15 11:16:04 -07:00
Joppy FurrandGitHub 6ed676fe32 [lvgl] Restore long_press_repeat_time functionality (#18393) 2026-08-14 23:14:53 -07:00
esphome[bot]andGitHub 7cceddb8a3 Bump bundled esphome-device-builder to 1.10.0 (#18389) 2026-08-15 01:48:27 +00:00
J. Nick KostonandGitHub 039b897e7b [ethernet] Defer clk_mode removal to 2026.11.0 (#18380) 2026-08-14 18:45:36 -07:00
J. Nick KostonandGitHub b178f74e5d [core] Save the validated config cache on the first upload or logs run (#18367) 2026-08-14 18:45:23 -07:00
Clyde StubbsandGitHub e5224e22ae [ci] Compare merge-branch base ref against the default branch (#18385) 2026-08-15 06:18:22 +10:00
Clyde StubbsandGitHub be66e8b99c [ci] Disable CodSpeed benchmarks job outside esphome/esphome (#18372) 2026-08-15 04:33:47 +10:00
Jesse Hills 4b91c8aff4 Merge branch 'beta' into dev 2026-08-14 23:15:57 +12:00
Jesse HillsandGitHub 9d7997f55a Merge pull request #18368 from esphome/bump-2026.8.0b3
2026.8.0b3
2026-08-14 22:49:40 +12:00
Jesse Hills 4db47de556 Bump version to 2026.8.0b3 2026-08-14 17:36:56 +12:00
Karl BeeckenandJesse Hills c8de632764 [core] fix PYTHONPATH leak (#18360) 2026-08-14 17:36:54 +12:00
J. Nick KostonandJesse Hills b794b7b1d1 [core] Restore cv.parse_esphome_version as a deprecated helper (#18366) 2026-08-14 17:36:54 +12:00
J. Nick KostonandJesse Hills add18d4e35 [core] Partially revert "Hash entity keys from the raw name to fix collisions" (#18361) 2026-08-14 17:36:54 +12:00
J. Nick KostonandJesse Hills 02c1810c3a [core] Load component aliases from a generated registry (#18335) 2026-08-14 17:36:54 +12:00
J. Nick KostonandJesse Hills 4f3153375a [ota] Retry uploads that fail from network errors (#18332) 2026-08-14 17:36:54 +12:00
J. Nick KostonandJesse Hills 1c3a67b5e8 [wifi] Fix ESP8266 crash in cnx_node_search when lwIP transmits after disconnect (#18333) 2026-08-14 17:36:54 +12:00
J. Nick KostonandJesse Hills 236ff33a09 [esp32_ble] Silence spurious warnings for local key GAP events (#18359) 2026-08-14 17:36:54 +12:00
J. Nick KostonandJesse Hills 7c07fb48c5 [esp32_ble_tracker] Fix missed BLE advertisements with WiFi on ESP-IDF 5.5.5 (#18356) 2026-08-14 17:36:54 +12:00
J. Nick KostonandJesse Hills d72bab79d7 [web_server_base] Stop deleting the web server on captive portal teardown (#18324) 2026-08-14 17:36:54 +12:00
Karl BeeckenandGitHub 617e2ec1e0 [core] fix PYTHONPATH leak (#18360) 2026-08-14 17:23:34 +12:00
J. Nick KostonandGitHub 1afac0312d [core] Restore cv.parse_esphome_version as a deprecated helper (#18366) 2026-08-14 17:22:22 +12:00
J. Nick KostonandGitHub b05465145f [core] Add preference key stability integration test (#18364) 2026-08-13 21:14:30 -05:00
dependabot[bot]andGitHub 990fc402fd Bump bleak from 2.1.1 to 3.0.2 (#16246)
Signed-off-by: dependabot[bot] <support@github.com>
2026-08-13 20:24:02 -05:00
J. Nick KostonandGitHub dd51624fbb [core] Partially revert "Hash entity keys from the raw name to fix collisions" (#18361) 2026-08-13 20:06:48 -05:00
dependabot[bot]andGitHub 45a056e337 Bump platformdirs from 4.11.1 to 4.11.2 (#18363)
Signed-off-by: dependabot[bot] <support@github.com>
2026-08-13 18:54:51 -05:00
dependabot[bot]andGitHub 37782f7206 Bump prek from 0.4.12 to 0.4.13 (#18362)
Signed-off-by: dependabot[bot] <support@github.com>
2026-08-13 18:54:37 -05:00
J. Nick KostonandGitHub 945c2458b3 [core] Load component aliases from a generated registry (#18335) 2026-08-13 13:31:19 -05:00
J. Nick KostonandGitHub 7420d23867 [ota] Retry uploads that fail from network errors (#18332) 2026-08-13 13:31:06 -05:00
J. Nick KostonandGitHub 191686c5b3 [wifi] Fix ESP8266 crash in cnx_node_search when lwIP transmits after disconnect (#18333) 2026-08-13 13:30:53 -05:00
J. Nick KostonandGitHub 137351fa8d [esp32_ble] Silence spurious warnings for local key GAP events (#18359) 2026-08-13 13:27:01 -05:00
J. Nick KostonandGitHub db5173697a [esp32_ble_tracker] Fix missed BLE advertisements with WiFi on ESP-IDF 5.5.5 (#18356) 2026-08-13 13:22:48 -05:00
Oliver KleineckeGitHubOliver Kleineckepre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
c7940382a9 [const] move CONF_LABEL to components/const (#18354)
Co-authored-by: Oliver Kleinecke <kleinecke.oliver@googlemail.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-08-13 14:18:22 -04:00
esphome[bot]andGitHub 87045ab9c0 Bump aioesphomeapi from 45.10.1 to 45.10.2 (#18357) 2026-08-13 11:14:14 -05:00
f1c4086778 [const] Move CONF_SLOT to components/const (#18350)
Co-authored-by: Oliver Kleinecke <p1ngb4ck@users.noreply.github.com>
2026-08-13 23:36:11 +12:00
Jesse Hills eec17043bc Merge remote-tracking branch 'origin/dev' into jesserockz-2026-584 2026-08-13 23:12:59 +12:00
Jesse Hills 041123b14c [api] Import noise-c and libsodium as ESP-IDF managed components
Both libraries now ship their own CMakeLists.txt, so on ESP-IDF they can be
pulled straight from the component registry (noise-c 0.1.15, libsodium
1.10021.2) instead of going through ESPHome's PlatformIO library converter.

A library must not be both converted and managed, or IDF refuses component
discovery, so the converter now takes a set of names the toolchain already
provides. Arduino keeps the converted path: arduino-esp32 brings its own
espressif/libsodium and IDF cannot pick between two managed components whose
names differ only by namespace.
2026-08-13 23:12:47 +12:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Jonathan Swoboda
f337d0acff Bump pylint from 4.0.6 to 4.0.7 (#18320)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-08-13 02:13:29 +00:00
J. Nick KostonandGitHub 9cc05b30d4 [web_server_base] Stop deleting the web server on captive portal teardown (#18324) 2026-08-12 21:09:01 -05:00
Jesse Hills e0b68c4d6d Merge branch 'beta' into dev 2026-08-13 14:06:46 +12:00
J. Nick KostonandGitHub 8e624b4117 [core] Retry framework downloads on transient network errors (#18330) 2026-08-13 12:14:50 +12:00
J. Nick KostonandGitHub 787a909aa4 [core] Don't block logs startup on MQTT IP discovery when addresses are known (#18313) 2026-08-13 12:11:32 +12:00
J. Nick KostonandGitHub 905485b673 [ld2420] Fix out-of-bounds read when device reports unknown command error (#18322) 2026-08-13 11:49:34 +12:00
esphome[bot]andGitHub 99677390e0 Bump bundled esphome-device-builder to 1.9.6 (#18328) 2026-08-12 16:27:14 -05:00
Kevin AhrendtandGitHub f2121130f9 [sendspin] Bump sendspin-cpp to v0.7.2 (#18316) 2026-08-12 12:49:54 -04:00
esphome[bot]andGitHub 1a01c34ec4 Bump aioesphomeapi from 45.10.0 to 45.10.1 (#18318) 2026-08-12 16:33:45 +00:00
J. Nick KostonandGitHub 3c46cc9c35 [usb_uart] Fix uint32_t format specifier warning in pl2303 (#18310) 2026-08-12 10:59:24 -04:00
Jesse Hills e192ec8fee Merge branch 'beta' into dev 2026-08-13 00:29:08 +12:00
Jesse Hills 8a1aa5753d Bump version to 2026.9.0-dev 2026-08-12 22:53:42 +12:00
165 changed files with 3576 additions and 1311 deletions
+2 -1
View File
@@ -70,6 +70,7 @@ async function isStackedPr(github, context) {
async function detectMergeBranch(github, context) {
const labels = new Set();
const baseRef = context.payload.pull_request.base.ref;
const defaultBranch = context.payload.repository.default_branch;
if (baseRef === 'release') {
labels.add('merging-to-release');
@@ -78,7 +79,7 @@ async function detectMergeBranch(github, context) {
} else if (await isStackedPr(github, context)) {
// GitHub manages the merge order for a stack, so these are not blocked.
labels.add('stacked-pr');
} else if (baseRef !== 'dev') {
} else if (baseRef !== defaultBranch) {
// A chain built by hand: it must not merge until its base branch does.
labels.add('chained-pr');
}
@@ -43,14 +43,14 @@ const WITHOUT_SCHEMA = 'CODEOWNERS = ["@esphome/core"]';
// Builds a fresh context for detectMergeBranch tests instead of mutating the
// shared CONTEXT fixture above (which other describe blocks rely on).
function makeMergeContext(baseRef, { stack } = {}) {
function makeMergeContext(baseRef, { stack, defaultBranch = 'dev' } = {}) {
const pull_request = { number: 1, base: { ref: baseRef } };
if (stack !== undefined) {
pull_request.stack = stack;
}
return {
repo: { owner: 'esphome', repo: 'esphome' },
payload: { pull_request }
payload: { pull_request, repository: { default_branch: defaultBranch } }
};
}
@@ -136,6 +136,21 @@ describe('detectMergeBranch', () => {
assert.deepEqual(Array.from(labels).sort(), ['chained-pr']);
assert.equal(state.calls, 1);
});
it('base ref matches default branch adds no labels', async () => {
const { github } = makeStackGithub({ stack: null });
const context = makeMergeContext('other', { defaultBranch: 'other' });
const labels = await detectMergeBranch(github, context);
assert.deepEqual(Array.from(labels).sort(), []);
});
it('base ref dev when the default branch is main adds chained-pr', async () => {
const { github } = makeStackGithub({ stack: null });
const context = makeMergeContext('dev', { defaultBranch: 'main' });
const labels = await detectMergeBranch(github, context);
assert.deepEqual(Array.from(labels).sort(), ['chained-pr']);
});
});
// ---------------------------------------------------------------------------
+7 -2
View File
@@ -179,6 +179,7 @@ jobs:
. venv/bin/activate
script/ci-custom.py
script/build_codeowners.py --check
script/build_alias_registry.py --check
script/build_language_schema.py --check
script/generate-esp32-boards.py --check
script/generate-rp2-boards.py --check
@@ -444,8 +445,12 @@ jobs:
- common
- determine-jobs
if: >-
(github.event_name == 'push' && github.ref_name == 'dev') ||
(github.event_name == 'pull_request' && needs.determine-jobs.outputs.benchmarks == 'true')
github.repository == 'esphome/esphome' && (
(github.event_name == 'push' && github.ref_name == 'dev') ||
(github.event_name == 'pull_request' && needs.determine-jobs.outputs.benchmarks == 'true')
)
# CodSpeed benchmarks require a CodSpeed account linked to the repository to run
# (https://codspeed.io) -- disabled on forks that aren't esphome/esphome itself.
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+1 -1
View File
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version
# control system is used.
PROJECT_NUMBER = 2026.8.0b2
PROJECT_NUMBER = 2026.9.0-dev
# Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a
+1 -1
View File
@@ -22,7 +22,7 @@ RUN \
-r /requirements.txt
# Install the ESPHome Device Builder dashboard.
RUN uv pip install --no-cache-dir esphome-device-builder==1.9.6
RUN uv pip install --no-cache-dir esphome-device-builder==1.11.0
RUN \
platformio settings set enable_telemetry No \
+14 -14
View File
@@ -2732,7 +2732,8 @@ def run_esphome(argv):
conf_path.name,
)
if config is None:
cache_missed = config is None
if cache_missed:
from esphome.config import read_config
config = read_config(
@@ -2741,26 +2742,25 @@ def run_esphome(argv):
# Snapshot only needed by `esphome config --no-defaults`.
snapshot_user_config=getattr(args, "no_defaults", False),
)
# Refresh the cache so the next upload/logs hits the fast path
# instead of re-running read_config. Skip when the storage
# sidecar is absent (no compile has run): the cache would
# never be loaded back, so writing secrets to disk is wasted.
if cache_eligible and config is not None:
from esphome.compiled_config import save_compiled_config
from esphome.storage_json import ext_storage_path
if ext_storage_path(conf_path.name).exists():
save_compiled_config(config)
if config is None:
return 2
if config is None:
return 2
CORE.config = config
# Fallback for platforms whose validators didn't set the toolchain
# (only the esp32 component reads esp32.framework.toolchain). All
# other platforms only support PlatformIO today.
# other platforms only support PlatformIO today. Must run before the
# cache refresh below so its sidecar records the same toolchain a
# compile would.
if CORE.toolchain is None:
CORE.toolchain = Toolchain.PLATFORMIO
# Refresh the cache so the next upload/logs hits the fast path
# instead of re-running read_config.
if cache_eligible and cache_missed:
from esphome.compiled_config import save_compiled_config_and_sidecar
save_compiled_config_and_sidecar(config)
if args.command not in POST_CONFIG_ACTIONS:
safe_print(f"Unknown command {args.command}")
return 1
+69 -8
View File
@@ -18,9 +18,9 @@ from pathlib import Path
from typing import Any
from esphome.const import __version__ as ESPHOME_VERSION
from esphome.core import CORE, Lambda
from esphome.core import CORE, EsphomeError, Lambda
from esphome.helpers import write_file
from esphome.storage_json import StorageJSON, ext_storage_path
from esphome.storage_json import StorageJSON, ext_storage_path, storage_path
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
@@ -65,7 +65,71 @@ def save_compiled_config(config: ConfigType) -> None:
# non-basic dict key), so every upload/logs pays the slow path.
_LOGGER.warning("Cannot cache the validated config: %s", err)
except Exception as err: # noqa: BLE001 # pylint: disable=broad-except
_LOGGER.debug("Skipping compiled config cache write: %s", err)
# Likely persistent (permissions, full disk): every upload/logs
# pays the slow path until it clears, so surface it.
_LOGGER.warning("Skipping compiled config cache write: %s", err)
def save_compiled_config_and_sidecar(config: ConfigType) -> None:
"""Refresh the cache from the upload/logs fallback (CORE.config must be set).
The cache is only written when a complete sidecar is on disk:
load_compiled_config can't use it otherwise, and it holds resolved
secrets.
"""
if _refresh_sidecar():
save_compiled_config(config)
def _refresh_sidecar() -> bool:
"""Ensure a complete sidecar is on disk; True when one is.
Writes one (without claiming a build) when missing or wizard-only.
Failures are non-fatal; the next upload/logs pays the slow path again.
"""
try:
path = storage_path()
try:
old = StorageJSON.load_strict(path)
except Exception as err: # noqa: BLE001 # pylint: disable=broad-except
# Present but unreadable: it may hold a real build's metadata,
# and a fresh rewrite would also stop the next compile from
# cleaning a possibly incoherent build tree.
_LOGGER.warning(
"Not caching: storage sidecar %s is unreadable (%s)", path, err
)
return False
if old is not None and old.can_apply_to_core():
# Compile-written; nothing to refresh.
return True
if CORE.build_path is not None and CORE.build_path.exists():
# An unvalidated build tree: its absent or mismatched sidecar
# is what makes the next compile wipe it, so don't vouch for
# a build this run never saw.
_LOGGER.warning(
"Not caching: build tree %s has no matching sidecar; "
"'esphome compile' will settle it",
CORE.build_path,
)
return False
new = StorageJSON.from_esphome_core(CORE, old, claim_build=False)
if not new.can_apply_to_core():
_LOGGER.warning("Not caching: rebuilt storage sidecar is still incomplete")
return False
new.save(path)
return True
except (OSError, EsphomeError) as err:
# write_file wraps OSError into EsphomeError. Persistent
# (unwritable storage dir), so surface that every upload/logs
# pays the slow path.
_LOGGER.warning("Could not refresh the storage sidecar: %s", err)
except Exception: # noqa: BLE001 # pylint: disable=broad-except
# A structural bug; keep the traceback so it isn't mistaken
# for the I/O failure above.
_LOGGER.warning(
"Unexpected error refreshing the storage sidecar", exc_info=True
)
return False
def load_compiled_config(conf_path: Path) -> ConfigType | None:
@@ -98,11 +162,8 @@ def load_compiled_config(conf_path: Path) -> ConfigType | None:
return None
storage = StorageJSON.load(ext_storage_path(conf_path.name))
if storage is None:
return None
# apply_to_core assumes a real compile wrote the sidecar; wizard-only
# sidecars leave both of these unset and can't drive upload/logs.
if not storage.core_platform and not storage.target_platform:
if storage is None or not storage.can_apply_to_core():
_LOGGER.debug("Ignoring compiled config cache: sidecar missing or incomplete")
return None
storage.apply_to_core()
return config
+10
View File
@@ -0,0 +1,10 @@
"""Component alias registry.
Generated by script/build_alias_registry.py - do not edit manually.
See the component-alias section of esphome/loader.py.
"""
# alias -> (canonical component, removal version or None)
COMPONENT_ALIASES: dict[str, tuple[str, str | None]] = {
"rp2040": ("rp2", "2027.7.0"),
}
+1 -1
View File
@@ -3,7 +3,7 @@
namespace esphome::adc {
static const char *const TAG = "adc.common";
static const char *const TAG = "adc";
const LogString *sampling_mode_to_str(SamplingMode mode) {
switch (mode) {
+1 -1
View File
@@ -6,7 +6,7 @@
namespace esphome::adc {
static const char *const TAG = "adc.esp32";
static const char *const TAG = "adc";
adc_oneshot_unit_handle_t ADCSensor::shared_adc_handles[2] = {nullptr, nullptr};
@@ -13,7 +13,7 @@ ADC_MODE(ADC_VCC)
namespace esphome::adc {
static const char *const TAG = "adc.esp8266";
static const char *const TAG = "adc";
void ADCSensor::setup() {
#ifndef USE_ADC_SENSOR_VCC
@@ -5,7 +5,7 @@
namespace esphome::adc {
static const char *const TAG = "adc.libretiny";
static const char *const TAG = "adc";
void ADCSensor::setup() {
#ifndef USE_ADC_SENSOR_VCC
+1 -1
View File
@@ -17,7 +17,7 @@
namespace esphome::adc {
static const char *const TAG = "adc.rp2";
static const char *const TAG = "adc";
// The on-die temperature sensor sits on the last ADC channel: input 4 on RP2040
// and RP2350A, but input 8 on RP2350B, which has eight external channels rather
+1 -1
View File
@@ -7,7 +7,7 @@
namespace esphome::adc {
static const char *const TAG = "adc.zephyr";
static const char *const TAG = "adc";
void ADCSensor::setup() {
if (!adc_is_ready_dt(this->channel_)) {
+27 -1
View File
@@ -43,6 +43,11 @@ DOMAIN = "api"
DEPENDENCIES = ["network"]
CODEOWNERS = ["@esphome/core"]
# Keep in sync with platformio.ini and esphome/idf_component.yml.
# LIBSODIUM_VERSION must match the version noise-c pins in its idf_component.yml.
NOISE_C_VERSION = "0.1.18"
LIBSODIUM_VERSION = "1.10021.2"
def AUTO_LOAD(config: ConfigType) -> list[str]:
"""Conditionally auto-load json only when capture_response is used."""
@@ -497,7 +502,28 @@ async def to_code(config: ConfigType) -> None:
# and plaintext disabled. Only a factory reset can remove it.
cg.add_define("USE_API_PLAINTEXT")
cg.add_define("USE_API_NOISE")
cg.add_library("esphome/noise-c", "0.1.11")
# Both libraries build themselves as ESP-IDF components, so on ESP32
# they are pulled straight from the component registry instead of going
# through ESPHome's PlatformIO-library converter. Deliberately not
# conditional on the toolchain: wireguard splits on the same condition,
# and if the two disagree one of them converts a second libsodium next
# to the managed one.
#
# Not on the Arduino framework though: arduino-esp32 depends on
# espressif/libsodium of its own (on IDF < 6.0), so the component
# manager would see two managed components whose names match once the
# namespace is stripped, and refuse to pick between them.
if CORE.is_esp32 and not CORE.using_arduino:
from esphome.components.esp32 import add_idf_component
add_idf_component(name="esphome/noise-c", ref=NOISE_C_VERSION)
# noise-c pulls libsodium in itself, but declaring it here too keeps
# other components that depend on it (wireguard, via esp_wireguard)
# from converting a second copy of the PlatformIO library alongside
# this managed one, which IDF rejects as a duplicate requirement.
add_idf_component(name="esphome/libsodium", ref=LIBSODIUM_VERSION)
else:
cg.add_library("esphome/noise-c", NOISE_C_VERSION)
# Enable optimized memzero/memcmp in libsodium instead of volatile byte loops
cg.add_build_flag("-DHAVE_WEAK_SYMBOLS=1")
cg.add_build_flag("-DHAVE_INLINE_ASM=1")
+15 -15
View File
@@ -160,11 +160,6 @@ APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *pa
#else
#error "No frame helper defined"
#endif
#ifdef USE_CAMERA
if (camera::Camera::instance() != nullptr) {
this->image_reader_ = std::unique_ptr<camera::CameraImageReader>{camera::Camera::instance()->create_image_reader()};
}
#endif
}
void APIConnection::start() {
@@ -448,7 +443,7 @@ void APIConnection::on_disconnect_response() {
uint16_t APIConnection::fill_and_encode_entity_state(EntityBase *entity, StateResponseProtoMessage &msg,
CalculateSizeFn size_fn, MessageEncodeFn encode_fn,
APIConnection *conn, uint32_t remaining_size) {
msg.key = entity->get_entity_key();
msg.key = entity->get_object_id_hash();
#ifdef USE_DEVICES
msg.device_id = entity->get_device_id();
#endif
@@ -459,7 +454,7 @@ uint16_t APIConnection::fill_and_encode_entity_info(EntityBase *entity, InfoResp
CalculateSizeFn size_fn, MessageEncodeFn encode_fn,
APIConnection *conn, uint32_t remaining_size) {
// Set common fields that are shared by all entity types
msg.key = entity->get_entity_key();
msg.key = entity->get_object_id_hash();
if (entity->has_own_name()) {
msg.name = entity->get_name();
@@ -1140,6 +1135,7 @@ void APIConnection::try_send_camera_image_() {
if (!this->image_reader_)
return;
const auto *cam = camera::Camera::instance();
// Send as many chunks as possible without blocking
while (this->image_reader_->available()) {
if (!this->helper_->can_write_without_blocking())
@@ -1149,11 +1145,11 @@ void APIConnection::try_send_camera_image_() {
bool done = this->image_reader_->available() == to_send;
CameraImageResponse msg;
msg.key = camera::Camera::instance()->get_entity_key();
msg.key = cam->get_object_id_hash();
msg.set_data(this->image_reader_->peek_data_buffer(), to_send);
msg.done = done;
#ifdef USE_DEVICES
msg.device_id = camera::Camera::instance()->get_device_id();
msg.device_id = cam->get_device_id();
#endif
if (!this->send_message(msg)) {
@@ -1169,15 +1165,19 @@ void APIConnection::try_send_camera_image_() {
void APIConnection::set_camera_state(std::shared_ptr<camera::CameraImage> image) {
if (!this->flags_.state_subscription)
return;
if (!this->image_reader_)
if (this->image_reader_ && this->image_reader_->available())
return;
if (this->image_reader_->available())
if (!image->was_requested_by(esphome::camera::API_REQUESTER) && !image->was_requested_by(esphome::camera::IDLE))
return;
if (image->was_requested_by(esphome::camera::API_REQUESTER) || image->was_requested_by(esphome::camera::IDLE)) {
this->image_reader_->set_image(std::move(image));
// Try to send immediately to reduce latency
this->try_send_camera_image_();
if (!this->image_reader_) {
// Created on the first image this connection will send, so connections
// that never receive one never pay for a reader. Only a registered
// camera's listener can reach this, so instance() is non-null here.
this->image_reader_ = std::unique_ptr<camera::CameraImageReader>{camera::Camera::instance()->create_image_reader()};
}
this->image_reader_->set_image(std::move(image));
// Try to send immediately to reduce latency
this->try_send_camera_image_();
}
uint16_t APIConnection::try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
auto *camera = static_cast<camera::Camera *>(entity);
@@ -591,18 +591,21 @@ APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) {
*/
APIError APINoiseFrameHelper::init_handshake_() {
int err;
memset(&nid_, 0, sizeof(nid_));
// const char *proto = "Noise_NNpsk0_25519_ChaChaPoly_SHA256";
// err = noise_protocol_name_to_id(&nid_, proto, strlen(proto));
nid_.pattern_id = NOISE_PATTERN_NN;
nid_.cipher_id = NOISE_CIPHER_CHACHAPOLY;
nid_.dh_id = NOISE_DH_CURVE25519;
nid_.prefix_id = NOISE_PREFIX_STANDARD;
nid_.hybrid_id = NOISE_DH_NONE;
nid_.hash_id = NOISE_HASH_SHA256;
nid_.modifier_ids[0] = NOISE_MODIFIER_PSK0;
// Noise_NNpsk0_25519_ChaChaPoly_SHA256, built on the stack:
// noise_handshakestate_new_by_id copies it, so a member would waste
// 104 bytes per connection, and a static const would sit in RAM on
// ESP8266 (.rodata is DRAM there).
const NoiseProtocolId nid = {
.prefix_id = NOISE_PREFIX_STANDARD,
.pattern_id = NOISE_PATTERN_NN,
.modifier_ids = {NOISE_MODIFIER_PSK0},
.dh_id = NOISE_DH_CURVE25519,
.cipher_id = NOISE_CIPHER_CHACHAPOLY,
.hash_id = NOISE_HASH_SHA256,
.hybrid_id = NOISE_DH_NONE,
};
err = noise_handshakestate_new_by_id(&handshake_, &nid_, NOISE_ROLE_RESPONDER);
err = noise_handshakestate_new_by_id(&handshake_, &nid, NOISE_ROLE_RESPONDER);
APIError aerr =
handle_noise_error_(err, LOG_STR("noise_handshakestate_new_by_id"), APIError::HANDSHAKESTATE_SETUP_FAILED);
if (aerr != APIError::OK)
@@ -63,9 +63,6 @@ class APINoiseFrameHelper final : public APIFrameHelper {
// Buffer for noise handshake prologue (released after handshake)
APIBuffer prologue_;
// NoiseProtocolId (size depends on implementation)
NoiseProtocolId nid_;
// Group small types together
// Fixed-size header buffer for noise protocol:
// 1 byte for indicator + 2 bytes for message size (16-bit value, not varint)
+38 -6
View File
@@ -5,11 +5,11 @@ bring-up and the controller BLE address. Consumers (bk72xx_ble_tracker) build
on this component and contain no SDK calls of their own.
Supported SoCs (BLE 5.x): BK7231N/BK7236 (BLE 5.1), BK7238/BK7252N/BK7253
(BLE 5.2), and any future BLE-5.x SoC. Capability is detected at compile time,
not by a chip list: the C++ guards on `__has_include("ble_api.h")` the Beken
BLE 5.x public API header, which the LibreTiny beken-72xx builder ships only
for BLE-5.x SoCs. BK7231T/BK7251/BK7271 (BLE 4.2) and BK7231Q (no BLE) fail
with a clear #error.
(BLE 5.2), and any future BLE-5.x SoC. Known non-5.x families are rejected in
to_code; unknown families are capability-checked at compile time via
`__has_include("app_ble.h")`, a header only on the BLE 5.x include path
(ble_api.h ships for every SoC, so it cannot be the probe). A non-5.x build
fails with a clear #error.
No framework patch is needed: the LibreTiny beken-72xx builder already compiles
and links the BLE 5.x stack (CFG_SUPPORT_BLE=1 + CFG_BLE_VERSION=BLE_VERSION_5_x;
@@ -21,9 +21,16 @@ import logging
import esphome.codegen as cg
from esphome.components import libretiny
from esphome.components.libretiny.const import FAMILY_BK7231N, FAMILY_BK7238
from esphome.components.libretiny.const import (
FAMILY_BK7231N,
FAMILY_BK7231Q,
FAMILY_BK7231T,
FAMILY_BK7238,
FAMILY_BK7251,
)
import esphome.config_validation as cv
from esphome.const import CONF_ENABLE_ON_BOOT, CONF_ID
from esphome.core import EsphomeError
from esphome.types import ConfigType
DEPENDENCIES = ["bk72xx"]
@@ -50,7 +57,32 @@ CONFIG_SCHEMA = cv.Schema(
request_scan_listener_slot = cg.slot_counter("BK72XX_BLE_SCAN_LISTENER_COUNT")
def _unsupported_family_message(family: str) -> str | None:
if family in (FAMILY_BK7231T, FAMILY_BK7251):
return (
f"bk72xx_ble does not support {family}: this SoC has the Beken BLE 4.2 "
"stack; a BLE 5.x SoC such as BK7231N or BK7238 is required"
)
if family == FAMILY_BK7231Q:
return "bk72xx_ble does not support BK7231Q: this SoC has no BLE"
return None
def _final_validate(config: ConfigType) -> ConfigType:
# Warn only: a hard error here would break the validate-only CI fixtures,
# which run on a BLE 4.2 board. The hard error is raised at codegen.
if msg := _unsupported_family_message(libretiny.get_libretiny_family()):
_LOGGER.warning("%s (this configuration cannot compile)", msg)
return config
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config: ConfigType) -> None:
if msg := _unsupported_family_message(libretiny.get_libretiny_family()):
raise EsphomeError(msg)
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
+2 -2
View File
@@ -10,7 +10,7 @@
#ifdef USE_BK72XX_BLE
// Same SDK gate as bk72xx_ble.cpp (which carries the explanatory #error).
#if !defined(CLANG_TIDY) && __has_include("ble_api.h")
#if !defined(CLANG_TIDY) && __has_include("ble_api.h") && __has_include("app_ble.h")
extern "C" {
#include "app_ble.h" // app_ble_env, app_ble_run, app_ble_reset, actv_state_t,
@@ -115,5 +115,5 @@ BdkOpResult bdk_scan_release(uint8_t activity_idx, bool created, int *err_out) {
} // namespace esphome::bk72xx_ble
#endif // !CLANG_TIDY && ble_api.h
#endif // !CLANG_TIDY && ble_api.h && app_ble.h
#endif // USE_BK72XX_BLE
+13 -9
View File
@@ -34,22 +34,26 @@
// ---------------------------------------------------------------------------
// SDK-capability gate (not a chip allowlist).
// This component drives the Beken BLE *5.x* controller via its public API,
// `ble_api.h`, which the LibreTiny beken-72xx builder ships only for the
// BLE-5.x SoCs (it selects the `ble_pub` 5.x stack from CFG_BLE_VERSION; the
// 4.2 SoCs build a different, older API with no ble_api.h). Gate on the header
// itself so any BLE-5.x Beken chip — present or future — is supported without a
// hard-coded list, and a non-5.x build fails here with a clear message instead
// of a cryptic "ble_api.h: No such file or directory".
// This component drives the Beken BLE *5.x* controller. `ble_api.h` cannot be
// the probe: it ships for every SoC (driver/include) and merely switches on
// CFG_BLE_VERSION internally. `app_ble.h` is on the include path only when the
// LibreTiny beken-72xx builder selects a 5.x stack, so gating on it supports
// any BLE-5.x chip — present or future — without a hard-coded list, and a
// non-5.x build fails here with a clear message instead of a cryptic
// "app_ble.h: No such file or directory".
// ---------------------------------------------------------------------------
#if defined(CLANG_TIDY)
// The clang-tidy environment does not carry the full Beken BDK BLE 5.x API
// (its ble_api.h variant lacks parts of the 5.x surface), so there is nothing
// accurate to analyze the SDK calls against — skip the file under analysis.
#define BK72XX_BLE_NO_SDK
#elif !__has_include("ble_api.h")
#elif !__has_include("ble_api.h") || !__has_include("app_ble.h")
// Also skip the SDK body: #error does not stop the preprocessor, and on a 4.2
// SoC ble_api.h exists, so without the guard the 5.x symbols would fail one by
// one and bury this message.
#define BK72XX_BLE_NO_SDK
#error \
"bk72xx_ble requires a BLE 5.x Beken SDK (ble_api.h). Supported SoCs: BK7231N/BK7236 (BLE 5.1) and BK7238/BK7252N/BK7253 (BLE 5.2). BK7231T/BK7251/BK7271 (BLE 4.2) and BK7231Q (no BLE) are not supported."
"bk72xx_ble requires a BLE 5.x Beken SDK (app_ble.h). Supported SoCs: BK7231N/BK7236 (BLE 5.1) and BK7238/BK7252N/BK7253 (BLE 5.2). BK7231T/BK7251/BK7271 (BLE 4.2) and BK7231Q (no BLE) are not supported."
#endif
#ifndef BK72XX_BLE_NO_SDK
+14 -6
View File
@@ -37,7 +37,7 @@ from esphome.const import (
CONF_INTERVAL,
KEY_TARGET_PLATFORM,
)
from esphome.core import CORE, ID, KEY_CORE
from esphome.core import CORE, ID, KEY_CORE, TimePeriod
from esphome.types import ConfigType
CODEOWNERS = ["@Bl00d-B0b"]
@@ -243,19 +243,27 @@ def validate_scan_parameters(config: ConfigType) -> ConfigType:
return config
# The historical scan window default shared by the trackers that do not pin
# their own; also the fallback for esp32's conditional default.
DEFAULT_SCAN_WINDOW = "30ms"
def scan_parameters_schema(
interval_default: str,
*,
window_default: str = "30ms",
window_default: str | Callable[[], TimePeriod] = DEFAULT_SCAN_WINDOW,
) -> cv.All:
"""Build the scan_parameters value schema shared by all BLE trackers.
interval_default and window_default are per chip (e.g. esp32 320/30 ms,
bk72xx/rp2 100/30 ms the reference scan rates of the respective stacks;
LN882H's SDK recommends 100/50 ms). The `active` option (default on) is
unconditional: active scanning is part of the tracker contract every
current proxy client assumes it, so a passive-only tracker must not share
this schema.
LN882H's SDK recommends 100/50 ms). window_default may also be a zero-arg
callable evaluated per validation when the user omits the key (esp32 uses
this to record that the window was defaulted, so a later validation step
can adjust it once sibling keys are resolved). The `active` option
(default on) is unconditional: active scanning is part of the tracker
contract every current proxy client assumes it, so a passive-only
tracker must not share this schema.
"""
schema = {
cv.Optional(CONF_DURATION, default="5min"): cv.positive_time_period_seconds,
@@ -20,7 +20,7 @@
namespace esphome::bluetooth_connection {
static const char *const TAG = "bluetooth_connection.bluedroid";
static const char *const TAG = "bluetooth_connection";
using ble_device_base::FAST_CONN_TIMEOUT;
using ble_device_base::FAST_MAX_CONN_INTERVAL;
@@ -15,7 +15,7 @@
namespace esphome::bluetooth_connection {
static const char *const TAG = "bluetooth_connection.rp2";
static const char *const TAG = "bluetooth_connection";
using ble_device_base::ESPBTUUID;
using ble_device_base::GATT_ERR_NOT_CONNECTED;
+2 -1
View File
@@ -103,7 +103,8 @@ struct CameraImageSpec {
/** Abstract camera base class. Collaborates with API.
* 1) API server starts and registers as a listener (add_listener)
* to receive new images from the camera.
* 2) New API client connects and creates a new image reader (create_image_reader).
* 2) API connection creates an image reader (create_image_reader) when it receives
* the first image it will send.
* 3) API connection receives protobuf CameraImageRequest and calls request_image.
* 3.a) API connection receives protobuf CameraImageRequest and calls start_stream.
* 4) Camera implementation provides JPEG data in the CameraImage and notifies listeners.
+2
View File
@@ -22,6 +22,7 @@ CONF_GYROSCOPE_ODR = "gyroscope_odr"
CONF_GYROSCOPE_RANGE = "gyroscope_range"
CONF_IAQ = "iaq"
CONF_IGNORE_NOT_FOUND = "ignore_not_found"
CONF_LABEL = "label"
CONF_LIBRETINY = "libretiny"
CONF_LOOP = "loop"
CONF_NOX_INDEX = "nox_index"
@@ -35,6 +36,7 @@ CONF_REQUEST_HEADERS = "request_headers"
CONF_ROWS = "rows"
CONF_SCAN_PARAMETERS = "scan_parameters"
CONF_SHA256 = "sha256"
CONF_SLOT = "slot"
CONF_STATE_SAVE_INTERVAL = "state_save_interval"
CONF_STOP_BITS = "stop_bits"
CONF_TARGET_COUNT = "target_count"
@@ -5,7 +5,7 @@
namespace esphome::deep_sleep {
static const char *const TAG = "deep_sleep.bk72xx";
static const char *const TAG = "deep_sleep";
#ifdef USE_DEEP_SLEEP_ON_WAKE
WakeupCause get_wakeup_cause() {
@@ -3,6 +3,7 @@ import re
from esphome import automation, core
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
from esphome.components.const import CONF_LABEL
from esphome.components.number import Number
from esphome.components.select import Select
from esphome.components.switch import Switch
@@ -30,7 +31,6 @@ display_menu_base_ns = cg.esphome_ns.namespace("display_menu_base")
CONF_ROTARY = "rotary"
CONF_JOYSTICK = "joystick"
CONF_LABEL = "label"
CONF_MENU = "menu"
CONF_BACK = "back"
CONF_SELECT = "select"
+9 -1
View File
@@ -570,6 +570,9 @@ def get_download_types(storage_json):
the shape stable so the download panel
doesn't have to special-case per-platform schemas.
"""
# No recorded firmware path means nothing was built; no downloads.
if storage_json.firmware_bin_path is None:
return []
return [
{
"title": "Factory format (Previously Modern)",
@@ -3235,7 +3238,12 @@ def _write_idf_component_yml():
# Don't process arduino libraries
if name not in ARDUINO_DISABLED_LIBRARIES
]
for component in generate_idf_components(libraries):
# A library that is also declared as a managed component must not be
# converted as well, or IDF sees the same requirement from two
# components and refuses to build. Converted components still link
# against it via ${ESPHOME_PROJECT_MANAGED_COMPONENTS}.
managed = set(CORE.data[KEY_ESP32].get(KEY_COMPONENTS, {}))
for component in generate_idf_components(libraries, managed=managed):
dependencies[component.get_sanitized_name()] = {
"override_path": str(component.path)
}
+15 -14
View File
@@ -360,17 +360,6 @@ static bool has_fault_addr() {
return s_raw_crash_data.exception == PANIC_EXCEPTION_FAULT && !s_raw_crash_data.pseudo_excause;
}
// Append both cores' backtrace addresses to buf; returns the new position.
static int append_all_backtraces(char *buf, int size, int pos) {
pos = append_addrs_to_hint(buf, size, pos, s_raw_crash_data.backtrace, s_raw_crash_data.backtrace_count,
s_raw_crash_data.reg_frame_count);
#if SOC_CPU_CORES_NUM > 1
pos = append_addrs_to_hint(buf, size, pos, s_raw_crash_data.other_backtrace, s_raw_crash_data.other_backtrace_count,
s_raw_crash_data.other_reg_frame_count);
#endif
return pos;
}
// The record was captured by a different firmware build (it survives soft
// resets, including the OTA reboot), so symbolizing its addresses against the
// current ELF would produce misleading symbols. Print them with lowercase
@@ -443,11 +432,23 @@ void crash_handler_log() {
}
#endif
// Build addr2line hint with all captured addresses for easy copy-paste
// Build addr2line hints for easy copy-paste. One line per core: the two
// backtraces are separate stacks, and a combined list decodes as one
// impossible call chain (and can overflow the buffer, dropping addresses).
static const char *const ADDR2LINE_CMD = "addr2line -pfiaC -e firmware.elf";
char hint[256];
int pos = snprintf(hint, sizeof(hint), "Use: addr2line -pfiaC -e firmware.elf 0x%08" PRIX32, s_raw_crash_data.pc);
append_all_backtraces(hint, sizeof(hint), pos);
int pos = snprintf(hint, sizeof(hint), "Use: %s 0x%08" PRIX32, ADDR2LINE_CMD, s_raw_crash_data.pc);
append_addrs_to_hint(hint, sizeof(hint), pos, s_raw_crash_data.backtrace, s_raw_crash_data.backtrace_count,
s_raw_crash_data.reg_frame_count);
ESP_LOGE(TAG, "%s", hint);
#if SOC_CPU_CORES_NUM > 1
if (s_raw_crash_data.other_backtrace_count > 0) {
pos = snprintf(hint, sizeof(hint), "Other core: %s", ADDR2LINE_CMD);
append_addrs_to_hint(hint, sizeof(hint), pos, s_raw_crash_data.other_backtrace,
s_raw_crash_data.other_backtrace_count, s_raw_crash_data.other_reg_frame_count);
ESP_LOGE(TAG, "%s", hint);
}
#endif
}
} // namespace esphome::esp32
+2
View File
@@ -648,6 +648,8 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa
case ESP_GAP_BLE_SET_PKT_LENGTH_COMPLETE_EVT:
case ESP_GAP_BLE_PHY_UPDATE_COMPLETE_EVT: // BLE 5.0 PHY update complete
case ESP_GAP_BLE_CHANNEL_SELECT_ALGORITHM_EVT: // BLE 5.0 channel selection algorithm
case ESP_GAP_BLE_LOCAL_IR_EVT: // Local identity root key generated at security init
case ESP_GAP_BLE_LOCAL_ER_EVT: // Local encryption root key generated at security init
return;
default:
@@ -1,5 +1,7 @@
from __future__ import annotations
import copy
from dataclasses import dataclass
import logging
from esphome import automation
@@ -8,6 +10,7 @@ from esphome.components import ble_device_base, esp32_ble, ota
from esphome.components.const import CONF_ON_SCAN_END, CONF_SCAN_PARAMETERS, CONF_WINDOW
from esphome.components.esp32 import (
add_idf_sdkconfig_option,
idf_version,
request_bluetooth,
request_software_coexistence,
)
@@ -35,10 +38,12 @@ from esphome.const import (
CONF_SERVICE_UUID,
CONF_TRIGGER_ID,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core import CORE, CoroPriority, TimePeriod, coroutine_with_priority
from esphome.enum import StrEnum
from esphome.types import ConfigType
DOMAIN = "esp32_ble_tracker"
AUTO_LOAD = ["ble_device_base", "esp32_ble"]
DEPENDENCIES = ["esp32"]
CODEOWNERS = ["@bdraco"]
@@ -125,10 +130,71 @@ def validate_max_connections_deprecated(config: ConfigType) -> ConfigType:
return config
# ESP-IDF 5.5.5 fixed a coexistence bug on the ESP32 where BLE scans ran far
# longer than the configured window (espressif/esp-idf#18931). Before the fix,
# the default 30 ms window in a 320 ms interval effectively scanned at a much
# higher duty cycle than requested; with the fix, that same default only
# listens 9.4 % of the time and misses most advertisements when wifi shares
# the radio. Espressif recommends setting the window equal to the interval in
# that case: the coexistence arbiter still shares the radio with wifi, and
# BLE uses the airtime wifi does not claim.
IDF_SCAN_WINDOW_FIX_VERSION = cv.Version(5, 5, 5)
@dataclass
class TrackerData:
"""Per-run validation state, namespaced under DOMAIN in CORE.data."""
scan_window_defaulted: bool = False
def _get_data() -> TrackerData:
if DOMAIN not in CORE.data:
CORE.data[DOMAIN] = TrackerData()
return CORE.data[DOMAIN]
def _scan_window_default() -> TimePeriod:
"""Schema default for the scan window.
Records that the user did not set a window, so _raise_defaulted_scan_window
can tell a defaulted 30 ms from an explicit one; the raise itself must wait
for the outer schema because it depends on software_coexistence, a sibling
key not yet resolved here.
"""
_get_data().scan_window_defaulted = True
return cv.positive_time_period(ble_device_base.DEFAULT_SCAN_WINDOW)
def _raise_defaulted_scan_window(config: ConfigType) -> ConfigType:
"""Raise a defaulted scan window to the interval where that is safe.
Only when the coexistence arbiter is compiled in (software_coexistence,
present iff wifi is configured and not disabled by the user) and the IDF
honors the window strictly (>= 5.5.5); without the arbiter a full-duty
scan would starve wifi outright, and a user-set window is never touched.
Raising to the interval cannot invalidate the already-validated
parameters, so no re-validation is needed.
"""
if (
_get_data().scan_window_defaulted
and config.get(CONF_SOFTWARE_COEXISTENCE)
and idf_version() >= IDF_SCAN_WINDOW_FIX_VERSION
):
params = config[CONF_SCAN_PARAMETERS]
# Copy so the config dump shows a plain value instead of a YAML
# anchor/alias pair pointing at the interval.
params[CONF_WINDOW] = copy.copy(params[CONF_INTERVAL])
return config
# 320 ms is the ESP-IDF reference scan interval; the shared schema also
# tightens validation to the controller's 2.5 ms .. 10240 ms range and rejects
# window/interval pairs that collapse to the same 0.625 ms unit count.
SCAN_PARAMETERS_SCHEMA = ble_device_base.scan_parameters_schema("320ms")
# The window default is conditional (see _scan_window_default above).
SCAN_PARAMETERS_SCHEMA = ble_device_base.scan_parameters_schema(
"320ms", window_default=_scan_window_default
)
# Codegen helpers are owned by ble_device_base; kept under the historical names
# here for the components that import them from this module.
@@ -183,6 +249,7 @@ CONFIG_SCHEMA = cv.All(
}
).extend(cv.COMPONENT_SCHEMA),
validate_max_connections_deprecated,
_raise_defaulted_scan_window,
)
+25 -12
View File
@@ -3,7 +3,7 @@ from pathlib import Path
from esphome import pins
from esphome.components import esp32
from esphome.components.const import CONF_USE_PSRAM
from esphome.components.const import CONF_SLOT, CONF_USE_PSRAM
import esphome.config_validation as cv
from esphome.const import (
CONF_CLK_PIN,
@@ -16,8 +16,10 @@ from esphome.const import (
CONF_VARIANT,
)
from esphome.cpp_generator import add_define
from esphome.types import ConfigType
CODEOWNERS = ["@swoboda1337"]
DEPENDENCIES = ["esp32"]
# esp32_ble raises the task watchdog around the remote BT controller bring-up
AUTO_LOAD = ["watchdog"]
@@ -33,7 +35,6 @@ CONF_DATA_READY_PIN = "data_ready_pin"
CONF_HANDSHAKE_ACTIVE_HIGH = "handshake_active_high"
CONF_HANDSHAKE_PIN = "handshake_pin"
CONF_SDIO_FREQUENCY = "sdio_frequency"
CONF_SLOT = "slot"
CONF_SPI_MODE = "spi_mode"
# Shared fields for both transport modes
@@ -125,6 +126,22 @@ CONFIG_SCHEMA = cv.typed_schema(
)
def _final_validate(config: ConfigType) -> ConfigType:
# The esp_hosted releases compatible with older ESP-IDF versions crash at
# boot with a heap double free in the SDIO RX path (fixed in esp_hosted
# 2.11.0, which requires ESP-IDF 5.3), so reject them at validation time.
if (idf_ver := esp32.idf_version()) < cv.Version(5, 3, 0):
raise cv.Invalid(
f"esp32_hosted requires ESP-IDF 5.3 or newer, got {idf_ver}. "
"Remove the framework version from your configuration to use the "
"recommended version, or pin a version at or above 5.3."
)
return config
FINAL_VALIDATE_SCHEMA = _final_validate
def _configure_sdio(config):
slot = config[CONF_SLOT]
esp32.add_idf_sdkconfig_option(
@@ -252,18 +269,14 @@ async def to_code(config):
if config[CONF_USE_PSRAM]:
esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_MEMPOOL_PREFER_SPIRAM", True)
# Library versions
# Library versions; this component set requires ESP-IDF 5.3 or newer,
# which is enforced at validation time.
idf_ver = esp32.idf_version()
os.environ["ESP_IDF_VERSION"] = f"{idf_ver.major}.{idf_ver.minor}"
if idf_ver >= cv.Version(5, 5, 0):
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.6.3")
esp32.add_idf_component(name="espressif/wifi_remote_over_eppp", ref="0.3.3")
esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.5")
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.12.12")
else:
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.13.0")
esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0")
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.0.11")
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.6.3")
esp32.add_idf_component(name="espressif/wifi_remote_over_eppp", ref="0.3.3")
esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.5")
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.12.12")
esp32.add_extra_script(
"post",
"esp32_hosted.py",
+3
View File
@@ -113,6 +113,9 @@ def get_download_types(storage_json):
the shape stable so the download panel
doesn't have to special-case per-platform schemas.
"""
# No recorded firmware path means nothing was built; no downloads.
if storage_json.firmware_bin_path is None:
return []
return [
{
"title": "Standard format",
+1 -1
View File
@@ -355,7 +355,7 @@ def _validate(config):
" clk:\n"
" mode: %s\n"
" pin: %s\n"
"Removal scheduled for 2026.9.0.",
"Removal scheduled for 2026.11.0.",
config[CONF_CLK_MODE],
mode,
pin,
@@ -16,7 +16,7 @@
namespace esphome::http_request {
static const char *const TAG = "http_request.arduino";
static const char *const TAG = "http_request";
#ifdef USE_ESP8266
// ESP8266 Arduino core (WiFiClientSecureBearSSL.cpp) returns -1000 on OOM
static constexpr int ESP8266_SSL_ERR_OOM = -1000;
@@ -14,7 +14,7 @@
namespace esphome::http_request {
static const char *const TAG = "http_request.host";
static const char *const TAG = "http_request";
std::shared_ptr<HttpContainer> HttpRequestHost::perform(const std::string &url, const std::string &method,
const std::string &body,
@@ -16,7 +16,7 @@
namespace esphome::http_request {
static const char *const TAG = "http_request.idf";
static const char *const TAG = "http_request";
static constexpr uint32_t ERROR_DURATION_MS = 1000;
void HttpRequestIDF::dump_config() {
+1 -1
View File
@@ -9,7 +9,7 @@
namespace esphome::i2c {
static const char *const TAG = "i2c.arduino";
static const char *const TAG = "i2c";
// Maximum bytes to log in hex format (truncates larger transfers)
static constexpr size_t I2C_MAX_LOG_BYTES = 32;
+1 -1
View File
@@ -12,7 +12,7 @@
namespace esphome::i2c {
static const char *const TAG = "i2c.idf";
static const char *const TAG = "i2c";
// Maximum bytes to log in hex format (truncates larger transfers)
static constexpr size_t I2C_MAX_LOG_BYTES = 32;
+1 -1
View File
@@ -16,7 +16,7 @@
namespace esphome::i2c {
static const char *const TAG = "i2c.host";
static const char *const TAG = "i2c";
HostI2CBus::~HostI2CBus() {
if (this->file_descriptor_ != -1) {
+1 -1
View File
@@ -6,7 +6,7 @@
namespace esphome::i2c {
static const char *const TAG = "i2c.zephyr";
static const char *const TAG = "i2c";
static const char *get_speed(uint32_t dev_config) {
switch (I2C_SPEED_GET(dev_config)) {
+6 -2
View File
@@ -154,8 +154,12 @@ bool Infrared::on_receive(remote_base::RemoteReceiveData data) {
// Forward received IR data to API server
#if defined(USE_API) && defined(USE_IR_RF)
if (api::global_api_server != nullptr) {
api::global_api_server->send_infrared_rf_receive_event(this->get_device_id_or_zero(), this->get_entity_key(),
&data.get_raw_data());
#ifdef USE_DEVICES
uint32_t device_id = this->get_device_id();
#else
uint32_t device_id = 0;
#endif
api::global_api_server->send_infrared_rf_receive_event(device_id, this->get_object_id_hash(), &data.get_raw_data());
}
#endif
return false; // Don't consume the event, allow other listeners to process it
@@ -9,7 +9,7 @@ uint32_t temp_single_get_current_temperature(uint32_t *temp_value);
namespace esphome::internal_temperature {
static const char *const TAG = "internal_temperature.bk72xx";
static const char *const TAG = "internal_temperature";
void InternalTemperatureSensor::update() {
float temperature = NAN;
@@ -16,7 +16,7 @@ uint8_t temprature_sens_read();
namespace esphome::internal_temperature {
static const char *const TAG = "internal_temperature.esp32";
static const char *const TAG = "internal_temperature";
void InternalTemperatureSensor::update() {
float temperature = NAN;
@@ -16,7 +16,7 @@
namespace esphome::internal_temperature {
static const char *const TAG = "internal_temperature.rp2";
static const char *const TAG = "internal_temperature";
// The on-die temperature sensor sits on the last ADC channel: input 4 on RP2040
// and RP2350A, but input 8 on RP2350B, which has eight external channels rather
@@ -8,7 +8,7 @@
namespace esphome::internal_temperature {
static const char *const TAG = "internal_temperature.zephyr";
static const char *const TAG = "internal_temperature";
static const struct device *const DIE_TEMPERATURE_SENSOR = DEVICE_DT_GET_ONE(nordic_nrf_temp);
-2
View File
@@ -184,8 +184,6 @@ static int32_t get_firmware_int(const char *version_string) {
return result;
}
float LD2420Component::get_setup_priority() const { return setup_priority::BUS; }
void LD2420Component::dump_config() {
ESP_LOGCONFIG(TAG,
"LD2420:\n"
-1
View File
@@ -105,7 +105,6 @@ class LD2420Component final : public Component, public uart::UARTDevice {
void apply_config_action();
void factory_reset_action();
void revert_config_action();
float get_setup_priority() const override;
int send_cmd_from_array(CmdFrameT cmd_frame);
void report_gate_data();
void handle_cmd_error(uint16_t error);
+3
View File
@@ -182,6 +182,9 @@ def get_download_types(storage_json: StorageJSON = None):
the shape stable so the download panel
doesn't have to special-case per-platform schemas.
"""
# No recorded firmware path means nothing was built; no downloads.
if storage_json.firmware_bin_path is None:
return []
types = [
{
"title": "UF2 package (recommended)",
+1 -1
View File
@@ -444,7 +444,7 @@ LVTouchListener::LVTouchListener(uint16_t long_press_time, uint16_t long_press_r
lv_indev_set_type(this->drv_, LV_INDEV_TYPE_POINTER);
lv_indev_set_disp(this->drv_, parent->get_disp());
lv_indev_set_long_press_time(this->drv_, long_press_time);
// long press repeat time TBD
lv_indev_set_long_press_repeat_time(this->drv_, long_press_repeat_time);
lv_indev_set_user_data(this->drv_, this);
lv_indev_set_read_cb(this->drv_, [](lv_indev_t *d, lv_indev_data_t *data) {
auto *l = static_cast<LVTouchListener *>(lv_indev_get_user_data(d));
+1 -2
View File
@@ -1,3 +1,4 @@
from esphome.components.const import CONF_LABEL
import esphome.config_validation as cv
from esphome.const import CONF_TEXT
@@ -14,8 +15,6 @@ from ..schemas import TEXT_SCHEMA
from ..types import LvText
from . import Widget, WidgetType
CONF_LABEL = "label"
class LabelType(WidgetType):
def __init__(self):
-63
View File
@@ -63,7 +63,6 @@ from esphome.const import (
PlatformFramework,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import ObjectIdEntity, validate_no_object_id_conflicts
from esphome.types import ConfigType
DEPENDENCIES = ["network"]
@@ -333,68 +332,6 @@ CONFIG_SCHEMA = cv.All(
)
# Platforms whose MQTT components subscribe to an object_id-derived command topic.
# Keep in sync with the platforms extending cv.MQTT_COMMAND_COMPONENT_SCHEMA, plus
# text, whose MQTT component subscribes a command topic that cannot be overridden.
_COMMAND_TOPIC_PLATFORMS = frozenset(
{
"alarm_control_panel",
"button",
"climate",
"cover",
"datetime",
"fan",
"light",
"lock",
"number",
"select",
"switch",
"text",
"update",
"valve",
}
)
# Platforms whose MQTT components derive extra sub-topics (position/command,
# mode/command, speed/command, ...) from the object_id, each with its own config
# key; custom state and command topics cannot exempt them from conflicting.
_SUB_TOPIC_PLATFORMS = frozenset({"climate", "cover", "fan", "valve"})
def _topics_conflict(entities: list[ObjectIdEntity], config: ConfigType) -> bool:
"""Check whether more than one entity actually uses an object_id-derived topic.
An empty topic_prefix disables default topics entirely, custom state and
command topics avoid the default topics, and disabling discovery (globally
or per entity) avoids the discovery config topic.
"""
if config[CONF_TOPIC_PREFIX]:
platform = entities[0].platform
if platform in _SUB_TOPIC_PLATFORMS:
return True
if sum(CONF_STATE_TOPIC not in entity.config for entity in entities) > 1:
return True
if (
platform in _COMMAND_TOPIC_PLATFORMS
and sum(CONF_COMMAND_TOPIC not in entity.config for entity in entities) > 1
):
return True
if not config[CONF_DISCOVERY]:
return False
discovery_entities = sum(
entity.config.get(CONF_DISCOVERY, True) for entity in entities
)
return discovery_entities > 1
FINAL_VALIDATE_SCHEMA = validate_no_object_id_conflicts(
"mqtt builds default topics and discovery topics from the entity object_id, "
"which is the name converted to ASCII",
conflict_filter=_topics_conflict,
)
def exp_mqtt_message(config):
if config is None:
return cg.optional(cg.TemplateArguments(MQTTMessage))
@@ -10,7 +10,7 @@
namespace esphome::mqtt {
static const char *const TAG = "mqtt.idf";
static const char *const TAG = "mqtt";
bool MQTTBackendESP32::initialize_() {
mqtt_cfg_.broker.address.hostname = this->host_.c_str();
@@ -13,7 +13,7 @@
namespace esphome::nextion {
static const char *const TAG = "nextion.upload.arduino";
static const char *const TAG = "nextion.upload";
static constexpr size_t NEXTION_MAX_RESPONSE_LOG_BYTES = 16;
// Timeout for display acknowledgment during TFT upload (ms).
@@ -16,7 +16,7 @@
namespace esphome::nextion {
static const char *const TAG = "nextion.upload.esp32";
static const char *const TAG = "nextion.upload";
static constexpr size_t NEXTION_MAX_RESPONSE_LOG_BYTES = 16;
// Timeout for display acknowledgment during TFT upload (ms).
+3
View File
@@ -473,6 +473,9 @@ def copy_files() -> None:
def get_download_types(storage_json: StorageJSON) -> list[dict[str, str]]:
"""Get the download types for the firmware."""
# No recorded firmware path means nothing was built; no downloads.
if storage_json.firmware_bin_path is None:
return []
types = []
UF2_PATH = "zephyr/zephyr.uf2"
DFU_PATH = "firmware.zip"
@@ -9,7 +9,7 @@
namespace esphome::ota {
static const char *const TAG = "ota.arduino_libretiny";
static const char *const TAG = "ota";
std::unique_ptr<ArduinoLibreTinyOTABackend> make_ota_backend() { return make_unique<ArduinoLibreTinyOTABackend>(); }
@@ -11,7 +11,7 @@
namespace esphome::ota {
static const char *const TAG = "ota.arduino_rp2";
static const char *const TAG = "ota";
std::unique_ptr<ArduinoRP2OTABackend> make_ota_backend() { return make_unique<ArduinoRP2OTABackend>(); }
@@ -46,7 +46,7 @@ static constexpr size_t MIN_BUFFER_SIZE = 256;
namespace esphome::ota {
static const char *const TAG = "ota.esp8266";
static const char *const TAG = "ota";
std::unique_ptr<ESP8266OTABackend> make_ota_backend() { return make_unique<ESP8266OTABackend>(); }
@@ -15,7 +15,7 @@
namespace esphome::ota {
static const char *const TAG = "ota.idf";
static const char *const TAG = "ota";
std::unique_ptr<IDFOTABackend> make_ota_backend() { return make_unique<IDFOTABackend>(); }
+1 -1
View File
@@ -27,7 +27,7 @@ namespace esphome::ota {
namespace {
const char *const TAG = "ota.host";
const char *const TAG = "ota";
constexpr size_t MAX_OTA_SIZE = 256u * 1024u * 1024u; // 256 MiB
constexpr size_t HEADER_PEEK_SIZE = 64;
@@ -11,7 +11,7 @@
namespace esphome::ota {
static const char *const TAG = "ota.idf";
static const char *const TAG = "ota";
OTAResponseTypes IDFOTABackend::register_and_validate_bootloader_part_() {
// Register the bootloader partition
@@ -16,7 +16,7 @@
namespace esphome::ota {
static const char *const TAG = "ota.idf";
static const char *const TAG = "ota";
static inline bool check_overlap(uint32_t a_offset, size_t a_size, uint32_t b_offset, size_t b_size) {
return (a_offset + a_size > b_offset && b_offset + b_size > a_offset);
@@ -31,7 +31,7 @@
namespace esphome::ota {
static const char *const TAG = "ota.idf";
static const char *const TAG = "ota";
// Route the "Signature check: " prefix (and its per-block form) through one
// shared format string each, so the prefix is pooled once by the linker instead
@@ -3,7 +3,6 @@ from esphome.components import web_server_base
from esphome.components.web_server_base import CONF_WEB_SERVER_BASE_ID
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_INCLUDE_INTERNAL, CONF_NAME, CONF_RELABEL
from esphome.core.entity_helpers import validate_no_object_id_conflicts
from esphome.cpp_types import EntityBase
AUTO_LOAD = ["web_server_base"]
@@ -36,11 +35,6 @@ CONFIG_SCHEMA = cv.Schema(
},
).extend(cv.COMPONENT_SCHEMA)
FINAL_VALIDATE_SCHEMA = validate_no_object_id_conflicts(
"prometheus builds metric labels from the entity object_id, "
"which is the name converted to ASCII"
)
async def to_code(config):
paren = await cg.get_variable(config[CONF_WEB_SERVER_BASE_ID])
@@ -99,8 +99,12 @@ bool RadioFrequency::on_receive(remote_base::RemoteReceiveData data) {
// Forward received RF data to API server
#if defined(USE_API) && defined(USE_RADIO_FREQUENCY)
if (api::global_api_server != nullptr) {
api::global_api_server->send_infrared_rf_receive_event(this->get_device_id_or_zero(), this->get_entity_key(),
&data.get_raw_data());
#ifdef USE_DEVICES
uint32_t device_id = this->get_device_id();
#else
uint32_t device_id = 0;
#endif
api::global_api_server->send_infrared_rf_receive_event(device_id, this->get_object_id_hash(), &data.get_raw_data());
}
#endif
return false; // Don't consume the event, allow other listeners to process it
@@ -9,7 +9,7 @@
namespace esphome::remote_receiver {
static const char *const TAG = "remote_receiver.esp32";
static const char *const TAG = "remote_receiver";
static bool IRAM_ATTR HOT rmt_callback(rmt_channel_handle_t channel, const rmt_rx_done_event_data_t *event, void *arg) {
RemoteReceiverComponentStore *store = (RemoteReceiverComponentStore *) arg;
@@ -220,7 +220,7 @@ void RotaryEncoderSensor::loop() {
}
if (this->pin_i_ != nullptr && this->pin_i_->digital_read()) {
this->store_.counter = 0;
this->store_.counter = std::clamp<int32_t>(0, this->store_.min_value, this->store_.max_value);
}
int counter = this->store_.counter;
if (this->store_.last_read != counter || this->publish_initial_value_) {
+3
View File
@@ -156,6 +156,9 @@ def get_download_types(storage_json):
the shape stable so the download panel
doesn't have to special-case per-platform schemas.
"""
# No recorded firmware path means nothing was built; no downloads.
if storage_json.firmware_bin_path is None:
return []
return [
{
"title": "UF2 factory format",
@@ -3,6 +3,7 @@
from esphome import automation
import esphome.codegen as cg
from esphome.components import runtime_image
from esphome.components.const import CONF_SLOT
from esphome.components.image import CONF_TRANSPARENCY, Image_, add_metadata
import esphome.config_validation as cv
from esphome.const import (
@@ -45,7 +46,6 @@ MAX_IMAGE_DIMENSION = 32767
MAX_DISPLAY_OFFSET = cv.TimePeriod(seconds=60)
MIN_DISPLAY_OFFSET = cv.TimePeriod(seconds=-60)
CONF_SLOT = "slot"
CONF_CURRENT_IMAGE = "current_image"
CONF_TRANSITION_IMAGE = "transition_image"
CONF_ON_IMAGE_DISPLAY = "on_image_display"
+6 -4
View File
@@ -283,8 +283,11 @@ DeltaFilter::DeltaFilter(float min_a0, float min_a1, float max_a0, float max_a1)
void DeltaFilter::set_baseline(float (*fn)(float)) { this->baseline_ = fn; }
optional<float> DeltaFilter::new_value(float value) {
// Always yield the first value.
if (std::isnan(this->last_value_)) {
const bool no_value = std::isnan(value);
const bool no_reference = std::isnan(this->last_value_);
if (no_value && no_reference)
return {};
if (no_value || no_reference) {
this->last_value_ = value;
return value;
}
@@ -293,8 +296,7 @@ optional<float> DeltaFilter::new_value(float value) {
float min = fabsf(this->min_a0_ + ref * this->min_a1_);
float max = fabsf(this->max_a0_ + ref * this->max_a1_);
float delta = fabsf(value - ref);
// if there is no reference, e.g. for the first value, just accept this one,
// otherwise accept only if within range.
// accept only if within range
if (delta > min && delta <= max) {
this->last_value_ = value;
return value;
@@ -43,7 +43,7 @@ namespace esphome::socket {
// (Ethernet). On ESP8266, it's a no-op.
#define LWIP_LOCK() esphome::LwIPLock lwip_lock_guard // NOLINT
static const char *const TAG = "socket.lwip";
static const char *const TAG = "socket";
// set to 1 to enable verbose lwip logging
#if 0 // NOLINT(readability-avoid-unconditional-preprocessor-if)
+1 -1
View File
@@ -4,7 +4,7 @@
namespace esphome::spi {
#if defined(USE_ARDUINO) && !defined(USE_ESP32)
static const char *const TAG = "spi-esp-arduino";
static const char *const TAG = "spi";
class SPIDelegateHw : public SPIDelegate {
public:
SPIDelegateHw(SPIInterface channel, uint32_t data_rate, SPIBitOrder bit_order, SPIMode mode, GPIOPin *cs_pin)
+1 -1
View File
@@ -4,7 +4,7 @@
namespace esphome::spi {
#ifdef USE_ESP32
static const char *const TAG = "spi-esp-idf";
static const char *const TAG = "spi";
static const size_t MAX_TRANSFER_SIZE = 4092; // dictated by ESP-IDF API.
class SPIDelegateHw : public SPIDelegate {
@@ -20,14 +20,18 @@ void TemplateText::setup() {
// Need std::string for pref_->setup() to fill from flash
std::string value{this->initial_value_ != nullptr ? this->initial_value_ : ""};
uint32_t extra = 0;
extra += this->traits.get_min_length() << 2;
extra += this->traits.get_max_length() << 4;
extra += fnv1_hash(this->traits.get_pattern_c_str()) << 6;
// TextSaver::setup() picks the key for the platform and migrates old data once
uint32_t key = this->preference_key_base_() + extra;
uint32_t old_key = this->old_preference_key_base_() + extra;
this->pref_->setup(key, old_key, value);
// For future hash migration: use migrate_entity_preference_() with:
// old_key = get_preference_hash() + extra
// new_key = get_preference_hash_v2() + extra
// See: https://github.com/esphome/backlog/issues/85
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
uint32_t key = this->get_preference_hash();
#pragma GCC diagnostic pop
key += this->traits.get_min_length() << 2;
key += this->traits.get_max_length() << 4;
key += fnv1_hash(this->traits.get_pattern_c_str()) << 6;
this->pref_->setup(key, value);
if (!value.empty())
this->publish_state(value);
}
@@ -14,9 +14,7 @@ class TemplateTextSaverBase {
public:
virtual bool save(const std::string &value) { return true; }
/// old_id is the pre-2026.8.0 preference key; data stored under it is moved to id once.
/// See: https://github.com/esphome/backlog/issues/85
virtual void setup(uint32_t id, uint32_t old_id, std::string &value) {}
virtual void setup(uint32_t id, std::string &value) {}
protected:
ESPPreferenceObject pref_;
@@ -47,16 +45,11 @@ template<uint8_t SZ> class TextSaver : public TemplateTextSaverBase {
// Make the preference object. Fill the provided location with the saved data
// If it is available, else leave it alone
void setup(uint32_t id, uint32_t old_id, std::string &value) override {
char temp[SZ + 1];
#ifdef USE_PREFERENCE_KEY_LOOKUP
void setup(uint32_t id, std::string &value) override {
this->pref_ = global_preferences->make_preference<uint8_t[SZ + 1]>(id);
bool hasdata = migrate_preference(this->pref_, reinterpret_cast<uint8_t *>(temp), SZ + 1, old_id, id);
#else
// Slot-based backends keep the old key; it is only a validity tag on a positional slot
this->pref_ = global_preferences->make_preference<uint8_t[SZ + 1]>(old_id);
char temp[SZ + 1];
bool hasdata = this->pref_.load(&temp);
#endif
if (hasdata) {
size_t len = static_cast<uint8_t>(temp[0]);
@@ -14,7 +14,7 @@
namespace esphome::uart {
static const char *const TAG = "uart.arduino_esp8266";
static const char *const TAG = "uart";
bool ESP8266UartComponent::serial0_in_use = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
uint32_t ESP8266UartComponent::get_config() {
@@ -21,7 +21,7 @@
namespace esphome::uart {
static const char *const TAG = "uart.idf";
static const char *const TAG = "uart";
/// Check if a pin number matches one of the default UART0 GPIO pins.
/// These pins may have residual IOMUX state from the ROM bootloader that
@@ -98,7 +98,7 @@ speed_t get_baud(int baud) {
namespace esphome::uart {
static const char *const TAG = "uart.host";
static const char *const TAG = "uart";
HostUartComponent::~HostUartComponent() {
if (this->file_descriptor_ != -1) {
@@ -16,7 +16,7 @@
namespace esphome::uart {
static const char *const TAG = "uart.lt";
static const char *const TAG = "uart";
static const char *const UART_TYPE[] = {
"hardware",
@@ -13,7 +13,7 @@
namespace esphome::uart {
static const char *const TAG = "uart.arduino_rp2";
static const char *const TAG = "uart";
uint16_t RP2UartComponent::get_config() {
uint16_t config = 0;
@@ -249,7 +249,7 @@ void WebServerOTAComponent::setup() {
return;
}
// AsyncWebServer takes ownership of the handler and will delete it when the server is destroyed
// The handler lives for the life of the process; WebServerBase never destroys its server
base->add_handler(new OTARequestHandler(this)); // NOLINT
}
@@ -112,9 +112,18 @@ class AuthMiddlewareHandler : public MiddlewareHandler {
class WebServerBase final {
public:
// The AsyncWebServer is created once and intentionally never deleted: on Arduino
// platforms ESPAsyncWebServer owns its registered handlers, so destroying it would
// also destroy live components (e.g. the captive portal) out from under us.
// init()/deinit() refcount users and start/stop the listener; handlers are
// registered once at creation and survive listener restarts.
void init() {
if (this->initialized_) {
this->initialized_++;
this->initialized_++;
if (this->server_ != nullptr) {
if (this->initialized_ == 1) {
// Restart the listener after a previous deinit()
this->server_->begin();
}
return;
}
this->server_ = new AsyncWebServer(this->port_);
@@ -126,14 +135,13 @@ class WebServerBase final {
for (auto *handler : this->handlers_)
this->server_->addHandler(handler);
this->initialized_++;
}
void deinit() {
if (this->initialized_ == 0)
return; // unbalanced deinit()
this->initialized_--;
if (this->initialized_ == 0) {
delete this->server_;
this->server_ = nullptr;
this->server_->end();
}
}
AsyncWebServer *get_server() const { return this->server_; }
@@ -136,10 +136,21 @@ bool WiFiComponent::wifi_apply_power_save_() {
https://github.com/d-a-v/Arduino/blob/0e7d21e17144cfc5f53c016191daca8723e89ee8/libraries/ESP8266WiFi/src/ESP8266WiFiSTA.cpp#L251
*/
#undef netif_set_addr // need to call lwIP-v1.4 netif_set_addr()
#undef netif_set_down // need to call lwIP-v1.4 netif_set_down()
extern "C" {
struct netif *eagle_lwip_getif(int netif_index);
void netif_set_addr(struct netif *netif, const ip4_addr_t *ip, const ip4_addr_t *netmask, const ip4_addr_t *gw);
void netif_set_down(struct netif *netif);
};
// The SDK can free its WiFi connection node before taking the STA netif down, letting lwIP
// timers (e.g. IGMP reports armed by mDNS) transmit into the dead driver and crash in
// cnx_node_search; taking the netif down first makes the glue drop such frames (#18308).
static void sta_netif_down() {
struct netif *iface = eagle_lwip_getif(STATION_IF);
if (iface != nullptr)
netif_set_down(iface);
}
#endif
bool WiFiComponent::wifi_sta_ip_config_(const optional<ManualIP> &manual_ip) {
@@ -523,6 +534,9 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
global_wifi_component->sta_state_ = static_cast<uint8_t>(ESP8266WiFiSTAState::ERROR_FAILED);
}
global_wifi_component->error_from_callback_ = true;
#if LWIP_VERSION_MAJOR != 1
sta_netif_down();
#endif
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
global_wifi_component->pending_.disconnect = true;
#endif
@@ -536,6 +550,9 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
// https://lbsfilm.at/blog/wpa2-authenticationmode-downgrade-in-espressif-microprocessors
if (it.old_mode != AUTH_OPEN && it.new_mode == AUTH_OPEN) {
ESP_LOGW(TAG, "Potential Authmode downgrade detected, disconnecting");
#if LWIP_VERSION_MAJOR != 1
sta_netif_down();
#endif
wifi_station_disconnect();
global_wifi_component->error_from_callback_ = true;
}
@@ -719,8 +736,12 @@ bool WiFiComponent::wifi_scan_start_(bool passive) {
bool WiFiComponent::wifi_disconnect_() {
bool ret = true;
// Only call disconnect if interface is up
if (wifi_get_opmode() & WIFI_STA)
if (wifi_get_opmode() & WIFI_STA) {
#if LWIP_VERSION_MAJOR != 1
sta_netif_down();
#endif
ret = wifi_station_disconnect();
}
station_config conf{};
memset(&conf, 0, sizeof(conf));
ETS_UART_INTR_DISABLE();
@@ -307,6 +307,11 @@ void ZigbeeComponent::setup() {
return;
}
#endif
#ifdef CONFIG_ZB_ZCZR
ezb_bdb_set_router_rejoin_required(true);
#endif
ezb_aps_secur_enable_distributed_security(false);
ezb_nwk_set_min_join_lqi(32);
if (ezb_app_signal_add_handler(ZigbeeComponent::app_signal_handler) != ESP_OK) {
+1 -1
View File
@@ -285,7 +285,7 @@ async def attributes_to_code(
async def esp32_to_code(config: ConfigType) -> "MockObj":
add_idf_component(
name="espressif/esp-zigbee-lib",
ref="2.0.3",
ref="2.0.4",
)
# add sdkconfigs later so they can overwrite esp32 defaults
+4
View File
@@ -99,6 +99,10 @@ from esphome.schema_extractors import (
schema_extractor_registry,
schema_extractor_typed,
)
# Deprecated re-export for external components; remove before 2027.2.0
# pylint: disable-next=unused-import
from esphome.util import parse_esphome_version # noqa: F401
from esphome.voluptuous_schema import _Schema
from esphome.yaml_util import SensitiveStr, make_data_base
+1 -1
View File
@@ -4,7 +4,7 @@ from enum import Enum
from esphome.enum import StrEnum
__version__ = "2026.8.0b2"
__version__ = "2026.9.0-dev"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
VALID_SUBSTITUTIONS_CHARACTERS = (
+4 -4
View File
@@ -120,8 +120,8 @@ class Application {
// NOLINTBEGIN(bugprone-macro-parentheses)
#define ENTITY_TYPE_(type, singular, plural, count, upper) \
void register_##singular(type *obj) { this->plural##_.push_back(obj); } \
void register_##singular(type *obj, const char *name, uint32_t entity_key, uint32_t entity_fields) { \
obj->configure_entity_(name, entity_key, entity_fields); \
void register_##singular(type *obj, const char *name, uint32_t object_id_hash, uint32_t entity_fields) { \
obj->configure_entity_(name, object_id_hash, entity_fields); \
this->plural##_.push_back(obj); \
}
#define ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) \
@@ -329,7 +329,7 @@ class Application {
#define GET_ENTITY_METHOD(entity_type, entity_name, entities_member) \
entity_type *get_##entity_name##_by_key(uint32_t key, uint32_t device_id, bool include_internal = false) { \
for (auto *obj : this->entities_member##_) { \
if (obj->get_entity_key() == key && obj->get_device_id() == device_id && \
if (obj->get_object_id_hash() == key && obj->get_device_id() == device_id && \
(include_internal || !obj->is_internal())) \
return obj; \
} \
@@ -340,7 +340,7 @@ class Application {
#define GET_ENTITY_METHOD(entity_type, entity_name, entities_member) \
entity_type *get_##entity_name##_by_key(uint32_t key, bool include_internal = false) { \
for (auto *obj : this->entities_member##_) { \
if (obj->get_entity_key() == key && (include_internal || !obj->is_internal())) \
if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) \
return obj; \
} \
return nullptr; \
+20 -32
View File
@@ -8,7 +8,7 @@ namespace esphome {
static const char *const TAG = "entity_base";
void EntityBase::configure_entity_(const char *name, uint32_t entity_key, uint32_t entity_fields) {
void EntityBase::configure_entity_(const char *name, uint32_t object_id_hash, uint32_t entity_fields) {
this->name_ = StringRef(name);
if (this->name_.empty()) {
#ifdef USE_DEVICES
@@ -30,15 +30,15 @@ void EntityBase::configure_entity_(const char *name, uint32_t entity_key, uint32
}
}
this->flags_.has_own_name = false;
// Dynamic name - must calculate key at runtime
this->calc_entity_key_();
// Dynamic name - must calculate hash at runtime
this->calc_object_id_();
} else {
this->flags_.has_own_name = true;
// Static name - use pre-computed key if provided
if (entity_key != 0) {
this->entity_key_ = entity_key;
// Static name - use pre-computed hash if provided
if (object_id_hash != 0) {
this->object_id_hash_ = object_id_hash;
} else {
this->calc_entity_key_();
this->calc_object_id_();
}
}
// Unpack entity string table indices and flags from entity_fields.
@@ -147,15 +147,9 @@ std::string EntityBase::get_icon() const {
}
#endif // !USE_ESP8266
// Calculate the entity key directly from the raw name (no transformations)
void EntityBase::calc_entity_key_() { this->entity_key_ = fnv1_hash_bytes(this->name_.c_str(), this->name_.size()); }
// Reconstruct the OLD (pre-2026.8.0) object_id-based hash for preference key compatibility.
// Named entities historically used the hash pre-computed by Python code generation, which
// sanitized per UTF-8 code point; entities without their own name computed the hash at
// runtime per byte. See https://github.com/esphome/backlog/issues/85
uint32_t EntityBase::calc_old_object_id_hash_() const {
return fnv1_hash_object_id(this->name_.c_str(), this->name_.size(), this->flags_.has_own_name);
// Calculate Object ID Hash directly from name using snake_case + sanitize
void EntityBase::calc_object_id_() {
this->object_id_hash_ = fnv1_hash_object_id(this->name_.c_str(), this->name_.size());
}
size_t EntityBase::write_object_id_to(char *buf, size_t buf_size) const {
@@ -173,22 +167,16 @@ StringRef EntityBase::get_object_id_to(std::span<char, OBJECT_ID_MAX_LEN> buf) c
}
ESPPreferenceObject EntityBase::make_entity_preference_(size_t size, uint32_t version) {
// The old key hashed the sanitized object_id, so multiple entity names could collide on
// one key and overwrite each other's stored preferences; the new key hashes the raw name.
// See: https://github.com/esphome/backlog/issues/85
uint32_t old_key = this->old_preference_key_base_() ^ version;
#ifdef USE_PREFERENCE_KEY_LOOKUP
uint32_t new_key = this->preference_key_base_() ^ version;
auto pref = global_preferences->make_preference(size, new_key);
// All in-tree entity preferences fit the stack buffer, so migration never hits the heap
SmallBufferWithHeapFallback<64> buffer(size);
migrate_preference(pref, buffer.get(), size, old_key, new_key);
return pref;
#else
// Slot-based backends keep the old key: it is only a validity tag on a positional slot,
// so collisions cannot corrupt data there and keeping it preserves stored state.
return global_preferences->make_preference(size, old_key);
#endif
// The key hashes the sanitized object_id, so multiple entity names can collide on one
// key and overwrite each other's stored preferences ("Living Room" and "living_room",
// or two UTF-8 names that both sanitize to underscores). Keys hashed from the raw name
// fix this, but they change the entity key API clients track, which the Home Assistant
// esphome integration cannot handle yet. See: https://github.com/esphome/backlog/issues/85
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
uint32_t key = this->get_preference_hash() ^ version;
#pragma GCC diagnostic pop
return global_preferences->make_preference(size, key);
}
#ifdef USE_ENTITY_ICON
+38 -42
View File
@@ -73,17 +73,8 @@ class EntityBase {
// Get whether this Entity has its own name or it should use the device friendly_name.
bool has_own_name() const { return this->flags_.has_own_name; }
// Get the unique key of this Entity: FNV-1 hash of the raw entity name.
// This is the key sent to API clients and used to route entity state.
uint32_t get_entity_key() const { return this->entity_key_; }
/// Returns the LEGACY object_id hash, unchanged from previous releases, so existing
/// callers keep getting stable values (for example preference keys). This is no longer
/// the key sent to API clients; that is get_entity_key().
ESPDEPRECATED("Use get_entity_key() for the entity key sent to API clients, or "
"make_entity_preference<T>() for preference storage. Will be removed in 2027.1.0.",
"2026.8.0")
uint32_t get_object_id_hash() const { return this->calc_old_object_id_hash_(); }
// Get the unique Object ID of this Entity
uint32_t get_object_id_hash() const { return this->object_id_hash_; }
/// Get object_id with zero heap allocation
/// For static case: returns StringRef to internal storage (buffer unused)
@@ -190,23 +181,39 @@ class EntityBase {
// Set has_state - for components that need to manually set this
void set_has_state(bool state) { this->flags_.has_state = state; }
/// Get this entity's device id, or 0 when devices are not compiled in (main device).
uint32_t get_device_id_or_zero() const {
#ifdef USE_DEVICES
return this->get_device_id();
#else
return 0;
#endif
}
/// Get the LEGACY preference key: FNV-1 hash of the sanitized object_id, XOR device_id.
/// Intentionally keeps the old algorithm so external callers that store preferences under
/// this key keep stable keys; make_entity_preference() migrates to the new raw-name key,
/// this method never will.
/**
* @brief Get a unique hash for storing preferences/settings for this entity.
*
* This method returns a hash that uniquely identifies the entity for the purpose of
* storing preferences (such as calibration, state, etc.). Unlike get_object_id_hash(),
* this hash also incorporates the device_id (if devices are enabled), ensuring uniqueness
* across multiple devices that may have entities with the same object_id.
*
* Use this method when storing or retrieving preferences/settings that should be unique
* per device-entity pair. Use get_object_id_hash() when you need a hash that identifies
* the entity regardless of the device it belongs to.
*
* For backward compatibility, if device_id is 0 (the main device), the hash is unchanged
* from previous versions, so existing single-device configurations will continue to work.
*
* @return uint32_t The unique hash for preferences, including device_id if available.
* @deprecated Use make_entity_preference<T>() instead, or preferences won't be migrated.
* See https://github.com/esphome/backlog/issues/85
*/
ESPDEPRECATED("Use make_entity_preference<T>() instead, or preferences won't be migrated. "
"See https://github.com/esphome/backlog/issues/85. Will be removed in 2027.1.0.",
"2026.8.0")
uint32_t get_preference_hash() { return this->old_preference_key_base_(); }
"2026.7.0")
uint32_t get_preference_hash() {
#ifdef USE_DEVICES
// Combine object_id_hash with device_id to ensure uniqueness across devices
// Note: device_id is 0 for the main device, so XORing with 0 preserves the original hash
// This ensures backward compatibility for existing single-device configurations
return this->get_object_id_hash() ^ this->get_device_id();
#else
// Without devices, just use object_id_hash as before
return this->get_object_id_hash();
#endif
}
/// Create a preference object for storing this entity's state/settings.
/// @tparam T The type of data to store (must be trivially copyable)
@@ -223,9 +230,9 @@ class EntityBase {
// before push_back, so codegen can emit a single combined call per entity.
friend class Application;
/// Combined entity setup from codegen: set name, entity key, entity string indices, and flags.
/// Combined entity setup from codegen: set name, object_id hash, entity string indices, and flags.
/// Bit layout of entity_fields is defined by the ENTITY_FIELD_*_SHIFT constants above.
void configure_entity_(const char *name, uint32_t entity_key, uint32_t entity_fields);
void configure_entity_(const char *name, uint32_t object_id_hash, uint32_t entity_fields);
#ifdef USE_DEVICES
// Codegen-only setter — only accessible from setup() via friend declaration.
@@ -233,24 +240,13 @@ class EntityBase {
#endif
/// Non-template helper for make_entity_preference() to avoid code bloat.
/// Migrates preferences from the old sanitized-object_id key to the raw-name key
/// on key-lookup platforms. See: https://github.com/esphome/backlog/issues/85
/// When the preference hash algorithm changes, migration logic goes here.
ESPPreferenceObject make_entity_preference_(size_t size, uint32_t version);
void calc_entity_key_();
/// Reconstruct the OLD (pre-2026.8.0) sanitized-object_id hash for preference keys.
uint32_t calc_old_object_id_hash_() const;
/// Preference key base for this entity: raw-name entity key XOR device_id.
uint32_t preference_key_base_() const { return this->entity_key_ ^ this->get_device_id_or_zero(); }
/// Legacy preference key base: sanitized-object_id hash XOR device_id.
/// Note: device_id is 0 for the main device, so XORing with 0 preserves the original hash.
uint32_t old_preference_key_base_() const { return this->calc_old_object_id_hash_() ^ this->get_device_id_or_zero(); }
void calc_object_id_();
StringRef name_;
uint32_t entity_key_{};
uint32_t object_id_hash_{};
#ifdef USE_DEVICES
Device *device_{};
#endif
+79 -111
View File
@@ -25,86 +25,25 @@ from esphome.core.config import (
from esphome.cpp_generator import MockObj, RawStatement, add, get_variable
from esphome.cpp_types import App
import esphome.final_validate as fv
from esphome.helpers import cpp_string_escape, fnv1_hash_name, sanitize, snake_case
from esphome.helpers import (
cpp_string_escape,
fnv1_hash,
fnv1_hash_object_id,
sanitize,
snake_case,
)
from esphome.types import ConfigType, EntityMetadata
_LOGGER = logging.getLogger(__name__)
DOMAIN = "entity_string_pool"
_OBJECT_ID_DOMAIN = "entity_object_ids"
@dataclass
class ObjectIdEntity:
"""An entity tracked by the sanitized object_id its name resolves to."""
name: str
platform: str
config: ConfigType
def _get_object_id_registry() -> dict[tuple[str, str, str], list[ObjectIdEntity]]:
"""(device_id, platform, sanitized object_id) -> entities resolving to it."""
return CORE.data.setdefault(_OBJECT_ID_DOMAIN, {})
def validate_no_object_id_conflicts(
reason: str,
conflict_filter: Callable[[list[ObjectIdEntity], ConfigType], bool] | None = None,
) -> Callable[[ConfigType], ConfigType]:
"""Create a final-validate step that rejects entities with colliding object_ids.
Entity keys are hashed from the raw name, so names that only differ in characters
lost during sanitizing (for example two UTF-8 names) validate fine in general.
Components that still address entities by the sanitized object_id string must
reject those configs until they are migrated to raw names.
Args:
reason: One sentence stating what the component builds from the object_id,
e.g. "mqtt builds default topics from the entity object_id"
conflict_filter: Optional predicate receiving the colliding entities and the
component config; return False when the component is not affected
Returns:
A validator function for use as (or within) FINAL_VALIDATE_SCHEMA
"""
def validator(config: ConfigType) -> ConfigType:
# Skip in testing_mode, which is used for grouped component testing
if CORE.testing_mode:
return config
conflicts = {
key: entities
for key, entities in _get_object_id_registry().items()
if len(entities) > 1
and (conflict_filter is None or conflict_filter(entities, config))
}
if not conflicts:
return config
lines = [f"{reason}, so these entities would conflict:"]
lines.extend(
f" - {platform} entities "
+ ", ".join(f"'{e.name}'" for e in entities)
+ (f" on device '{device_id}'" if device_id else "")
+ f" share the object_id '{object_id}'"
for (device_id, platform, object_id), entities in conflicts.items()
)
lines.append(
"To fix: Add unique ASCII characters (e.g., '1', '2', or 'A', 'B') "
"to distinguish the names"
)
raise cv.Invalid("\n".join(lines))
return validator
# Private config keys for storing registered string indices
_KEY_DC_IDX = "_entity_dc_idx"
_KEY_UOM_IDX = "_entity_uom_idx"
_KEY_ICON_IDX = "_entity_icon_idx"
_KEY_ENTITY_NAME = "_entity_name"
_KEY_ENTITY_KEY = "_entity_key"
_KEY_OBJECT_ID_HASH = "_entity_object_id_hash"
# Bit layout for entity_fields in configure_entity_().
# Keep in sync with ENTITY_FIELD_*_SHIFT constants in esphome/core/entity_base.h
@@ -367,7 +306,7 @@ def finalize_entity_strings(var: MockObj, config: ConfigType) -> None:
standalone ``var->configure_entity_(name, hash, packed)``.
"""
entity_name = config[_KEY_ENTITY_NAME]
entity_key = config[_KEY_ENTITY_KEY]
object_id_hash = config[_KEY_OBJECT_ID_HASH]
dc_idx = config.get(_KEY_DC_IDX, 0)
uom_idx = config.get(_KEY_UOM_IDX, 0)
icon_idx = config.get(_KEY_ICON_IDX, 0)
@@ -387,30 +326,57 @@ def finalize_entity_strings(var: MockObj, config: ConfigType) -> None:
register_method = config.get(_KEY_REGISTER_METHOD)
if register_method is not None:
expr = getattr(App, f"register_{register_method}")(
var, entity_name, entity_key, packed
var, entity_name, object_id_hash, packed
)
else:
expr = var.configure_entity_(entity_name, entity_key, packed)
expr = var.configure_entity_(entity_name, object_id_hash, packed)
if comment:
add(RawStatement(f"{expr}; // {comment}"))
else:
add(expr)
def get_base_entity_name(
def get_base_entity_object_id(
name: str, friendly_name: str | None, device_name: str | None = None
) -> str:
"""Return the base name whose hash becomes this entity's key on the device.
"""Calculate the base object ID for an entity that will be set via set_object_id().
Follows the name selection in C++ EntityBase::configure_entity_() (entity_base.cpp):
entity name, then sub-device name, then friendly name, then the device name.
This function calculates what object_id_c_str_ should be set to in C++.
This is a config-time approximation for duplicate checking: when
name_add_mac_suffix is enabled the device appends the MAC suffix at runtime,
which is unknown here and identical for every entity on the device, so
ignoring it cannot change whether two entities collide with each other.
The C++ EntityBase::write_object_id_to() (entity_base.cpp) works as:
- If !has_own_name && is_name_add_mac_suffix_enabled():
return str_sanitize(str_snake_case(App.get_friendly_name())) // Dynamic
- Else:
return object_id_c_str_ ?? "" // What we set via set_object_id()
Since we're calculating what to pass to set_object_id(), we always need to
generate the object_id the same way, regardless of name_add_mac_suffix setting.
Args:
name: The entity name (empty string if no name)
friendly_name: The friendly name from CORE.friendly_name
device_name: The device name if entity is on a sub-device
Returns:
The base object ID to use for duplicate checking and to pass to set_object_id()
"""
return name or device_name or friendly_name or CORE.name
if name:
# Entity has its own name (has_own_name will be true)
base_str = name
elif device_name:
# Entity has empty name and is on a sub-device
# C++ EntityBase::set_name() uses device->get_name() when device is set
base_str = device_name
elif friendly_name:
# Entity has empty name (has_own_name will be false)
# C++ uses App.get_friendly_name() which returns friendly_name or device name
base_str = friendly_name
else:
# Fallback to device name
base_str = CORE.name
return sanitize(snake_case(base_str))
def setup_entity(var_or_platform, config=None, platform=None):
@@ -469,15 +435,15 @@ async def _setup_entity_impl(var: MockObj, config: ConfigType, platform: str) ->
device: MockObj = await get_variable(device_id_obj)
add(var.set_device_(device))
# Pre-compute entity name and entity key for configure_entity_()
# Pre-compute entity name and object_id hash for configure_entity_()
# which is emitted later by finalize_entity_strings().
# For named entities: pre-compute the key from the raw entity name
# For empty-name entities: pass 0, C++ calculates the key at runtime from
# device name, friendly_name, or app name
# For named entities: pre-compute hash from entity name
# For empty-name entities: pass 0, C++ calculates hash at runtime from
# device name, friendly_name, or app name (bug-for-bug compatibility)
entity_name = config[CONF_NAME]
entity_key = fnv1_hash_name(entity_name) if entity_name else 0
object_id_hash = fnv1_hash_object_id(entity_name) if entity_name else 0
config[_KEY_ENTITY_NAME] = entity_name
config[_KEY_ENTITY_KEY] = entity_key
config[_KEY_OBJECT_ID_HASH] = object_id_hash
# Store flags for packing into configure_entity_()
config[_KEY_DISABLED_BY_DEFAULT] = int(config[CONF_DISABLED_BY_DEFAULT])
if CONF_INTERNAL in config:
@@ -590,13 +556,16 @@ def entity_duplicate_validator(platform: str) -> Callable[[ConfigType], ConfigTy
# Use the device ID string directly for uniqueness
device_id = device_id_obj.id
# Hash the same raw name the device hashes into the entity key at runtime.
# This handles empty names correctly by using device/friendly names.
base_name = get_base_entity_name(entity_name, CORE.friendly_name, device_name)
name_hash = fnv1_hash_name(base_name)
# Calculate what object_id will actually be used
# This handles empty names correctly by using device/friendly names
name_key = get_base_entity_object_id(
entity_name, CORE.friendly_name, device_name
)
# Check for duplicates: two entities on the same device and platform must not
# share an entity key, since the key is what routes state to API clients
# Check for duplicates by the FNV-1 hash of the object_id, which is the entity
# key that routes state to API clients. This rejects names that sanitize to the
# same object_id, and also two different object_ids whose 32-bit hashes collide.
name_hash = fnv1_hash(name_key)
unique_key = (device_id, platform, name_hash)
if unique_key in CORE.unique_ids:
# Get the existing entity metadata
@@ -621,14 +590,26 @@ def entity_duplicate_validator(platform: str) -> Callable[[ConfigType], ConfigTy
if existing_component != "unknown":
conflict_msg += f" from component '{existing_component}'"
# Different names can only clash here through a genuine hash collision
# Distinguish names that sanitize to the same object_id from a genuine
# 32-bit hash collision between two different object_ids
collision_msg = ""
if entity_name != existing_name:
collision_msg = (
f"\n The names '{entity_name}' and '{existing_name}' produce the"
f"\n same entity key hash ({name_hash:#010x})."
"\n To fix: Rename one of the entities"
existing_object_id = get_base_entity_object_id(
existing_name, CORE.friendly_name, existing_device or None
)
if existing_object_id == name_key:
collision_msg = (
f"\n Original names: '{entity_name}' and '{existing_name}'"
f"\n Both convert to ASCII ID: '{name_key}'"
"\n To fix: Add unique ASCII characters (e.g., '1', '2', or 'A', 'B')"
"\n to distinguish them"
)
else:
collision_msg = (
f"\n The object_ids '{name_key}' and '{existing_object_id}'"
f"\n produce the same entity key hash ({name_hash:#010x})."
"\n To fix: Rename one of the entities"
)
# Skip duplicate entity name validation when testing_mode is enabled
# This flag is used for grouped component testing
@@ -640,19 +621,6 @@ def entity_duplicate_validator(platform: str) -> Callable[[ConfigType], ConfigTy
f"{collision_msg}"
)
# Components that still address entities by the sanitized object_id reject
# colliding names in final validation via validate_no_object_id_conflicts(),
# so track every entity by the object_id its name resolves to. Scoped per
# device and platform to match the strictness configs had before entity keys
# moved to raw names: same-named entities on different sub-devices were
# already accepted then, internal entities were already skipped (above), and
# overlaps between platforms that share an MQTT component type (sensor and
# text_sensor both publish under "sensor") were already possible.
object_id = sanitize(snake_case(base_name))
_get_object_id_registry().setdefault(
(device_id, platform, object_id), []
).append(ObjectIdEntity(base_name, platform, config))
# Store metadata about this entity
entity_metadata: EntityMetadata = {
"name": entity_name,
+4 -25
View File
@@ -809,19 +809,6 @@ constexpr uint32_t FNV1_OFFSET_BASIS = 2166136261UL;
/// FNV-1 32-bit prime
constexpr uint32_t FNV1_PRIME = 16777619UL;
/// Calculate a FNV-1 hash over raw bytes with an explicit length. Unlike fnv1_hash(const char *),
/// each byte is hashed as an unsigned value, so results are platform-independent for bytes >= 0x80.
/// IMPORTANT: Must match Python fnv1_hash_name() in esphome/helpers.py, which hashes the UTF-8
/// encoded bytes of the name. Used to compute entity keys from raw names.
inline uint32_t fnv1_hash_bytes(const char *str, size_t len) {
uint32_t hash = FNV1_OFFSET_BASIS;
for (size_t i = 0; i < len; i++) {
hash *= FNV1_PRIME;
hash ^= static_cast<uint8_t>(str[i]);
}
return hash;
}
/// Extend a FNV-1 hash with an integer (hashes each byte).
template<std::integral T> constexpr uint32_t fnv1_hash_extend(uint32_t hash, T value) {
using UnsignedT = std::make_unsigned_t<T>;
@@ -1026,20 +1013,12 @@ template<size_t N> inline char *str_sanitize_to(char (&buffer)[N], const char *s
// str_sanitize moved to alloc_helpers.h - remove this comment before 2026.11.0
/// Calculate FNV-1 hash of a string while applying snake_case + sanitize transformations.
/// This is the LEGACY entity hash, kept only to reconstruct preference keys that existing
/// devices already have stored; see https://github.com/esphome/backlog/issues/85.
/// With per_code_point set, UTF-8 continuation bytes are skipped so each multi-byte character
/// contributes one underscore — this matches Python fnv1_hash_object_id() in esphome/helpers.py,
/// which produced the hash for named entities. The per-byte form (default) matches the old
/// runtime hash for entities without their own name. Do not change either behavior.
/// Known limitation: Python's lower() is Unicode aware, so the rare code points it maps to a
/// different number of characters or to ASCII (e.g. 'İ', the Kelvin sign) reconstruct wrong;
/// such names skip migration once and fall back to their defaults.
inline uint32_t fnv1_hash_object_id(const char *str, size_t len, bool per_code_point = false) {
/// This computes object_id hashes directly from names without creating an intermediate buffer.
/// IMPORTANT: Must match Python fnv1_hash_object_id() in esphome/helpers.py.
/// If you modify this function, update the Python version and tests in both places.
inline uint32_t fnv1_hash_object_id(const char *str, size_t len) {
uint32_t hash = FNV1_OFFSET_BASIS;
for (size_t i = 0; i < len; i++) {
if (per_code_point && (static_cast<uint8_t>(str[i]) & 0xC0) == 0x80)
continue; // UTF-8 continuation byte, already counted via its lead byte
hash *= FNV1_PRIME;
// Apply snake_case (space->underscore, uppercase->lowercase) then sanitize
hash ^= static_cast<uint8_t>(to_sanitized_char(to_snake_case_char(str[i])));
+7 -7
View File
@@ -24,9 +24,10 @@
#endif
// Key-lookup preference backends find stored data by key; their platforms add the
// USE_PREFERENCE_KEY_LOOKUP define from Python codegen, which enables preference key
// migration. Slot-based backends (ESP8266, RP2040) instead allocate a storage slot for
// every make_preference() call and use the key only as a validity tag on that slot;
// USE_PREFERENCE_KEY_LOOKUP define from Python codegen, which enables one-shot reads
// of stored data by key (the primitive preference key migrations need). Slot-based
// backends (ESP8266, RP2040) instead allocate a storage slot for every
// make_preference() call and use the key only as a validity tag on that slot;
// migration is not possible there, and key collisions cannot corrupt data.
namespace esphome {
@@ -104,10 +105,9 @@ concept PreferencesContract = requires(T prefs, size_t len, uint32_t type, bool
};
// Key-lookup platforms additionally provide load_from_key(), a one-shot read
// of a stored preference by key that migrate_preference() relies on; see the
// key-lookup note at the top of this file. Not part of PreferencesContract,
// so it is asserted in preferences.h only where USE_PREFERENCE_KEY_LOOKUP
// is set.
// of a stored preference by key; see the key-lookup note at the top of this
// file. Not part of PreferencesContract, so it is asserted in preferences.h
// only where USE_PREFERENCE_KEY_LOOKUP is set.
template<typename T>
concept PreferencesKeyLookupContract = requires(T prefs, uint32_t type, uint8_t *data, size_t len) {
{ prefs.load_from_key(type, data, len) } -> std::same_as<bool>;
-25
View File
@@ -1,25 +0,0 @@
#include "esphome/core/preferences.h"
#include "esphome/core/log.h"
#include <cinttypes>
namespace esphome {
#ifdef USE_PREFERENCE_KEY_LOOKUP
static const char *const TAG = "preferences";
bool migrate_preference(ESPPreferenceObject &new_pref, uint8_t *scratch, size_t size, uint32_t old_key,
uint32_t new_key) {
if (new_pref.load(scratch, size))
return true; // Current data present - never overwrite newer data with the old copy
// One-shot read by key: no backend is allocated for the old key, so boots with
// nothing to migrate (for example fresh installs) cost no heap
if (old_key == new_key || !global_preferences->load_from_key(old_key, scratch, size))
return false; // No data stored under the old key, nothing to migrate
if (!new_pref.save(scratch, size)) {
ESP_LOGW(TAG, "Pref migration %" PRIx32 " -> %" PRIx32 " failed", old_key, new_key);
}
return true;
}
#endif // USE_PREFERENCE_KEY_LOOKUP
} // namespace esphome
-12
View File
@@ -56,17 +56,5 @@ namespace esphome {
static_assert(PreferencesKeyLookupContract<ESPPreferences>,
"This platform emits USE_PREFERENCE_KEY_LOOKUP but its preferences manager does not provide "
"load_from_key() (esphome/core/preference_backend.h)");
/// Copy preference data stored under old_key into new_pref (created for new_key) if the keys
/// differ and new_pref has no data yet. scratch must hold at least size bytes.
/// Returns true when scratch holds the entity's current data (loaded or just migrated).
/// The old entry is intentionally left in place so a firmware downgrade still finds its data.
/// If saving under the new key fails, callers that consume scratch (like TextSaver) still get
/// valid data for this boot, callers that reload from the preference fall back to their
/// defaults, and the migration simply runs again on the next boot.
/// Only available on key-lookup preference backends; slot-based backends keep their old
/// keys instead. See: https://github.com/esphome/backlog/issues/85
bool migrate_preference(ESPPreferenceObject &new_pref, uint8_t *scratch, size_t size, uint32_t old_key,
uint32_t new_key);
} // namespace esphome
#endif // USE_PREFERENCE_KEY_LOOKUP
+7
View File
@@ -15,6 +15,13 @@ inline void ESPHOME_ALWAYS_INLINE wake_loop_impl() {
// Set the wake-requested flag BEFORE esp_schedule so the consumer is
// guaranteed to see it on its next gate check.
wake_request_set();
// Skip the post when a wake was already signalled and not yet consumed by
// wakeable_delay(): esp_schedule() -> ets_post() can enter SDK WiFi pm code,
// which must not be poked per-byte from the software serial RX ISR (see
// esphome#18409). The flag can stay latched while the loop is awake, which
// is intentional; posts are only needed to cut a suspend short.
if (g_main_loop_woke)
return;
g_main_loop_woke = true;
esp_schedule();
}
+21 -8
View File
@@ -232,6 +232,17 @@ def _parse_lib_deps(platformio_ini: Path, framework: str):
return libs
def _esphome_manifest_deps() -> set[str]:
"""Names of the managed components declared in ``esphome/idf_component.yml``."""
import yaml
esphome_dir = Path(__file__).resolve().parent.parent
manifest = yaml.safe_load(
(esphome_dir / "idf_component.yml").read_text(encoding="utf-8")
)
return set(manifest.get("dependencies") or {})
def _convert_pio_libs(
platformio_ini: Path, framework: str
) -> dict[str, dict[str, str]]:
@@ -244,12 +255,20 @@ def _convert_pio_libs(
The whole library set is resolved as a single batch so a shared transitive
dependency (e.g. esphome/libsodium pulled by both noise-c and esp_wireguard)
is deduplicated to one component instead of clashing override_path entries.
Libraries ESPHome's own manifest already provides as managed components
(noise-c, libsodium, ...) are skipped, mirroring what the real esp32 build
does -- converting them too would make IDF see the same requirement twice.
On Arduino those entries are rule-disabled in the manifest (arduino-esp32
brings its own libsodium), so nothing provides them there and they have to
go through the converter as before.
"""
from esphome.espidf.component import generate_idf_components
libraries = _parse_lib_deps(platformio_ini, framework)
managed = set() if framework == "arduino" else _esphome_manifest_deps()
deps: dict[str, dict[str, str]] = {}
for component in generate_idf_components(libraries):
for component in generate_idf_components(libraries, managed=managed):
deps[component.get_sanitized_name()] = {"override_path": str(component.path)}
return deps
@@ -267,19 +286,13 @@ def _arduino_excluded_stubs(work_dir: Path) -> dict[str, dict]:
ethernet) are NOT stubbed -- those are real deps we need, and arduino-esp32
resolves to the same component rather than conflicting.
"""
import yaml
from esphome.components.esp32 import (
ARDUINO_EXCLUDED_IDF_COMPONENTS,
_idf_component_dep_name,
_idf_component_stub_name,
)
esphome_dir = Path(__file__).resolve().parent.parent
base_manifest = yaml.safe_load(
(esphome_dir / "idf_component.yml").read_text(encoding="utf-8")
)
esphome_deps = set(base_manifest.get("dependencies") or {})
esphome_deps = _esphome_manifest_deps()
stubs_dir = work_dir / "component_stubs"
stubs_dir.mkdir(parents=True, exist_ok=True)
+13 -3
View File
@@ -310,12 +310,22 @@ def _emit_idf_component(component: IDFComponent) -> None:
)
def generate_idf_components(libraries: list[Library]) -> list[IDFComponent]:
"""Resolve and convert a batch of PlatformIO libraries to IDF components."""
def generate_idf_components(
libraries: list[Library], managed: set[str] | None = None
) -> list[IDFComponent]:
"""Resolve and convert a batch of PlatformIO libraries to IDF components.
``managed`` names the registry components already declared in the project
manifest (via ``add_idf_component``). Those are skipped by the converter --
a library must not be both converted and managed, or IDF fails component
discovery with "Requirement <owner>__<name> and requirement <name> are both
added as project_managed_components". Converted components pick the managed
one up through ``${ESPHOME_PROJECT_MANAGED_COMPONENTS}`` in their REQUIRES.
"""
backend = LibraryBackend(
platform=ESP32_PLATFORM,
framework=_idf_framework(),
emit=_emit_idf_component,
cache_key="idf",
)
return convert_libraries(libraries, backend)
return convert_libraries(libraries, backend, provided=managed)

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