diff --git a/.clang-tidy.hash b/.clang-tidy.hash
deleted file mode 100644
index 52d75d16016..00000000000
--- a/.clang-tidy.hash
+++ /dev/null
@@ -1 +0,0 @@
-27aaab4e0ebfc10491720345aa746fc2dffa6a3985f73ec111b12dd99078d46f
diff --git a/.claude/skills/pr-workflow/SKILL.md b/.claude/skills/pr-workflow/SKILL.md
index 4ec2551804c..2c529dcd0fb 100644
--- a/.claude/skills/pr-workflow/SKILL.md
+++ b/.claude/skills/pr-workflow/SKILL.md
@@ -29,7 +29,7 @@ Required fields:
- **What does this implement/fix?**: Brief description of changes
- **Types of changes**: Check ONE appropriate box (Bugfix, New feature, Breaking change, etc.)
- **Related issue**: Use `fixes ` syntax if applicable
-- **Pull request in esphome-docs**: Link if docs are needed
+- **Pull request in esphome.io**: Link if docs are needed
- **Test Environment**: Check platforms you tested on
- **Example config.yaml**: Include working example YAML
- **Checklist**: Verify code is tested and tests added
@@ -54,9 +54,9 @@ Required fields:
- fixes https://github.com/esphome/esphome/issues/XXX
-**Pull request in [esphome-docs](https://github.com/esphome/esphome-docs) with documentation (if applicable):**
+**Pull request in [esphome.io](https://github.com/esphome/esphome.io) with documentation (if applicable):**
-- esphome/esphome-docs#XXX
+- esphome/esphome.io#XXX
## Test Environment
@@ -83,7 +83,7 @@ component_name:
- [x] Tests have been added to verify that the new code works (under `tests/` folder).
If user exposed functionality or configuration variables are added/changed:
- - [ ] Documentation added/updated in [esphome-docs](https://github.com/esphome/esphome-docs).
+ - [ ] Documentation added/updated in [esphome.io](https://github.com/esphome/esphome.io).
```
## 5. Push and Create PR
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 19f52349a66..3b39d519c4a 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -2,7 +2,7 @@
blank_issues_enabled: false
contact_links:
- name: Report an issue with the ESPHome documentation
- url: https://github.com/esphome/esphome-docs/issues/new/choose
+ url: https://github.com/esphome/esphome.io/issues/new/choose
about: Report an issue with the ESPHome documentation.
- name: Report an issue with the ESPHome web server
url: https://github.com/esphome/esphome-webserver/issues/new/choose
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 72013e411eb..08def885774 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -16,9 +16,9 @@
- fixes
-**Pull request in [esphome-docs](https://github.com/esphome/esphome-docs) with documentation (if applicable):**
+**Pull request in [esphome.io](https://github.com/esphome/esphome.io) with documentation (if applicable):**
-- esphome/esphome-docs#
+- esphome/esphome.io#
## Test Environment
@@ -43,4 +43,4 @@
- [ ] Tests have been added to verify that the new code works (under `tests/` folder).
If user exposed functionality or configuration variables are added/changed:
- - [ ] Documentation added/updated in [esphome-docs](https://github.com/esphome/esphome-docs).
+ - [ ] Documentation added/updated in [esphome.io](https://github.com/esphome/esphome.io).
diff --git a/.github/actions/build-image/action.yaml b/.github/actions/build-image/action.yaml
index 2081264b911..494c0cebe80 100644
--- a/.github/actions/build-image/action.yaml
+++ b/.github/actions/build-image/action.yaml
@@ -15,11 +15,6 @@ inputs:
description: "Version to build"
required: true
example: "2023.12.0"
- base_os:
- description: "Base OS to use"
- required: false
- default: "debian"
- example: "debian"
runs:
using: "composite"
steps:
@@ -60,7 +55,6 @@ runs:
build-args: |
BUILD_TYPE=${{ inputs.build_type }}
BUILD_VERSION=${{ inputs.version }}
- BUILD_OS=${{ inputs.base_os }}
outputs: |
type=image,name=ghcr.io/${{ steps.tags.outputs.image_name }},push-by-digest=true,name-canonical=true,push=true
@@ -86,7 +80,6 @@ runs:
build-args: |
BUILD_TYPE=${{ inputs.build_type }}
BUILD_VERSION=${{ inputs.version }}
- BUILD_OS=${{ inputs.base_os }}
outputs: |
type=image,name=docker.io/${{ steps.tags.outputs.image_name }},push-by-digest=true,name-canonical=true,push=true
diff --git a/.github/actions/cache-esp-idf/action.yml b/.github/actions/cache-esp-idf/action.yml
new file mode 100644
index 00000000000..f566ba4c434
--- /dev/null
+++ b/.github/actions/cache-esp-idf/action.yml
@@ -0,0 +1,52 @@
+name: Cache ESP-IDF
+description: >
+ Resolve the pinned ESP-IDF version and cache the native ESP-IDF install
+ (toolchains + source) at ~/.esphome-idf. Every job that installs ESP-IDF
+ natively (clang-tidy for IDF/Arduino and the component test batches) shares
+ one cache, since the install is identical (ESPHOME_IDF_DEFAULT_TARGETS
+ defaults to "all", so all toolchains are present regardless of the chip).
+ Callers must set env ESPHOME_ESP_IDF_PREFIX: ~/.esphome-idf and have the
+ Python venv already restored.
+inputs:
+ framework:
+ description: 'Which pinned IDF version to key on: "espidf" (recommended) or "arduino".'
+ default: espidf
+ restore-only:
+ description: >
+ When "true", only restore -- never save the cache, even on dev. Use from
+ jobs that may not produce an ESP-IDF install (e.g. a component batch with
+ no esp32 target), so a partial/empty install is never written to the key.
+ default: "false"
+runs:
+ using: composite
+ steps:
+ - name: Resolve ESP-IDF version for cache key
+ # The native-IDF version is pinned in code, not in any file that feeds the
+ # other cache keys, so resolve it explicitly. Keying on it means the cache
+ # invalidates on a version bump (actions/cache never overwrites a key).
+ id: version
+ shell: bash
+ run: |
+ . venv/bin/activate
+ if [ "${{ inputs.framework }}" = "arduino" ]; then
+ version=$(python -c 'from esphome.components.esp32 import ARDUINO_FRAMEWORK_VERSION_LOOKUP as A, ARDUINO_IDF_VERSION_LOOKUP as L; print(L[A["recommended"]])')
+ else
+ version=$(python -c 'from esphome.components.esp32 import ESP_IDF_FRAMEWORK_VERSION_LOOKUP as L; print(L["recommended"])')
+ fi
+ echo "version=$version" >> "$GITHUB_OUTPUT"
+ # Mirror the adjacent PlatformIO cache: only dev-branch runs write the
+ # shared cache (so it lives in the default-branch scope readable by all
+ # PRs), and PRs are restore-only -- they never push multi-GB artifacts into
+ # their own scope / the repo quota (e.g. on a version-bump PR).
+ - name: Cache ESP-IDF install (write on dev)
+ if: github.ref == 'refs/heads/dev' && inputs.restore-only != 'true'
+ uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
+ with:
+ path: ~/.esphome-idf
+ key: ${{ runner.os }}-esphome-idf-${{ steps.version.outputs.version }}
+ - name: Cache ESP-IDF install (restore-only off dev)
+ if: github.ref != 'refs/heads/dev' || inputs.restore-only == 'true'
+ uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
+ with:
+ path: ~/.esphome-idf
+ key: ${{ runner.os }}-esphome-idf-${{ steps.version.outputs.version }}
diff --git a/.github/actions/restore-python/action.yml b/.github/actions/restore-python/action.yml
index 03b48038601..66d016b42d8 100644
--- a/.github/actions/restore-python/action.yml
+++ b/.github/actions/restore-python/action.yml
@@ -32,7 +32,7 @@ runs:
# detects the activated venv via ``VIRTUAL_ENV`` so the venv layout
# downstream jobs rely on is preserved.
if: steps.cache-venv.outputs.cache-hit != 'true'
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
+ uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
# Pin uv version so the action does not have to fetch the
diff --git a/.github/scripts/auto-label-pr/constants.js b/.github/scripts/auto-label-pr/constants.js
index e02b450bf0e..2938fd923c9 100644
--- a/.github/scripts/auto-label-pr/constants.js
+++ b/.github/scripts/auto-label-pr/constants.js
@@ -35,6 +35,9 @@ module.exports = {
],
DOCS_PR_PATTERNS: [
+ /https:\/\/github\.com\/esphome\/esphome\.io\/pull\/\d+/,
+ /esphome\/esphome\.io#\d+/,
+ // Keep matching the old esphome-docs name during the transition period
/https:\/\/github\.com\/esphome\/esphome-docs\/pull\/\d+/,
/esphome\/esphome-docs#\d+/
]
diff --git a/.github/scripts/auto-label-pr/detectors.js b/.github/scripts/auto-label-pr/detectors.js
index 410c1a53c07..81bb77843d1 100644
--- a/.github/scripts/auto-label-pr/detectors.js
+++ b/.github/scripts/auto-label-pr/detectors.js
@@ -107,6 +107,8 @@ async function detectNewPlatforms(github, context, prFiles, apiData) {
/^esphome\/components\/([^\/]+)\/([^\/]+)\/__init__\.py$/,
];
+ const removedFiles = new Set(prFiles.filter(file => file.status === 'removed').map(file => file.filename));
+
for (const file of addedFiles) {
for (const re of platformPathPatterns) {
const match = file.match(re);
@@ -114,6 +116,12 @@ async function detectNewPlatforms(github, context, prFiles, apiData) {
const platform = match[2];
if (!apiData.platformComponents.includes(platform)) break;
+ // Skip if this is a restructure between flat and subdirectory forms (either direction):
+ // /.py <-> //__init__.py
+ const flatEquivalent = `esphome/components/${match[1]}/${platform}.py`;
+ const subdirEquivalent = `esphome/components/${match[1]}/${platform}/__init__.py`;
+ if (removedFiles.has(flatEquivalent) || removedFiles.has(subdirEquivalent)) break;
+
labels.add('new-platform');
const content = await fetchPrFileContent(github, context, file);
if (content === null) {
diff --git a/.github/scripts/auto-label-pr/package.json b/.github/scripts/auto-label-pr/package.json
new file mode 100644
index 00000000000..401b376db6b
--- /dev/null
+++ b/.github/scripts/auto-label-pr/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "auto-label-pr",
+ "private": true,
+ "scripts": {
+ "test": "node --test tests/*.test.js"
+ }
+}
diff --git a/.github/scripts/auto-label-pr/tests/detectors.test.js b/.github/scripts/auto-label-pr/tests/detectors.test.js
new file mode 100644
index 00000000000..02d69ca95ea
--- /dev/null
+++ b/.github/scripts/auto-label-pr/tests/detectors.test.js
@@ -0,0 +1,147 @@
+const { describe, it } = require('node:test');
+const assert = require('node:assert/strict');
+const { detectNewPlatforms, detectNewComponents } = require('../detectors');
+
+// Minimal GitHub API mock — only repos.getContent is called by detectNewPlatforms/detectNewComponents
+// to check for CONFIG_SCHEMA in newly added files.
+function makeGithub(content = '') {
+ return {
+ rest: {
+ repos: {
+ getContent: async () => ({
+ data: { content: Buffer.from(content).toString('base64') }
+ })
+ }
+ }
+ };
+}
+
+const CONTEXT = {
+ repo: { owner: 'esphome', repo: 'esphome' },
+ payload: { pull_request: { head: { sha: 'abc123' }, base: { ref: 'dev' } } }
+};
+
+const API_DATA = {
+ targetPlatforms: ['esp32', 'esp8266', 'rp2040'],
+ platformComponents: ['cover', 'sensor', 'binary_sensor', 'switch', 'light', 'fan', 'climate', 'valve']
+};
+
+const WITH_SCHEMA = 'CONFIG_SCHEMA = cv.Schema({})';
+const WITHOUT_SCHEMA = 'CODEOWNERS = ["@esphome/core"]';
+
+// ---------------------------------------------------------------------------
+// detectNewPlatforms
+// ---------------------------------------------------------------------------
+
+describe('detectNewPlatforms', () => {
+ describe('restructure detection (no false positives)', () => {
+ it('flat .py -> subdir __init__.py is not a new platform', async () => {
+ const prFiles = [
+ { filename: 'esphome/components/endstop/cover.py', status: 'removed' },
+ { filename: 'esphome/components/endstop/cover/__init__.py', status: 'added' },
+ ];
+ const result = await detectNewPlatforms(makeGithub(WITH_SCHEMA), CONTEXT, prFiles, API_DATA);
+ assert.equal(result.labels.size, 0);
+ assert.equal(result.hasYamlLoadable, false);
+ });
+
+ it('subdir __init__.py -> flat .py is not a new platform', async () => {
+ const prFiles = [
+ { filename: 'esphome/components/endstop/cover/__init__.py', status: 'removed' },
+ { filename: 'esphome/components/endstop/cover.py', status: 'added' },
+ ];
+ const result = await detectNewPlatforms(makeGithub(WITH_SCHEMA), CONTEXT, prFiles, API_DATA);
+ assert.equal(result.labels.size, 0);
+ assert.equal(result.hasYamlLoadable, false);
+ });
+ });
+
+ describe('genuine new platforms', () => {
+ it('new subdir platform with CONFIG_SCHEMA sets new-platform and hasYamlLoadable', async () => {
+ const prFiles = [
+ { filename: 'esphome/components/my_sensor/cover/__init__.py', status: 'added' },
+ ];
+ const result = await detectNewPlatforms(makeGithub(WITH_SCHEMA), CONTEXT, prFiles, API_DATA);
+ assert.ok(result.labels.has('new-platform'));
+ assert.equal(result.hasYamlLoadable, true);
+ });
+
+ it('new flat platform with CONFIG_SCHEMA sets new-platform and hasYamlLoadable', async () => {
+ const prFiles = [
+ { filename: 'esphome/components/my_sensor/cover.py', status: 'added' },
+ ];
+ const result = await detectNewPlatforms(makeGithub(WITH_SCHEMA), CONTEXT, prFiles, API_DATA);
+ assert.ok(result.labels.has('new-platform'));
+ assert.equal(result.hasYamlLoadable, true);
+ });
+
+ it('new platform without CONFIG_SCHEMA sets new-platform but not hasYamlLoadable', async () => {
+ const prFiles = [
+ { filename: 'esphome/components/my_sensor/cover.py', status: 'added' },
+ ];
+ const result = await detectNewPlatforms(makeGithub(WITHOUT_SCHEMA), CONTEXT, prFiles, API_DATA);
+ assert.ok(result.labels.has('new-platform'));
+ assert.equal(result.hasYamlLoadable, false);
+ });
+
+ it('non-platform file addition produces no labels', async () => {
+ const prFiles = [
+ { filename: 'esphome/components/my_sensor/sensor.py', status: 'added' },
+ ];
+ // Override platformComponents so 'sensor' is not a recognized platform -> no label expected.
+ const nonPlatformApiData = { ...API_DATA, platformComponents: ['cover'] };
+ const result = await detectNewPlatforms(makeGithub(WITH_SCHEMA), CONTEXT, prFiles, nonPlatformApiData);
+ assert.equal(result.labels.size, 0);
+ assert.equal(result.hasYamlLoadable, false);
+ });
+ });
+});
+
+// ---------------------------------------------------------------------------
+// detectNewComponents
+// ---------------------------------------------------------------------------
+
+describe('detectNewComponents', () => {
+ it('new top-level __init__.py sets new-component', async () => {
+ const prFiles = [
+ { filename: 'esphome/components/actuator/__init__.py', status: 'added', },
+ ];
+ const result = await detectNewComponents(makeGithub(WITHOUT_SCHEMA), CONTEXT, prFiles);
+ assert.ok(result.labels.has('new-component'));
+ assert.equal(result.hasYamlLoadable, false);
+ });
+
+ it('new top-level __init__.py with CONFIG_SCHEMA sets hasYamlLoadable', async () => {
+ const prFiles = [
+ { filename: 'esphome/components/my_component/__init__.py', status: 'added' },
+ ];
+ const result = await detectNewComponents(makeGithub(WITH_SCHEMA), CONTEXT, prFiles);
+ assert.ok(result.labels.has('new-component'));
+ assert.equal(result.hasYamlLoadable, true);
+ });
+
+ it('new top-level __init__.py with IS_TARGET_PLATFORM sets new-target-platform', async () => {
+ const prFiles = [
+ { filename: 'esphome/components/my_platform/__init__.py', status: 'added' },
+ ];
+ const result = await detectNewComponents(makeGithub('IS_TARGET_PLATFORM = True'), CONTEXT, prFiles);
+ assert.ok(result.labels.has('new-component'));
+ assert.ok(result.labels.has('new-target-platform'));
+ });
+
+ it('modified __init__.py does not set new-component', async () => {
+ const prFiles = [
+ { filename: 'esphome/components/existing/__init__.py', status: 'modified' },
+ ];
+ const result = await detectNewComponents(makeGithub(WITH_SCHEMA), CONTEXT, prFiles);
+ assert.equal(result.labels.size, 0);
+ });
+
+ it('nested __init__.py does not set new-component', async () => {
+ const prFiles = [
+ { filename: 'esphome/components/endstop/cover/__init__.py', status: 'added' },
+ ];
+ const result = await detectNewComponents(makeGithub(WITH_SCHEMA), CONTEXT, prFiles);
+ assert.equal(result.labels.size, 0);
+ });
+});
diff --git a/.github/workflows/auto-label-pr.yml b/.github/workflows/auto-label-pr.yml
index 6c80d36d20b..e48d6f69bd2 100644
--- a/.github/workflows/auto-label-pr.yml
+++ b/.github/workflows/auto-label-pr.yml
@@ -24,7 +24,7 @@ jobs:
if: github.event.pull_request.state == 'open' && (github.event.action != 'labeled' || github.event.sender.type != 'Bot')
steps:
- name: Checkout
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Generate a token
id: generate-token
diff --git a/.github/workflows/ci-api-proto.yml b/.github/workflows/ci-api-proto.yml
index 675bbe9d2c7..c6e9a358ab2 100644
--- a/.github/workflows/ci-api-proto.yml
+++ b/.github/workflows/ci-api-proto.yml
@@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
@@ -29,7 +29,7 @@ jobs:
- name: Set up uv
# ``--system`` (below) installs into the setup-python interpreter;
# no venv is created or restored by this workflow.
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
+ uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
# Pin uv version so the action does not have to fetch the
diff --git a/.github/workflows/ci-clang-tidy-hash.yml b/.github/workflows/ci-clang-tidy-hash.yml
deleted file mode 100644
index d9148fb06dc..00000000000
--- a/.github/workflows/ci-clang-tidy-hash.yml
+++ /dev/null
@@ -1,76 +0,0 @@
-name: Clang-tidy Hash CI
-
-on:
- pull_request:
- paths:
- - ".clang-tidy"
- - "platformio.ini"
- - "requirements_dev.txt"
- - "sdkconfig.defaults"
- - ".clang-tidy.hash"
- - "script/clang_tidy_hash.py"
- - ".github/workflows/ci-clang-tidy-hash.yml"
-
-permissions:
- contents: read # actions/checkout for the PR head
- pull-requests: write # pulls.createReview / listReviews / dismissReview when the clang-tidy hash is out of date
-
-jobs:
- verify-hash:
- name: Verify clang-tidy hash
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
-
- - name: Set up Python
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
- with:
- python-version: "3.11"
-
- - name: Verify hash
- run: |
- python script/clang_tidy_hash.py --verify
-
- - if: failure()
- name: Show hash details
- run: |
- python script/clang_tidy_hash.py
- echo "## Job Failed" | tee -a $GITHUB_STEP_SUMMARY
- echo "You have modified clang-tidy configuration but have not updated the hash." | tee -a $GITHUB_STEP_SUMMARY
- echo "Please run 'script/clang_tidy_hash.py --update' and commit the changes." | tee -a $GITHUB_STEP_SUMMARY
-
- - if: failure() && github.event.pull_request.head.repo.full_name == github.repository
- name: Request changes
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
- with:
- script: |
- await github.rest.pulls.createReview({
- pull_number: context.issue.number,
- owner: context.repo.owner,
- repo: context.repo.repo,
- event: 'REQUEST_CHANGES',
- body: 'You have modified clang-tidy configuration but have not updated the hash.\nPlease run `script/clang_tidy_hash.py --update` and commit the changes.'
- })
-
- - if: success() && github.event.pull_request.head.repo.full_name == github.repository
- name: Dismiss review
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
- with:
- script: |
- let reviews = await github.rest.pulls.listReviews({
- pull_number: context.issue.number,
- owner: context.repo.owner,
- repo: context.repo.repo
- });
- for (let review of reviews.data) {
- if (review.user.login === 'github-actions[bot]' && review.state === 'CHANGES_REQUESTED') {
- await github.rest.pulls.dismissReview({
- pull_number: context.issue.number,
- owner: context.repo.owner,
- repo: context.repo.repo,
- review_id: review.id,
- message: 'Clang-tidy hash now matches configuration.'
- });
- }
- }
diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml
index 89fbec54206..373cd905b19 100644
--- a/.github/workflows/ci-docker.yml
+++ b/.github/workflows/ci-docker.yml
@@ -1,28 +1,41 @@
---
name: CI for docker images
-# Only run when docker paths change
+# Only run on PRs that touch the docker image, its build inputs, or any code
+# whose toolchain the compile smoke test exercises (core + target platforms).
on:
- push:
- branches: [dev, beta, release]
- paths:
- - "docker/**"
- - ".github/workflows/ci-docker.yml"
- - "requirements*.txt"
- - "platformio.ini"
- - "script/platformio_install_deps.py"
-
pull_request:
paths:
+ # Docker image and its build inputs.
- "docker/**"
- ".github/workflows/ci-docker.yml"
- "requirements*.txt"
+ - "pyproject.toml"
- "platformio.ini"
+ - "esphome/idf_component.yml"
- "script/platformio_install_deps.py"
+ # Core, build pipeline, toolchain, and target-platform changes can change
+ # how a toolchain is set up or built, so re-run the per-toolchain compile
+ # smoke test when they change.
+ - "esphome/core/**"
+ - "esphome/writer.py"
+ - "esphome/build_gen/**"
+ - "esphome/espidf/**"
+ - "esphome/platformio/**"
+ - "esphome/components/bk72xx/**"
+ - "esphome/components/esp32/**"
+ - "esphome/components/esp8266/**"
+ - "esphome/components/host/**"
+ - "esphome/components/libretiny/**"
+ - "esphome/components/ln882x/**"
+ - "esphome/components/nrf52/**"
+ - "esphome/components/rp2040/**"
+ - "esphome/components/rtl87xx/**"
+ - "esphome/components/zephyr/**"
permissions:
- contents: read # actions/checkout only; the build does not push images
+ contents: read # actions/checkout only
concurrency:
# yamllint disable-line rule:line-length
@@ -33,6 +46,9 @@ jobs:
check-docker:
name: Build docker containers
runs-on: ${{ matrix.os }}
+ permissions:
+ contents: read # actions/checkout to load Dockerfile and build context
+ packages: write # push branch-tagged images to ghcr.io for local testing
strategy:
fail-fast: false
matrix:
@@ -41,8 +57,11 @@ jobs:
- "ha-addon"
- "docker"
# - "lint"
+ outputs:
+ tag: ${{ steps.tag.outputs.tag }}
+ push: ${{ steps.tag.outputs.push }}
steps:
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
@@ -50,14 +69,149 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- - name: Set TAG
+ - name: Determine tag and whether to push
+ id: tag
run: |
- echo "TAG=check" >> $GITHUB_ENV
+ # Sanitize the branch name into a valid docker tag: replace invalid
+ # characters, ensure the first character is valid (tags must start
+ # with [A-Za-z0-9_]), and cap the length at 128 characters.
+ branch="${{ github.head_ref || github.ref_name }}"
+ tag="${branch//[^a-zA-Z0-9_.-]/-}"
+ case "$tag" in
+ [a-zA-Z0-9_]*) ;;
+ *) tag="pr-${tag}" ;;
+ esac
+ tag="${tag:0:128}"
+ echo "tag=${tag}" >> "$GITHUB_OUTPUT"
+ # Only push branch images for same-repo pull requests. Push events
+ # only fire for dev/beta/release, whose images are owned by the
+ # release pipeline -- never overwrite those from here.
+ if [ "${{ github.event_name }}" = "pull_request" ] \
+ && [ "${{ github.repository }}" = "esphome/esphome" ] \
+ && [ "${{ github.event.pull_request.head.repo.full_name }}" = "esphome/esphome" ]; then
+ echo "push=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "push=false" >> "$GITHUB_OUTPUT"
+ fi
+
+ - name: Log in to the GitHub container registry
+ if: steps.tag.outputs.push == 'true'
+ uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
- name: Run build
run: |
docker/build.py \
- --tag "${TAG}" \
+ --tag "${{ steps.tag.outputs.tag }}" \
--arch "${{ matrix.os == 'ubuntu-24.04-arm' && 'aarch64' || 'amd64' }}" \
--build-type "${{ matrix.build_type }}" \
- build
+ --registry ghcr \
+ build ${{ steps.tag.outputs.push == 'true' && '--push --no-cache-to' || '' }} ${{ (matrix.os == 'ubuntu-24.04' && matrix.build_type == 'docker') && '--load' || '' }}
+
+ # The amd64 "docker" image is also loaded locally (above) and handed to
+ # compile-test as an artifact, so the smoke test reuses this build instead
+ # of building the image a second time. Using an artifact (rather than the
+ # pushed image) keeps it working for fork PRs, which never push to ghcr.io.
+ - name: Export image for compile-test
+ if: matrix.os == 'ubuntu-24.04' && matrix.build_type == 'docker'
+ run: docker save "ghcr.io/esphome/esphome-amd64:${{ steps.tag.outputs.tag }}" | gzip > compile-test-image.tar.gz
+
+ - name: Upload compile-test image artifact
+ if: matrix.os == 'ubuntu-24.04' && matrix.build_type == 'docker'
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ # The tar is already gzipped, so upload it as-is. archive: false skips
+ # the redundant zip and makes the file name the artifact name (the
+ # `name` input is ignored in that mode).
+ path: compile-test-image.tar.gz
+ retention-days: 1
+ archive: false
+
+ manifest:
+ name: Push ${{ matrix.build_type }} manifest to ghcr.io
+ needs: [check-docker]
+ if: needs.check-docker.outputs.push == 'true'
+ runs-on: ubuntu-24.04
+ permissions:
+ contents: read # actions/checkout to run docker/build.py
+ packages: write # buildx imagetools writes the multi-arch tag to ghcr.io
+ strategy:
+ fail-fast: false
+ matrix:
+ build_type:
+ - "ha-addon"
+ - "docker"
+ steps:
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
+ - name: Set up Python
+ uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
+ with:
+ python-version: "3.11"
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
+
+ - name: Log in to the GitHub container registry
+ uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Create and push manifest
+ run: |
+ docker/build.py \
+ --tag "${{ needs.check-docker.outputs.tag }}" \
+ --build-type "${{ matrix.build_type }}" \
+ --registry ghcr \
+ manifest
+
+ # Smoke-test the built image by compiling one minimal config per target
+ # platform / toolchain. This catches missing system dependencies in the image
+ # that only surface when a given toolchain is downloaded and run. The image is
+ # the amd64 "docker" build produced by check-docker (shared as an artifact).
+ compile-test:
+ name: Compile ${{ matrix.id }}
+ needs: check-docker
+ runs-on: ubuntu-24.04
+ permissions:
+ contents: read # actions/checkout to load the test configs
+ strategy:
+ fail-fast: false
+ # Cap concurrency so this smoke test doesn't hog all the shared runners.
+ max-parallel: 2
+ matrix:
+ # One entry per distinct toolchain. ESP32 variants (c3/c6/s2/s3/p4)
+ # share a toolchain bundle, so esp32 is exercised on the base variant
+ # across the full framework x toolchain cross-product (arduino/esp-idf
+ # framework, each built with the platformio and native esp-idf
+ # toolchains) so both toolchains stay covered regardless of which one is
+ # the default.
+ id:
+ - esp8266-arduino
+ - esp32-arduino-platformio
+ - esp32-arduino-esp-idf
+ - esp32-idf-platformio
+ - esp32-idf-esp-idf
+ - rp2040-arduino
+ - bk72xx-arduino
+ - rtl87xx-arduino
+ - ln882x-arduino
+ - nrf52
+ - host
+ steps:
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
+ - name: Download image artifact
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ name: compile-test-image.tar.gz
+ - name: Load image
+ run: docker load --input compile-test-image.tar.gz
+ - name: Compile ${{ matrix.id }}
+ run: |
+ docker run --rm \
+ -v "${{ github.workspace }}/docker/test_configs:/config" \
+ "ghcr.io/esphome/esphome-amd64:${{ needs.check-docker.outputs.tag }}" \
+ compile "${{ matrix.id }}.yaml"
diff --git a/.github/workflows/ci-github-scripts.yml b/.github/workflows/ci-github-scripts.yml
new file mode 100644
index 00000000000..43d530128cc
--- /dev/null
+++ b/.github/workflows/ci-github-scripts.yml
@@ -0,0 +1,27 @@
+name: CI - GitHub Scripts
+
+on:
+ push:
+ branches: [dev, beta, release]
+ paths:
+ - ".github/scripts/**"
+ - ".github/workflows/ci-github-scripts.yml"
+ pull_request:
+ paths:
+ - ".github/scripts/**"
+ - ".github/workflows/ci-github-scripts.yml"
+
+permissions:
+ contents: read
+
+jobs:
+ test-auto-label-pr:
+ name: Test auto-label-pr scripts
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out code from GitHub
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
+
+ - name: Run tests
+ working-directory: .github/scripts/auto-label-pr
+ run: npm test
diff --git a/.github/workflows/ci-memory-impact-comment.yml b/.github/workflows/ci-memory-impact-comment.yml
index 025b9609859..35cfce65f80 100644
--- a/.github/workflows/ci-memory-impact-comment.yml
+++ b/.github/workflows/ci-memory-impact-comment.yml
@@ -49,7 +49,7 @@ jobs:
- name: Check out code from base repository
if: steps.pr.outputs.skip != 'true'
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
# Always check out from the base repository (esphome/esphome), never from forks
# Use the PR's target branch to ensure we run trusted code from the main repo
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 53516db9138..29d42330cda 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -28,7 +28,7 @@ jobs:
cache-key: ${{ steps.cache-key.outputs.key }}
steps:
- name: Check out code from GitHub
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Generate cache-key
id: cache-key
run: echo key="${{ hashFiles('requirements.txt', 'requirements_dev.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
@@ -49,7 +49,7 @@ jobs:
# detects the activated venv via ``VIRTUAL_ENV`` so downstream jobs
# that ``. venv/bin/activate`` see an identical layout.
if: steps.cache-venv.outputs.cache-hit != 'true'
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
+ uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
# Pin uv version so the action does not have to fetch the
@@ -74,7 +74,7 @@ jobs:
if: needs.determine-jobs.outputs.python-linters == 'true'
steps:
- name: Check out code from GitHub
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -97,7 +97,7 @@ jobs:
if: needs.determine-jobs.outputs.core-ci == 'true'
steps:
- name: Check out code from GitHub
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -113,6 +113,7 @@ jobs:
script/build_language_schema.py --check
script/generate-esp32-boards.py --check
script/generate-rp2040-boards.py --check
+ script/ci_check_duplicate_test_ids.py
import-time:
name: Check import esphome.__main__ time
@@ -123,7 +124,7 @@ jobs:
if: needs.determine-jobs.outputs.import-time == 'true'
steps:
- name: Check out code from GitHub
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -151,11 +152,11 @@ jobs:
if: needs.determine-jobs.outputs.device-builder == 'true'
steps:
- name: Check out esphome (this PR)
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
path: esphome
- name: Check out esphome/device-builder
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
repository: esphome/device-builder
ref: main
@@ -170,7 +171,7 @@ jobs:
# install step (order-of-magnitude faster on cold boots,
# with its own wheel cache). actions/setup-python still
# provides the interpreter.
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
+ uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
# Pin uv version so the action does not have to fetch the
@@ -189,9 +190,12 @@ jobs:
- name: Run device-builder pytest
# ``-n auto`` runs under pytest-xdist (matches device-builder's
# own CI). No ``--cov`` here -- this is purely a downstream
- # smoke check against this PR's esphome code.
+ # smoke check against this PR's esphome code. ``tests/e2e/slow``
+ # is excluded: those are real multi-minute toolchain compiles
+ # (LibreTiny SDK clone, native ESP-IDF install) that device-builder
+ # runs in its own dedicated jobs, not this smoke check.
working-directory: device-builder
- run: pytest -q -n auto --maxfail=5 --durations=30 --no-cov --ignore=tests/benchmarks
+ run: pytest -q -n auto --maxfail=5 --durations=30 --no-cov --ignore=tests/benchmarks --ignore=tests/e2e/slow
pytest:
name: Run pytest
@@ -221,7 +225,7 @@ jobs:
if: needs.determine-jobs.outputs.core-ci == 'true'
steps:
- name: Check out code from GitHub
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Restore Python
id: restore-python
uses: ./.github/actions/restore-python
@@ -241,7 +245,7 @@ jobs:
. venv/bin/activate
pytest -vv --cov-report=xml --tb=native --durations=30 -n auto tests --ignore=tests/integration/
- name: Upload coverage to Codecov
- uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
+ uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
- name: Save Python virtual environment cache
@@ -266,8 +270,8 @@ jobs:
python-linters: ${{ steps.determine.outputs.python-linters }}
import-time: ${{ steps.determine.outputs.import-time }}
device-builder: ${{ steps.determine.outputs.device-builder }}
- native-idf: ${{ steps.determine.outputs.native-idf }}
- native-idf-components: ${{ steps.determine.outputs.native-idf-components }}
+ esp32-platformio: ${{ steps.determine.outputs.esp32-platformio }}
+ esp32-platformio-components: ${{ steps.determine.outputs.esp32-platformio-components }}
changed-components: ${{ steps.determine.outputs.changed-components }}
changed-components-with-tests: ${{ steps.determine.outputs.changed-components-with-tests }}
directly-changed-components-with-tests: ${{ steps.determine.outputs.directly-changed-components-with-tests }}
@@ -281,7 +285,7 @@ jobs:
benchmarks: ${{ steps.determine.outputs.benchmarks }}
steps:
- name: Check out code from GitHub
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
# Fetch enough history to find the merge base
fetch-depth: 2
@@ -320,8 +324,8 @@ jobs:
echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT
echo "import-time=$(echo "$output" | jq -r '.import_time')" >> $GITHUB_OUTPUT
echo "device-builder=$(echo "$output" | jq -r '.device_builder')" >> $GITHUB_OUTPUT
- echo "native-idf=$(echo "$output" | jq -r '.native_idf')" >> $GITHUB_OUTPUT
- echo "native-idf-components=$(echo "$output" | jq -r '.native_idf_components')" >> $GITHUB_OUTPUT
+ echo "esp32-platformio=$(echo "$output" | jq -r '.esp32_platformio')" >> $GITHUB_OUTPUT
+ echo "esp32-platformio-components=$(echo "$output" | jq -r '.esp32_platformio_components')" >> $GITHUB_OUTPUT
echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT
echo "changed-components-with-tests=$(echo "$output" | jq -c '.changed_components_with_tests')" >> $GITHUB_OUTPUT
echo "directly-changed-components-with-tests=$(echo "$output" | jq -c '.directly_changed_components_with_tests')" >> $GITHUB_OUTPUT
@@ -353,7 +357,7 @@ jobs:
bucket: ${{ fromJson(needs.determine-jobs.outputs.integration-test-buckets) }}
steps:
- name: Check out code from GitHub
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Set up Python 3.13
id: python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
@@ -368,7 +372,7 @@ jobs:
- name: Set up uv
# Only needed on cache miss to populate the venv.
if: steps.cache-venv.outputs.cache-hit != 'true'
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
+ uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
# Pin uv version so the action does not have to fetch the
@@ -405,7 +409,7 @@ jobs:
if: github.event_name == 'pull_request' && (needs.determine-jobs.outputs.cpp-unit-tests-run-all == 'true' || needs.determine-jobs.outputs.cpp-unit-tests-components != '[]')
steps:
- name: Check out code from GitHub
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Restore Python
uses: ./.github/actions/restore-python
@@ -434,7 +438,7 @@ jobs:
(github.event_name == 'pull_request' && needs.determine-jobs.outputs.benchmarks == 'true')
steps:
- name: Check out code from GitHub
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Restore Python
uses: ./.github/actions/restore-python
@@ -452,7 +456,7 @@ jobs:
echo "binary=$BINARY" >> $GITHUB_OUTPUT
- name: Run CodSpeed benchmarks
- uses: CodSpeedHQ/action@3194d9a39c4d46684cb44bf7207fc56626aad8fd # v4.15.1
+ uses: CodSpeedHQ/action@c145068895e045cc725ee76fcd2307624b65c3af # v4.17.5
with:
run: |
. venv/bin/activate
@@ -469,6 +473,8 @@ jobs:
if: needs.determine-jobs.outputs.clang-tidy == 'true'
env:
GH_TOKEN: ${{ github.token }}
+ # esp32-arduino-tidy installs ESP-IDF natively; share the native IDF cache.
+ ESPHOME_ESP_IDF_PREFIX: ~/.esphome-idf
strategy:
fail-fast: false
max-parallel: 2
@@ -479,9 +485,9 @@ jobs:
options: --environment esp8266-arduino-tidy --grep USE_ESP8266
pio_cache_key: tidyesp8266
- id: clang-tidy
- name: Run script/clang-tidy for ESP32 IDF
- options: --environment esp32-idf-tidy --grep USE_ESP_IDF
- pio_cache_key: tidyesp32-idf
+ name: Run script/clang-tidy for ESP32 Arduino
+ options: --environment esp32-arduino-tidy --grep USE_ARDUINO
+ cache_idf: true
- id: clang-tidy
name: Run script/clang-tidy for ZEPHYR
options: --environment nrf52-tidy --grep USE_ZEPHYR --grep USE_NRF52
@@ -490,7 +496,7 @@ jobs:
steps:
- name: Check out code from GitHub
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
@@ -502,44 +508,40 @@ jobs:
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache platformio
- if: github.ref == 'refs/heads/dev'
+ if: github.ref == 'refs/heads/dev' && matrix.pio_cache_key
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
- name: Cache platformio
- if: github.ref != 'refs/heads/dev'
+ if: github.ref != 'refs/heads/dev' && matrix.pio_cache_key
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
+ - name: Cache ESP-IDF install
+ if: matrix.cache_idf
+ uses: ./.github/actions/cache-esp-idf
+ with:
+ framework: arduino
+
- name: Register problem matchers
run: |
echo "::add-matcher::.github/workflows/matchers/gcc.json"
echo "::add-matcher::.github/workflows/matchers/clang-tidy.json"
- - name: Run 'pio run --list-targets -e esp32-idf-tidy'
- if: matrix.name == 'Run script/clang-tidy for ESP32 IDF'
- run: |
- . venv/bin/activate
- mkdir -p .temp
- pio run --list-targets -e esp32-idf-tidy
-
- name: Check if full clang-tidy scan needed
id: check_full_scan
run: |
. venv/bin/activate
- # determine-jobs.clang-tidy-full-scan is true when core C++ changed
- # OR the ci-run-all label forced --force-all. Independent of the
- # hash check, both must produce a full scan in the job itself.
+ # determine-jobs.clang-tidy-full-scan is true when core C++ or a
+ # clang-tidy-relevant config file changed, or the ci-run-all label
+ # forced --force-all.
if [ "${{ needs.determine-jobs.outputs.clang-tidy-full-scan }}" = "true" ]; then
echo "full_scan=true" >> $GITHUB_OUTPUT
echo "reason=determine_jobs" >> $GITHUB_OUTPUT
- elif python script/clang_tidy_hash.py --check; then
- echo "full_scan=true" >> $GITHUB_OUTPUT
- echo "reason=hash_changed" >> $GITHUB_OUTPUT
else
echo "full_scan=false" >> $GITHUB_OUTPUT
echo "reason=normal" >> $GITHUB_OUTPUT
@@ -565,7 +567,7 @@ jobs:
if: always()
clang-tidy-nosplit:
- name: Run script/clang-tidy for ESP32 Arduino
+ name: Run script/clang-tidy for ESP32 IDF
runs-on: ubuntu-24.04
needs:
- common
@@ -573,9 +575,11 @@ jobs:
if: needs.determine-jobs.outputs.clang-tidy-mode == 'nosplit'
env:
GH_TOKEN: ${{ github.token }}
+ # esp32-idf-tidy installs ESP-IDF natively; share the native IDF cache.
+ ESPHOME_ESP_IDF_PREFIX: ~/.esphome-idf
steps:
- name: Check out code from GitHub
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
@@ -586,19 +590,8 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- - name: Cache platformio
- if: github.ref == 'refs/heads/dev'
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
- with:
- path: ~/.platformio
- key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
-
- - name: Cache platformio
- if: github.ref != 'refs/heads/dev'
- uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
- with:
- path: ~/.platformio
- key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
+ - name: Cache ESP-IDF install
+ uses: ./.github/actions/cache-esp-idf
- name: Register problem matchers
run: |
@@ -609,15 +602,12 @@ jobs:
id: check_full_scan
run: |
. venv/bin/activate
- # determine-jobs.clang-tidy-full-scan is true when core C++ changed
- # OR the ci-run-all label forced --force-all. Independent of the
- # hash check, both must produce a full scan in the job itself.
+ # determine-jobs.clang-tidy-full-scan is true when core C++ or a
+ # clang-tidy-relevant config file changed, or the ci-run-all label
+ # forced --force-all.
if [ "${{ needs.determine-jobs.outputs.clang-tidy-full-scan }}" = "true" ]; then
echo "full_scan=true" >> $GITHUB_OUTPUT
echo "reason=determine_jobs" >> $GITHUB_OUTPUT
- elif python script/clang_tidy_hash.py --check; then
- echo "full_scan=true" >> $GITHUB_OUTPUT
- echo "reason=hash_changed" >> $GITHUB_OUTPUT
else
echo "full_scan=false" >> $GITHUB_OUTPUT
echo "reason=normal" >> $GITHUB_OUTPUT
@@ -628,10 +618,10 @@ jobs:
. venv/bin/activate
if [ "${{ steps.check_full_scan.outputs.full_scan }}" = "true" ]; then
echo "Running FULL clang-tidy scan (reason: ${{ steps.check_full_scan.outputs.reason }})"
- script/clang-tidy --all-headers --fix --environment esp32-arduino-tidy
+ script/clang-tidy --all-headers --fix --environment esp32-idf-tidy
else
echo "Running clang-tidy on changed files only"
- script/clang-tidy --all-headers --fix --changed --environment esp32-arduino-tidy
+ script/clang-tidy --all-headers --fix --changed --environment esp32-idf-tidy
fi
env:
# Also cache libdeps, store them in a ~/.platformio subfolder
@@ -650,27 +640,26 @@ jobs:
if: needs.determine-jobs.outputs.clang-tidy-mode == 'split'
env:
GH_TOKEN: ${{ github.token }}
+ # esp32-idf-tidy installs ESP-IDF natively; share the native IDF cache.
+ ESPHOME_ESP_IDF_PREFIX: ~/.esphome-idf
strategy:
fail-fast: false
- max-parallel: 2
+ max-parallel: 3
matrix:
include:
- id: clang-tidy
- name: Run script/clang-tidy for ESP32 Arduino 1/4
- options: --environment esp32-arduino-tidy --split-num 4 --split-at 1
+ name: Run script/clang-tidy for ESP32 IDF 1/3
+ options: --environment esp32-idf-tidy --split-num 3 --split-at 1
- id: clang-tidy
- name: Run script/clang-tidy for ESP32 Arduino 2/4
- options: --environment esp32-arduino-tidy --split-num 4 --split-at 2
+ name: Run script/clang-tidy for ESP32 IDF 2/3
+ options: --environment esp32-idf-tidy --split-num 3 --split-at 2
- id: clang-tidy
- name: Run script/clang-tidy for ESP32 Arduino 3/4
- options: --environment esp32-arduino-tidy --split-num 4 --split-at 3
- - id: clang-tidy
- name: Run script/clang-tidy for ESP32 Arduino 4/4
- options: --environment esp32-arduino-tidy --split-num 4 --split-at 4
+ name: Run script/clang-tidy for ESP32 IDF 3/3
+ options: --environment esp32-idf-tidy --split-num 3 --split-at 3
steps:
- name: Check out code from GitHub
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
@@ -681,19 +670,8 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- - name: Cache platformio
- if: github.ref == 'refs/heads/dev'
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
- with:
- path: ~/.platformio
- key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
-
- - name: Cache platformio
- if: github.ref != 'refs/heads/dev'
- uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
- with:
- path: ~/.platformio
- key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
+ - name: Cache ESP-IDF install
+ uses: ./.github/actions/cache-esp-idf
- name: Register problem matchers
run: |
@@ -704,15 +682,12 @@ jobs:
id: check_full_scan
run: |
. venv/bin/activate
- # determine-jobs.clang-tidy-full-scan is true when core C++ changed
- # OR the ci-run-all label forced --force-all. Independent of the
- # hash check, both must produce a full scan in the job itself.
+ # determine-jobs.clang-tidy-full-scan is true when core C++ or a
+ # clang-tidy-relevant config file changed, or the ci-run-all label
+ # forced --force-all.
if [ "${{ needs.determine-jobs.outputs.clang-tidy-full-scan }}" = "true" ]; then
echo "full_scan=true" >> $GITHUB_OUTPUT
echo "reason=determine_jobs" >> $GITHUB_OUTPUT
- elif python script/clang_tidy_hash.py --check; then
- echo "full_scan=true" >> $GITHUB_OUTPUT
- echo "reason=hash_changed" >> $GITHUB_OUTPUT
else
echo "full_scan=false" >> $GITHUB_OUTPUT
echo "reason=normal" >> $GITHUB_OUTPUT
@@ -736,6 +711,89 @@ jobs:
run: script/ci-suggest-changes
if: always()
+ clang-tidy-esp32-variants:
+ name: ${{ matrix.name }}
+ runs-on: ubuntu-24.04
+ needs:
+ - common
+ - determine-jobs
+ if: needs.determine-jobs.outputs.clang-tidy == 'true'
+ env:
+ GH_TOKEN: ${{ github.token }}
+ # The variant tidy envs install ESP-IDF natively; share the native IDF cache.
+ ESPHOME_ESP_IDF_PREFIX: ~/.esphome-idf
+ strategy:
+ fail-fast: false
+ max-parallel: 3
+ matrix:
+ include:
+ - id: clang-tidy
+ name: Run script/clang-tidy for ESP32 S3
+ options: --environment esp32s3-idf-tidy --grep USE_ESP32_VARIANT_ESP32S3
+ - id: clang-tidy
+ name: Run script/clang-tidy for ESP32 P4
+ # P4 has no native Wi-Fi/BLE; those run over the hosted co-processor,
+ # so their code paths differ -- lint them under the P4 build too.
+ # yamllint disable-line rule:line-length
+ options: --environment esp32p4-idf-tidy --grep USE_ESP32_VARIANT_ESP32P4 --grep USE_ESP32_HOSTED --grep USE_WIFI --grep USE_BLE
+ - id: clang-tidy
+ name: Run script/clang-tidy for ESP32 C6
+ # yamllint disable-line rule:line-length
+ options: --environment esp32c6-idf-tidy --grep USE_ESP32_VARIANT_ESP32C6 --grep USE_OPENTHREAD --grep USE_ZIGBEE
+
+ steps:
+ - name: Check out code from GitHub
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
+ with:
+ # Need history for HEAD~1 to work for checking changed files
+ fetch-depth: 2
+
+ - name: Restore Python
+ uses: ./.github/actions/restore-python
+ with:
+ python-version: ${{ env.DEFAULT_PYTHON }}
+ cache-key: ${{ needs.common.outputs.cache-key }}
+
+ - name: Cache ESP-IDF install
+ uses: ./.github/actions/cache-esp-idf
+
+ - name: Register problem matchers
+ run: |
+ echo "::add-matcher::.github/workflows/matchers/gcc.json"
+ echo "::add-matcher::.github/workflows/matchers/clang-tidy.json"
+
+ - name: Check if full clang-tidy scan needed
+ id: check_full_scan
+ run: |
+ . venv/bin/activate
+ # determine-jobs.clang-tidy-full-scan is true when core C++ or a
+ # clang-tidy-relevant config file changed, or the ci-run-all label
+ # forced --force-all.
+ if [ "${{ needs.determine-jobs.outputs.clang-tidy-full-scan }}" = "true" ]; then
+ echo "full_scan=true" >> $GITHUB_OUTPUT
+ echo "reason=determine_jobs" >> $GITHUB_OUTPUT
+ else
+ echo "full_scan=false" >> $GITHUB_OUTPUT
+ echo "reason=normal" >> $GITHUB_OUTPUT
+ fi
+
+ - name: Run clang-tidy
+ # Limited variant scan: only the files carrying that variant's code paths
+ # (no --all-headers; the comprehensive esp32-idf pass covers the shared tree).
+ run: |
+ . venv/bin/activate
+ if [ "${{ steps.check_full_scan.outputs.full_scan }}" = "true" ]; then
+ echo "Running FULL clang-tidy scan (reason: ${{ steps.check_full_scan.outputs.reason }})"
+ script/clang-tidy --fix ${{ matrix.options }}
+ else
+ echo "Running clang-tidy on changed files only"
+ script/clang-tidy --fix --changed ${{ matrix.options }}
+ fi
+
+ - name: Suggested changes
+ run: script/ci-suggest-changes
+ if: always()
+
test-build-components-split:
name: Test components batch (${{ matrix.components }})
runs-on: ubuntu-24.04
@@ -743,6 +801,10 @@ jobs:
- common
- determine-jobs
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0
+ env:
+ # esp32 component builds use the native ESP-IDF toolchain (default), so
+ # share the tidy jobs' install location -- the restore below lands here.
+ ESPHOME_ESP_IDF_PREFIX: ~/.esphome-idf
strategy:
fail-fast: false
max-parallel: ${{ (startsWith(github.base_ref, 'beta') || startsWith(github.base_ref, 'release')) && 8 || 4 }}
@@ -764,12 +826,18 @@ jobs:
version: 1.0
- name: Check out code from GitHub
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
+ - name: Cache ESP-IDF install (restore-only)
+ # A batch may contain no esp32 build, so never save -- just reuse the
+ # shared install the dev tidy jobs already cached when present.
+ uses: ./.github/actions/cache-esp-idf
+ with:
+ restore-only: true
- name: Validate and compile components with intelligent grouping
run: |
. venv/bin/activate
@@ -821,7 +889,7 @@ jobs:
fi
echo ""
- # Show disk space before validation (after bind mounts setup)
+ # Show disk space before validation
echo "Disk space before config validation:"
df -h
echo ""
@@ -873,23 +941,22 @@ jobs:
echo "All components in this batch are validate-only -- skipping compile stage."
fi
- test-native-idf:
- name: Test components with native ESP-IDF
+ test-esp32-platformio:
+ name: Test esp32 components with PlatformIO
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
- if: github.event_name == 'pull_request' && needs.determine-jobs.outputs.native-idf == 'true'
+ if: github.event_name == 'pull_request' && needs.determine-jobs.outputs.esp32-platformio == 'true'
env:
- ESPHOME_ESP_IDF_PREFIX: ~/.esphome-idf
- # Comma-joined subset of the native-IDF representative component list,
- # computed by script/determine-jobs.py (native_idf_components_to_test).
+ # Comma-joined subset of the esp32 PlatformIO representative component list,
+ # computed by script/determine-jobs.py (esp32_platformio_components_to_test).
# Single source of truth -- the full list lives in
- # script/determine-jobs.py::NATIVE_IDF_TEST_COMPONENTS.
- TEST_COMPONENTS: ${{ needs.determine-jobs.outputs.native-idf-components }}
+ # script/determine-jobs.py::ESP32_PLATFORMIO_TEST_COMPONENTS.
+ TEST_COMPONENTS: ${{ needs.determine-jobs.outputs.esp32-platformio-components }}
steps:
- name: Check out code from GitHub
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Restore Python
uses: ./.github/actions/restore-python
@@ -897,70 +964,22 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- - name: Cache ESPHome
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
- with:
- path: ~/.esphome-idf
- key: ${{ runner.os }}-esphome-${{ needs.common.outputs.cache-key }}
-
- - name: Run native ESP-IDF compile test
+ - name: Run PlatformIO compile test
run: |
. venv/bin/activate
- # Check if /mnt has more free space than / before bind mounting
- # Extract available space in KB for comparison
- root_avail=$(df -k / | awk 'NR==2 {print $4}')
- mnt_avail=$(df -k /mnt 2>/dev/null | awk 'NR==2 {print $4}')
-
- echo "Available space: / has ${root_avail}KB, /mnt has ${mnt_avail}KB"
-
- # Only use /mnt if it has more space than /
- if [ -n "$mnt_avail" ] && [ "$mnt_avail" -gt "$root_avail" ]; then
- echo "Using /mnt for build files (more space available)"
- # Bind mount PlatformIO directory to /mnt (tools, packages, build cache all go there)
- sudo mkdir -p /mnt/esphome-idf
- sudo chown $USER:$USER /mnt/esphome-idf
- mkdir -p ~/.esphome-idf
- sudo mount --bind /mnt/esphome-idf ~/.esphome-idf
-
- # Bind mount test build directory to /mnt
- sudo mkdir -p /mnt/test_build_components_build
- sudo chown $USER:$USER /mnt/test_build_components_build
- mkdir -p tests/test_build_components/build
- sudo mount --bind /mnt/test_build_components_build tests/test_build_components/build
- else
- echo "Using / for build files (more space available than /mnt or /mnt unavailable)"
- fi
-
echo "Testing components: $TEST_COMPONENTS"
echo ""
- # Show disk space before validation (after bind mounts setup)
- echo "Disk space before config validation:"
- df -h
- echo ""
-
# Run config validation (auto-grouped by test_build_components.py)
- python3 script/test_build_components.py -e config -t esp32-idf -c "$TEST_COMPONENTS" -f --toolchain esp-idf
+ python3 script/test_build_components.py -e config -t esp32-idf -c "$TEST_COMPONENTS" -f --toolchain platformio
echo ""
echo "Config validation passed! Starting compilation..."
echo ""
- # Show disk space before compilation
- echo "Disk space before compilation:"
- df -h
- echo ""
-
# Run compilation (auto-grouped by test_build_components.py)
- python3 script/test_build_components.py -e compile -t esp32-idf -c "$TEST_COMPONENTS" -f --toolchain esp-idf
-
- - name: Save ESPHome cache
- if: github.ref == 'refs/heads/dev'
- uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
- with:
- path: ~/.esphome-idf
- key: ${{ runner.os }}-esphome-${{ needs.common.outputs.cache-key }}
+ python3 script/test_build_components.py -e compile -t esp32-idf -c "$TEST_COMPONENTS" -f --toolchain platformio
pre-commit-ci-lite:
name: pre-commit.ci lite
@@ -971,7 +990,7 @@ jobs:
if: github.event_name == 'pull_request' && !startsWith(github.base_ref, 'beta') && !startsWith(github.base_ref, 'release') && needs.determine-jobs.outputs.core-ci == 'true'
steps:
- name: Check out code from GitHub
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -979,7 +998,7 @@ jobs:
cache-key: ${{ needs.common.outputs.cache-key }}
- uses: esphome/pre-commit-action@43cd1109c09c544d97196f7730ee5b2e0cc6d81e # v3.0.1 fork with pinned actions/cache
env:
- SKIP: pylint,clang-tidy-hash,ci-custom
+ SKIP: pylint,ci-custom
- uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0
if: always()
@@ -997,7 +1016,7 @@ jobs:
skip: ${{ steps.check-script.outputs.skip || steps.check-tests.outputs.skip }}
steps:
- name: Check out target branch
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ github.base_ref }}
@@ -1179,7 +1198,7 @@ jobs:
flash_usage: ${{ steps.extract.outputs.flash_usage }}
steps:
- name: Check out PR branch
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -1248,7 +1267,7 @@ jobs:
GH_TOKEN: ${{ github.token }}
steps:
- name: Check out code
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -1291,10 +1310,11 @@ jobs:
- clang-tidy-single
- clang-tidy-nosplit
- clang-tidy-split
+ - clang-tidy-esp32-variants
- determine-jobs
- device-builder
- test-build-components-split
- - test-native-idf
+ - test-esp32-platformio
- pre-commit-ci-lite
- memory-impact-target-branch
- memory-impact-pr-branch
@@ -1310,4 +1330,7 @@ jobs:
# 1. The target branch has a build issue independent of this PR
# 2. This PR fixes a build issue on the target branch
# In either case, we only care that the PR branch builds successfully.
- echo "$NEEDS_JSON" | jq -e 'del(.["memory-impact-target-branch"]) | all(.result != "failure")'
+ # Every other job must have succeeded or been skipped; a "cancelled" or
+ # "failure" result fails this check so CI is not reported green when the
+ # workflow was cancelled.
+ echo "$NEEDS_JSON" | jq -e 'del(.["memory-impact-target-branch"]) | all(.result == "success" or .result == "skipped")'
diff --git a/.github/workflows/codeowner-approved-label-update.yml b/.github/workflows/codeowner-approved-label-update.yml
index 013517bde6d..1bd60fd11d8 100644
--- a/.github/workflows/codeowner-approved-label-update.yml
+++ b/.github/workflows/codeowner-approved-label-update.yml
@@ -26,7 +26,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout base branch
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ github.event.pull_request.base.sha }}
sparse-checkout: |
diff --git a/.github/workflows/codeowner-review-request.yml b/.github/workflows/codeowner-review-request.yml
index 7cdbfcf3285..5ad0b02de16 100644
--- a/.github/workflows/codeowner-review-request.yml
+++ b/.github/workflows/codeowner-review-request.yml
@@ -29,7 +29,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout base branch
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ github.event.pull_request.base.sha }}
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index dfc0e08bfa8..e559472b60c 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -52,11 +52,11 @@ jobs:
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
+ uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -84,6 +84,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
+ uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
category: "/language:${{matrix.language}}"
diff --git a/.github/workflows/pr-title-check.yml b/.github/workflows/pr-title-check.yml
index e8320672d2b..0e2efb1bcf5 100644
--- a/.github/workflows/pr-title-check.yml
+++ b/.github/workflows/pr-title-check.yml
@@ -16,7 +16,7 @@ jobs:
name: Validate PR title
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 344bd416c6b..8efc395951a 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -20,7 +20,7 @@ jobs:
branch_build: ${{ steps.tag.outputs.branch_build }}
deploy_env: ${{ steps.tag.outputs.deploy_env }}
steps:
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Get tag
id: tag
# yamllint disable rule:line-length
@@ -60,7 +60,7 @@ jobs:
contents: read # actions/checkout to build the sdist/wheel
id-token: write # OIDC token for PyPI Trusted Publishing (pypa/gh-action-pypi-publish)
steps:
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
@@ -92,7 +92,7 @@ jobs:
os: "ubuntu-24.04-arm"
steps:
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
@@ -168,7 +168,7 @@ jobs:
- ghcr
- dockerhub
steps:
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Download digests
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
diff --git a/.github/workflows/sync-device-classes.yml b/.github/workflows/sync-device-classes.yml
index 84be3c8e226..ab1ce2b5874 100644
--- a/.github/workflows/sync-device-classes.yml
+++ b/.github/workflows/sync-device-classes.yml
@@ -28,10 +28,10 @@ jobs:
permission-pull-requests: write # pulls.create / pulls.update to open or refresh the sync PR
- name: Checkout
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Checkout Home Assistant
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
repository: home-assistant/core
path: lib/home-assistant
@@ -47,7 +47,7 @@ jobs:
# setup-python interpreter so subsequent ``pre-commit`` /
# ``script/run-in-env.py`` steps find the deps without a
# ``uv run`` prefix.
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
+ uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
# Pin uv version so the action does not have to fetch the
diff --git a/.gitignore b/.gitignore
index 4a4a88fd48f..de3e4fa68e9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -141,6 +141,7 @@ tests/.esphome/
sdkconfig.*
!sdkconfig.defaults
+!sdkconfig.defaults.*
.tests/
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 0470a948f59..ba74aff07cf 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -6,12 +6,12 @@ ci:
autoupdate_commit_msg: 'pre-commit: autoupdate'
autoupdate_schedule: off # Disabled until ruff versions are synced between deps and pre-commit
# Skip hooks that have issues in pre-commit CI environment
- skip: [pylint, clang-tidy-hash]
+ skip: [pylint]
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
- rev: v0.15.14
+ rev: v0.15.15
hooks:
# Run the linter.
- id: ruff
@@ -59,13 +59,6 @@ repos:
language: system
types: [python]
files: ^esphome/.+\.py$
- - id: clang-tidy-hash
- name: Update clang-tidy hash
- entry: python script/clang_tidy_hash.py --update-if-changed
- language: python
- files: ^(\.clang-tidy|platformio\.ini|requirements_dev\.txt)$
- pass_filenames: false
- additional_dependencies: []
- id: ci-custom
name: ci-custom
entry: python script/run-in-env.py script/ci-custom.py
diff --git a/AGENTS.md b/AGENTS.md
index 2139a2b796d..4346ffbdae0 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -59,6 +59,19 @@ This document provides essential context for AI models interacting with this pro
- Protected/private fields: `lower_snake_case_with_trailing_underscore_`
- Favor descriptive names over abbreviations
+* **Python Idioms:**
+ * **Assignment expressions (PEP 572):** Prefer the walrus operator (`:=`) wherever it removes a redundant lookup or a throwaway temporary. The most common case in component code is presence-checking a config key and then indexing it separately — fetch once with `.get()` and bind in the condition instead:
+ ```python
+ # Bad - looks up CONF_BLAH twice
+ if CONF_BLAH in config:
+ cg.add(var.set_blah(config[CONF_BLAH]))
+
+ # Good - single lookup, value bound inline
+ if (blah := config.get(CONF_BLAH)) is not None:
+ cg.add(var.set_blah(blah))
+ ```
+ The same applies to `while` loops and comprehensions where it avoids recomputing a value. Don't contort code to use it — reach for `:=` only when it genuinely cuts repetition or an extra assignment line.
+
* **C++ Field Visibility:**
* **Prefer `protected`:** Use `protected` for most class fields to enable extensibility and testing. Fields should be `lower_snake_case_with_trailing_underscore_`.
* **Use `private` for safety-critical cases:** Use `private` visibility when direct field access could introduce bugs or violate invariants:
@@ -462,7 +475,7 @@ This document provides essential context for AI models interacting with this pro
6. **Pull Request:** Submit a PR against the `dev` branch. The Pull Request title should have a prefix of the component being worked on (e.g., `[display] Fix bug`, `[abc123] Add new component`). Update documentation, examples, and add `CODEOWNERS` entries as needed. Pull requests should always be made using the `.github/PULL_REQUEST_TEMPLATE.md` template - fill out all sections completely without removing any parts of the template.
* **Documentation Contributions:**
- * Documentation is hosted in the separate `esphome/esphome-docs` repository.
+ * Documentation is hosted in the separate `esphome/esphome.io` repository.
* The contribution workflow is the same as for the codebase.
* When editing a component's documentation page, also update the corresponding component index page to ensure both pages remain in sync.
@@ -681,7 +694,7 @@ This document provides essential context for AI models interacting with this pro
- [ ] Explored non-breaking alternatives
- [ ] Added deprecation warnings if possible (use `ESPDEPRECATED` macro for C++)
- [ ] Documented migration path in PR description with before/after examples
- - [ ] Updated all internal usage and esphome-docs
+ - [ ] Updated all internal usage and esphome.io
- [ ] Tested backward compatibility during deprecation period
* **Deprecation Pattern (C++):**
diff --git a/CODEOWNERS b/CODEOWNERS
index 3c3e502058d..3265627c030 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -19,7 +19,6 @@ esphome/components/ac_dimmer/* @glmnet
esphome/components/adc/* @esphome/core
esphome/components/adc128s102/* @DeerMaximum
esphome/components/addressable_light/* @justfalter
-esphome/components/ade7880/* @kpfleming
esphome/components/ade7953/* @angelnu
esphome/components/ade7953_base/* @angelnu
esphome/components/ade7953_i2c/* @angelnu
@@ -28,7 +27,7 @@ esphome/components/ads1118/* @solomondg1
esphome/components/ags10/* @mak-42
esphome/components/aic3204/* @kbx81
esphome/components/airthings_ble/* @jeromelaban
-esphome/components/airthings_wave_base/* @jeromelaban @kpfleming @ncareau
+esphome/components/airthings_wave_base/* @jeromelaban @ncareau
esphome/components/airthings_wave_mini/* @ncareau
esphome/components/airthings_wave_plus/* @jeromelaban @precurse
esphome/components/alarm_control_panel/* @grahambrown11 @hwstar
@@ -84,6 +83,7 @@ esphome/components/bme680_bsec/* @trvrnrth
esphome/components/bme68x_bsec2/* @kbx81 @neffs
esphome/components/bme68x_bsec2_i2c/* @kbx81 @neffs
esphome/components/bmi160/* @flaviut
+esphome/components/bmi270/* @clydebarrow
esphome/components/bmp280_base/* @ademuri
esphome/components/bmp280_i2c/* @ademuri
esphome/components/bmp280_spi/* @ademuri
@@ -139,7 +139,7 @@ esphome/components/dfplayer/* @glmnet
esphome/components/dfrobot_sen0395/* @niklasweber
esphome/components/dht/* @OttoWinter
esphome/components/display_menu_base/* @numo68
-esphome/components/dlms_meter/* @SimonFischer04
+esphome/components/dlms_meter/* @latonita @PolarGoose @SimonFischer04 @Tomer27cz
esphome/components/dps310/* @kbx81
esphome/components/ds1307/* @badbadc0ffee
esphome/components/ds2484/* @mrk-its
@@ -291,6 +291,7 @@ esphome/components/lock/* @esphome/core
esphome/components/logger/* @esphome/core
esphome/components/logger/select/* @clydebarrow
esphome/components/lps22/* @nagisa
+esphome/components/lsm6ds/* @clydebarrow
esphome/components/ltr390/* @latonita @sjtrny
esphome/components/ltr501/* @latonita
esphome/components/ltr_als_ps/* @latonita
@@ -351,6 +352,7 @@ esphome/components/modbus_server/* @exciton
esphome/components/mopeka_ble/* @Fabian-Schmidt @spbrogan
esphome/components/mopeka_pro_check/* @spbrogan
esphome/components/mopeka_std_check/* @Fabian-Schmidt
+esphome/components/motion/* @esphome/core
esphome/components/mpl3115a2/* @kbickar
esphome/components/mpu6886/* @fabaff
esphome/components/ms8607/* @e28eta
@@ -379,6 +381,7 @@ esphome/components/pca6416a/* @Mat931
esphome/components/pca9554/* @bdraco @clydebarrow @hwstar
esphome/components/pcf85063/* @brogon
esphome/components/pcf8563/* @KoenBreeman
+esphome/components/pcm5122/* @remcom
esphome/components/pi4ioe5v6408/* @jesserockz
esphome/components/pid/* @OttoWinter
esphome/components/pipsolar/* @andreashergert1984
@@ -558,6 +561,7 @@ esphome/components/uart/packet_transport/* @clydebarrow
esphome/components/udp/* @clydebarrow
esphome/components/ufire_ec/* @pvizeli
esphome/components/ufire_ise/* @pvizeli
+esphome/components/ufm01/* @ljungqvist
esphome/components/ultrasonic/* @ssieb @swoboda1337
esphome/components/update/* @jesserockz
esphome/components/uponor_smatrix/* @kroimon
@@ -595,6 +599,7 @@ esphome/components/wk2212_spi/* @DrCoolZic
esphome/components/wl_134/* @hobbypunk90
esphome/components/wts01/* @alepee
esphome/components/x9c/* @EtienneMD
+esphome/components/xdb401/* @RT530
esphome/components/xgzp68xx/* @gcormier
esphome/components/xiaomi_hhccjcy10/* @fariouche
esphome/components/xiaomi_lywsd02mmc/* @juanluss31
diff --git a/Doxyfile b/Doxyfile
index 3537516996b..9f4e20b977f 100644
--- a/Doxyfile
+++ b/Doxyfile
@@ -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.6.0-dev
+PROJECT_NUMBER = 2026.7.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
diff --git a/codecov.yml b/codecov.yml
new file mode 100644
index 00000000000..f8afbbde049
--- /dev/null
+++ b/codecov.yml
@@ -0,0 +1,18 @@
+coverage:
+ status:
+ patch:
+ default:
+ target: 100%
+ threshold: 0%
+ project:
+ default:
+ informational: true
+
+ignore:
+ - "esphome/components/**/*"
+ - "esphome/analyze_memory/**/*"
+ - "tests/integration/**/*"
+
+comment:
+ layout: "reach, diff, flags, files"
+ require_changes: true
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 25de9472b63..18a99037351 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -1,10 +1,9 @@
ARG BUILD_VERSION=dev
-ARG BUILD_OS=alpine
-ARG BUILD_BASE_VERSION=2025.04.0
+ARG BUILD_BASE_VERSION=2026.06.0
ARG BUILD_TYPE=docker
-FROM ghcr.io/esphome/docker-base:${BUILD_OS}-${BUILD_BASE_VERSION} AS base-source-docker
-FROM ghcr.io/esphome/docker-base:${BUILD_OS}-ha-addon-${BUILD_BASE_VERSION} AS base-source-ha-addon
+FROM ghcr.io/esphome/docker-base:debian-${BUILD_BASE_VERSION} AS base-source-docker
+FROM ghcr.io/esphome/docker-base:debian-ha-addon-${BUILD_BASE_VERSION} AS base-source-ha-addon
ARG BUILD_TYPE
FROM base-source-${BUILD_TYPE} AS base
@@ -18,13 +17,9 @@ RUN git config --system --add safe.directory "*" \
# validate openocd-esp32 (it dynamically links libusb-1.0.so.0); without
# it idf_tools.py rejects the openocd install with exit 127 and aborts
# the whole framework setup.
-RUN if command -v apk > /dev/null; then \
- apk add --no-cache build-base libusb; \
- else \
- apt-get update \
- && apt-get install -y --no-install-recommends build-essential libusb-1.0-0 \
- && rm -rf /var/lib/apt/lists/*; \
- fi
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends build-essential libusb-1.0-0 \
+ && rm -rf /var/lib/apt/lists/*
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
@@ -36,6 +31,9 @@ RUN \
uv pip install --no-cache-dir \
-r /requirements.txt
+# Install the ESPHome Device Builder dashboard.
+RUN uv pip install --no-cache-dir esphome-device-builder==1.0.9
+
RUN \
platformio settings set enable_telemetry No \
&& platformio settings set check_platformio_interval 1000000 \
diff --git a/docker/build.py b/docker/build.py
index 4d093cf88df..475986e905a 100755
--- a/docker/build.py
+++ b/docker/build.py
@@ -20,6 +20,10 @@ TYPE_HA_ADDON = "ha-addon"
TYPE_LINT = "lint"
TYPES = [TYPE_DOCKER, TYPE_HA_ADDON, TYPE_LINT]
+REGISTRY_GHCR = "ghcr"
+REGISTRY_DOCKERHUB = "dockerhub"
+REGISTRIES = [REGISTRY_GHCR, REGISTRY_DOCKERHUB]
+
parser = argparse.ArgumentParser()
parser.add_argument(
@@ -34,6 +38,12 @@ parser.add_argument(
parser.add_argument(
"--build-type", choices=TYPES, required=True, help="The type of build to run"
)
+parser.add_argument(
+ "--registry",
+ choices=REGISTRIES,
+ action="append",
+ help="Restrict to specific registries (default: all). May be passed multiple times.",
+)
parser.add_argument(
"--dry-run", action="store_true", help="Don't run any commands, just print them"
)
@@ -45,6 +55,11 @@ build_parser.add_argument("--push", help="Also push the images", action="store_t
build_parser.add_argument(
"--load", help="Load the docker image locally", action="store_true"
)
+build_parser.add_argument(
+ "--no-cache-to",
+ help="Don't write the build cache (avoids polluting the shared cache)",
+ action="store_true",
+)
manifest_parser = subparsers.add_parser(
"manifest", help="Create a manifest from already pushed images"
)
@@ -95,11 +110,14 @@ def main():
print("Command failed")
sys.exit(1)
+ registries = args.registry or REGISTRIES
+
# detect channel from tag
match = re.match(r"^(\d+\.\d+)(?:\.\d+)?(b\d+)?$", args.tag)
major_minor_version = None
if match is None:
- channel = CHANNEL_DEV
+ # Custom tag (e.g. a branch name) -- push only the tag itself
+ channel = None
elif match.group(2) is None:
major_minor_version = match.group(1)
channel = CHANNEL_RELEASE
@@ -128,11 +146,18 @@ def main():
CHANNEL_DEV: "cache-dev",
CHANNEL_BETA: "cache-beta",
CHANNEL_RELEASE: "cache-latest",
- }[channel]
- cache_img = f"ghcr.io/{params.build_to}:{cache_tag}"
+ }.get(channel, "cache-dev")
+ # Cache images live alongside the pushed images; prefer GHCR when it is
+ # one of the selected registries, otherwise fall back to Docker Hub so a
+ # registry-restricted build doesn't need GHCR auth.
+ cache_prefix = "ghcr.io/" if REGISTRY_GHCR in registries else ""
+ cache_img = f"{cache_prefix}{params.build_to}:{cache_tag}"
- imgs = [f"{params.build_to}:{tag}" for tag in tags_to_push]
- imgs += [f"ghcr.io/{params.build_to}:{tag}" for tag in tags_to_push]
+ imgs = []
+ if REGISTRY_DOCKERHUB in registries:
+ imgs += [f"{params.build_to}:{tag}" for tag in tags_to_push]
+ if REGISTRY_GHCR in registries:
+ imgs += [f"ghcr.io/{params.build_to}:{tag}" for tag in tags_to_push]
# 3. build
cmd = [
@@ -155,7 +180,9 @@ def main():
for img in imgs:
cmd += ["--tag", img]
if args.push:
- cmd += ["--push", "--cache-to", f"type=registry,ref={cache_img},mode=max"]
+ cmd += ["--push"]
+ if not args.no_cache_to:
+ cmd += ["--cache-to", f"type=registry,ref={cache_img},mode=max"]
if args.load:
cmd += ["--load"]
@@ -163,20 +190,22 @@ def main():
elif args.command == "manifest":
manifest = DockerParams.for_type_arch(args.build_type, ARCH_AMD64).manifest_to
- targets = [f"{manifest}:{tag}" for tag in tags_to_push]
- targets += [f"ghcr.io/{manifest}:{tag}" for tag in tags_to_push]
- # 1. Create manifests
+ targets = []
+ if REGISTRY_DOCKERHUB in registries:
+ targets += [f"{manifest}:{tag}" for tag in tags_to_push]
+ if REGISTRY_GHCR in registries:
+ targets += [f"ghcr.io/{manifest}:{tag}" for tag in tags_to_push]
+ # Use buildx imagetools (not `docker manifest`) so the per-arch sources,
+ # which buildx pushes as single-platform manifest lists, are combined
+ # and pushed correctly in one step.
for target in targets:
- cmd = ["docker", "manifest", "create", target]
+ cmd = ["docker", "buildx", "imagetools", "create", "--tag", target]
for arch in ARCHS:
src = f"{DockerParams.for_type_arch(args.build_type, arch).build_to}:{args.tag}"
if target.startswith("ghcr.io"):
src = f"ghcr.io/{src}"
cmd.append(src)
run_command(*cmd)
- # 2. Push manifests
- for target in targets:
- run_command("docker", "manifest", "push", target)
if __name__ == "__main__":
diff --git a/docker/docker_entrypoint.sh b/docker/docker_entrypoint.sh
index 1b9224244ca..18baf40c29b 100755
--- a/docker/docker_entrypoint.sh
+++ b/docker/docker_entrypoint.sh
@@ -27,4 +27,12 @@ if [[ -d /build ]]; then
export ESPHOME_BUILD_PATH=/build
fi
+# The default CMD is "dashboard /config". Route the dashboard to the new
+# Device Builder, but pass every other subcommand (compile, run, config,
+# logs, ...) straight through to the esphome CLI so direct CLI use keeps working.
+if [[ "$1" == "dashboard" ]]; then
+ shift
+ exec esphome-device-builder "$@"
+fi
+
exec esphome "$@"
diff --git a/docker/ha-addon-rootfs/etc/cont-init.d/40-device-builder.sh b/docker/ha-addon-rootfs/etc/cont-init.d/40-device-builder.sh
deleted file mode 100755
index b9904697626..00000000000
--- a/docker/ha-addon-rootfs/etc/cont-init.d/40-device-builder.sh
+++ /dev/null
@@ -1,22 +0,0 @@
-#!/usr/bin/with-contenv bashio
-# ==============================================================================
-# Installs the latest prerelease of esphome-device-builder when the
-# `use_new_device_builder` config option is enabled.
-# This is a temporary install-on-boot step until esphome-device-builder
-# becomes a direct dependency of esphome.
-# ==============================================================================
-
-if ! bashio::config.true 'use_new_device_builder'; then
- exit 0
-fi
-
-bashio::log.info "Installing latest prerelease of esphome-device-builder..."
-if command -v uv > /dev/null; then
- uv pip install --system --no-cache-dir --prerelease=allow --upgrade \
- esphome-device-builder ||
- bashio::exit.nok "Failed installing esphome-device-builder."
-else
- pip install --no-cache-dir --pre --upgrade esphome-device-builder ||
- bashio::exit.nok "Failed installing esphome-device-builder."
-fi
-bashio::log.info "Installed esphome-device-builder."
diff --git a/docker/ha-addon-rootfs/etc/nginx/includes/mime.types b/docker/ha-addon-rootfs/etc/nginx/includes/mime.types
deleted file mode 100644
index 7c7cdef2d1a..00000000000
--- a/docker/ha-addon-rootfs/etc/nginx/includes/mime.types
+++ /dev/null
@@ -1,96 +0,0 @@
-types {
- text/html html htm shtml;
- text/css css;
- text/xml xml;
- image/gif gif;
- image/jpeg jpeg jpg;
- application/javascript js;
- application/atom+xml atom;
- application/rss+xml rss;
-
- text/mathml mml;
- text/plain txt;
- text/vnd.sun.j2me.app-descriptor jad;
- text/vnd.wap.wml wml;
- text/x-component htc;
-
- image/png png;
- image/svg+xml svg svgz;
- image/tiff tif tiff;
- image/vnd.wap.wbmp wbmp;
- image/webp webp;
- image/x-icon ico;
- image/x-jng jng;
- image/x-ms-bmp bmp;
-
- font/woff woff;
- font/woff2 woff2;
-
- application/java-archive jar war ear;
- application/json json;
- application/mac-binhex40 hqx;
- application/msword doc;
- application/pdf pdf;
- application/postscript ps eps ai;
- application/rtf rtf;
- application/vnd.apple.mpegurl m3u8;
- application/vnd.google-earth.kml+xml kml;
- application/vnd.google-earth.kmz kmz;
- application/vnd.ms-excel xls;
- application/vnd.ms-fontobject eot;
- application/vnd.ms-powerpoint ppt;
- application/vnd.oasis.opendocument.graphics odg;
- application/vnd.oasis.opendocument.presentation odp;
- application/vnd.oasis.opendocument.spreadsheet ods;
- application/vnd.oasis.opendocument.text odt;
- application/vnd.openxmlformats-officedocument.presentationml.presentation
- pptx;
- application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
- xlsx;
- application/vnd.openxmlformats-officedocument.wordprocessingml.document
- docx;
- application/vnd.wap.wmlc wmlc;
- application/x-7z-compressed 7z;
- application/x-cocoa cco;
- application/x-java-archive-diff jardiff;
- application/x-java-jnlp-file jnlp;
- application/x-makeself run;
- application/x-perl pl pm;
- application/x-pilot prc pdb;
- application/x-rar-compressed rar;
- application/x-redhat-package-manager rpm;
- application/x-sea sea;
- application/x-shockwave-flash swf;
- application/x-stuffit sit;
- application/x-tcl tcl tk;
- application/x-x509-ca-cert der pem crt;
- application/x-xpinstall xpi;
- application/xhtml+xml xhtml;
- application/xspf+xml xspf;
- application/zip zip;
-
- application/octet-stream bin exe dll;
- application/octet-stream deb;
- application/octet-stream dmg;
- application/octet-stream iso img;
- application/octet-stream msi msp msm;
-
- audio/midi mid midi kar;
- audio/mpeg mp3;
- audio/ogg ogg;
- audio/x-m4a m4a;
- audio/x-realaudio ra;
-
- video/3gpp 3gpp 3gp;
- video/mp2t ts;
- video/mp4 mp4;
- video/mpeg mpeg mpg;
- video/quicktime mov;
- video/webm webm;
- video/x-flv flv;
- video/x-m4v m4v;
- video/x-mng mng;
- video/x-ms-asf asx asf;
- video/x-ms-wmv wmv;
- video/x-msvideo avi;
-}
diff --git a/docker/ha-addon-rootfs/etc/nginx/includes/proxy_params.conf b/docker/ha-addon-rootfs/etc/nginx/includes/proxy_params.conf
deleted file mode 100644
index a1ebb5079ad..00000000000
--- a/docker/ha-addon-rootfs/etc/nginx/includes/proxy_params.conf
+++ /dev/null
@@ -1,16 +0,0 @@
-proxy_http_version 1.1;
-proxy_ignore_client_abort off;
-proxy_read_timeout 86400s;
-proxy_redirect off;
-proxy_send_timeout 86400s;
-proxy_max_temp_file_size 0;
-
-proxy_set_header Accept-Encoding "";
-proxy_set_header Connection $connection_upgrade;
-proxy_set_header Host $http_host;
-proxy_set_header Upgrade $http_upgrade;
-proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
-proxy_set_header X-Forwarded-Proto $scheme;
-proxy_set_header X-NginX-Proxy true;
-proxy_set_header X-Real-IP $remote_addr;
-proxy_set_header Authorization "";
diff --git a/docker/ha-addon-rootfs/etc/nginx/includes/server_params.conf b/docker/ha-addon-rootfs/etc/nginx/includes/server_params.conf
deleted file mode 100644
index debdf83a8c0..00000000000
--- a/docker/ha-addon-rootfs/etc/nginx/includes/server_params.conf
+++ /dev/null
@@ -1,8 +0,0 @@
-root /dev/null;
-server_name $hostname;
-
-client_max_body_size 512m;
-
-add_header X-Content-Type-Options nosniff;
-add_header X-XSS-Protection "1; mode=block";
-add_header X-Robots-Tag none;
diff --git a/docker/ha-addon-rootfs/etc/nginx/includes/ssl_params.conf b/docker/ha-addon-rootfs/etc/nginx/includes/ssl_params.conf
deleted file mode 100644
index e6789cbb9bf..00000000000
--- a/docker/ha-addon-rootfs/etc/nginx/includes/ssl_params.conf
+++ /dev/null
@@ -1,8 +0,0 @@
-ssl_protocols TLSv1.2 TLSv1.3;
-ssl_prefer_server_ciphers off;
-ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
-ssl_session_timeout 10m;
-ssl_session_cache shared:SSL:10m;
-ssl_session_tickets off;
-ssl_stapling on;
-ssl_stapling_verify on;
diff --git a/docker/ha-addon-rootfs/etc/nginx/includes/upstream.conf b/docker/ha-addon-rootfs/etc/nginx/includes/upstream.conf
deleted file mode 100644
index 8e782bdc885..00000000000
--- a/docker/ha-addon-rootfs/etc/nginx/includes/upstream.conf
+++ /dev/null
@@ -1,3 +0,0 @@
-upstream esphome {
- server unix:/var/run/esphome.sock;
-}
diff --git a/docker/ha-addon-rootfs/etc/nginx/nginx.conf b/docker/ha-addon-rootfs/etc/nginx/nginx.conf
deleted file mode 100644
index 497427596de..00000000000
--- a/docker/ha-addon-rootfs/etc/nginx/nginx.conf
+++ /dev/null
@@ -1,30 +0,0 @@
-daemon off;
-user root;
-pid /var/run/nginx.pid;
-worker_processes 1;
-error_log /proc/1/fd/1 error;
-events {
- worker_connections 1024;
-}
-
-http {
- include /etc/nginx/includes/mime.types;
-
- access_log off;
- default_type application/octet-stream;
- gzip on;
- keepalive_timeout 65;
- sendfile on;
- server_tokens off;
-
- tcp_nodelay on;
- tcp_nopush on;
-
- map $http_upgrade $connection_upgrade {
- default upgrade;
- '' close;
- }
-
- include /etc/nginx/includes/upstream.conf;
- include /etc/nginx/servers/*.conf;
-}
diff --git a/docker/ha-addon-rootfs/etc/nginx/servers/.gitkeep b/docker/ha-addon-rootfs/etc/nginx/servers/.gitkeep
deleted file mode 100644
index 85ad51be5f2..00000000000
--- a/docker/ha-addon-rootfs/etc/nginx/servers/.gitkeep
+++ /dev/null
@@ -1 +0,0 @@
-Without requirements or design, programming is the art of adding bugs to an empty text file. (Louis Srygley)
diff --git a/docker/ha-addon-rootfs/etc/nginx/templates/direct.gtpl b/docker/ha-addon-rootfs/etc/nginx/templates/direct.gtpl
deleted file mode 100644
index 4fb0ca3f90f..00000000000
--- a/docker/ha-addon-rootfs/etc/nginx/templates/direct.gtpl
+++ /dev/null
@@ -1,28 +0,0 @@
-server {
- {{ if not .ssl }}
- listen 6052 default_server;
- {{ else }}
- listen 6052 default_server ssl http2;
- {{ end }}
-
- include /etc/nginx/includes/server_params.conf;
- include /etc/nginx/includes/proxy_params.conf;
-
- {{ if .ssl }}
- include /etc/nginx/includes/ssl_params.conf;
-
- ssl_certificate /ssl/{{ .certfile }};
- ssl_certificate_key /ssl/{{ .keyfile }};
-
- # Redirect http requests to https on the same port.
- # https://rageagainstshell.com/2016/11/redirect-http-to-https-on-the-same-port-in-nginx/
- error_page 497 https://$http_host$request_uri;
- {{ end }}
-
- # Clear Home Assistant Ingress header
- proxy_set_header X-HA-Ingress "";
-
- location / {
- proxy_pass http://esphome;
- }
-}
diff --git a/docker/ha-addon-rootfs/etc/nginx/templates/ingress.gtpl b/docker/ha-addon-rootfs/etc/nginx/templates/ingress.gtpl
deleted file mode 100644
index 105ddde7105..00000000000
--- a/docker/ha-addon-rootfs/etc/nginx/templates/ingress.gtpl
+++ /dev/null
@@ -1,18 +0,0 @@
-server {
- listen 127.0.0.1:{{ .port }} default_server;
- listen {{ .interface }}:{{ .port }} default_server;
-
- include /etc/nginx/includes/server_params.conf;
- include /etc/nginx/includes/proxy_params.conf;
-
- # Set Home Assistant Ingress header
- proxy_set_header X-HA-Ingress "YES";
-
- location / {
- allow 172.30.32.2;
- allow 127.0.0.1;
- deny all;
-
- proxy_pass http://esphome;
- }
-}
diff --git a/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/discovery/run b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/discovery/run
index 111157d3015..bb36cfcdb4f 100755
--- a/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/discovery/run
+++ b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/discovery/run
@@ -16,7 +16,7 @@ fi
port=$(bashio::addon.ingress_port)
-# Wait for NGINX to become available
+# Wait for the ESPHome Device Builder to become available
bashio::net.wait_for "${port}" "127.0.0.1" 300
config=$(\
diff --git a/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/esphome/finish b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/esphome/finish
index 6e0f8fe23a4..da450c25f99 100755
--- a/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/esphome/finish
+++ b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/esphome/finish
@@ -2,7 +2,7 @@
# shellcheck shell=bash
# ==============================================================================
# Home Assistant Community Add-on: ESPHome
-# Take down the S6 supervision tree when ESPHome dashboard fails
+# Take down the S6 supervision tree when ESPHome Device Builder fails
# ==============================================================================
declare exit_code
readonly exit_code_container=$( /run/s6-linux-init-container-results/exitcode
- fi
- [[ "${exit_code_signal}" -eq 15 ]] && exec /run/s6/basedir/bin/halt
-elif [[ "${exit_code_service}" -ne 0 ]]; then
- if [[ "${exit_code_container}" -eq 0 ]]; then
- echo "${exit_code_service}" > /run/s6-linux-init-container-results/exitcode
- fi
- exec /run/s6/basedir/bin/halt
-fi
diff --git a/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/nginx/run b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/nginx/run
deleted file mode 100755
index bb5f52e10c7..00000000000
--- a/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/nginx/run
+++ /dev/null
@@ -1,23 +0,0 @@
-#!/command/with-contenv bashio
-# shellcheck shell=bash
-# ==============================================================================
-# Community Hass.io Add-ons: ESPHome
-# Runs the NGINX proxy
-# ==============================================================================
-
-# The new device builder handles HA ingress itself, so nginx is bypassed.
-# Block the longrun forever so s6 keeps the dependency satisfied and does
-# not respawn it.
-if bashio::config.true 'use_new_device_builder'; then
- bashio::log.info "NGINX bypassed: new device builder serves ingress directly."
- exec sleep infinity
-fi
-
-bashio::log.info "Waiting for ESPHome dashboard to come up..."
-
-while [[ ! -S /var/run/esphome.sock ]]; do
- sleep 0.5
-done
-
-bashio::log.info "Starting NGINX..."
-exec nginx
diff --git a/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/nginx/type b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/nginx/type
deleted file mode 100644
index 5883cff0cd1..00000000000
--- a/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/nginx/type
+++ /dev/null
@@ -1 +0,0 @@
-longrun
diff --git a/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/init-nginx b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/init-nginx
deleted file mode 100644
index e69de29bb2d..00000000000
diff --git a/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/nginx b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/nginx
deleted file mode 100644
index e69de29bb2d..00000000000
diff --git a/docker/test_configs/bk72xx-arduino.yaml b/docker/test_configs/bk72xx-arduino.yaml
new file mode 100644
index 00000000000..138aa9e282c
--- /dev/null
+++ b/docker/test_configs/bk72xx-arduino.yaml
@@ -0,0 +1,7 @@
+esphome:
+ name: docker-test-bk72xx-arduino
+
+bk72xx:
+ board: generic-bk7231n-qfn32-tuya
+
+logger:
diff --git a/docker/test_configs/esp32-arduino-esp-idf.yaml b/docker/test_configs/esp32-arduino-esp-idf.yaml
new file mode 100644
index 00000000000..fbc68aff0c3
--- /dev/null
+++ b/docker/test_configs/esp32-arduino-esp-idf.yaml
@@ -0,0 +1,10 @@
+esphome:
+ name: docker-test-esp32-ard-idf
+
+esp32:
+ variant: esp32
+ framework:
+ type: arduino
+ toolchain: esp-idf
+
+logger:
diff --git a/docker/test_configs/esp32-arduino-platformio.yaml b/docker/test_configs/esp32-arduino-platformio.yaml
new file mode 100644
index 00000000000..e216c020599
--- /dev/null
+++ b/docker/test_configs/esp32-arduino-platformio.yaml
@@ -0,0 +1,10 @@
+esphome:
+ name: docker-test-esp32-ard-pio
+
+esp32:
+ variant: esp32
+ framework:
+ type: arduino
+ toolchain: platformio
+
+logger:
diff --git a/docker/test_configs/esp32-idf-esp-idf.yaml b/docker/test_configs/esp32-idf-esp-idf.yaml
new file mode 100644
index 00000000000..b180aa9c0a4
--- /dev/null
+++ b/docker/test_configs/esp32-idf-esp-idf.yaml
@@ -0,0 +1,10 @@
+esphome:
+ name: docker-test-esp32-idf-idf
+
+esp32:
+ variant: esp32
+ framework:
+ type: esp-idf
+ toolchain: esp-idf
+
+logger:
diff --git a/docker/test_configs/esp32-idf-platformio.yaml b/docker/test_configs/esp32-idf-platformio.yaml
new file mode 100644
index 00000000000..5aec23e40d2
--- /dev/null
+++ b/docker/test_configs/esp32-idf-platformio.yaml
@@ -0,0 +1,10 @@
+esphome:
+ name: docker-test-esp32-idf-pio
+
+esp32:
+ variant: esp32
+ framework:
+ type: esp-idf
+ toolchain: platformio
+
+logger:
diff --git a/docker/test_configs/esp8266-arduino.yaml b/docker/test_configs/esp8266-arduino.yaml
new file mode 100644
index 00000000000..80b52260e4d
--- /dev/null
+++ b/docker/test_configs/esp8266-arduino.yaml
@@ -0,0 +1,7 @@
+esphome:
+ name: docker-test-esp8266-arduino
+
+esp8266:
+ board: d1_mini
+
+logger:
diff --git a/docker/test_configs/host.yaml b/docker/test_configs/host.yaml
new file mode 100644
index 00000000000..9f990693049
--- /dev/null
+++ b/docker/test_configs/host.yaml
@@ -0,0 +1,6 @@
+esphome:
+ name: docker-test-host
+
+host:
+
+logger:
diff --git a/docker/test_configs/ln882x-arduino.yaml b/docker/test_configs/ln882x-arduino.yaml
new file mode 100644
index 00000000000..4cff3a48837
--- /dev/null
+++ b/docker/test_configs/ln882x-arduino.yaml
@@ -0,0 +1,7 @@
+esphome:
+ name: docker-test-ln882x-arduino
+
+ln882x:
+ board: generic-ln882hki
+
+logger:
diff --git a/docker/test_configs/nrf52.yaml b/docker/test_configs/nrf52.yaml
new file mode 100644
index 00000000000..d6337149cc8
--- /dev/null
+++ b/docker/test_configs/nrf52.yaml
@@ -0,0 +1,8 @@
+esphome:
+ name: docker-test-nrf52
+
+nrf52:
+ board: adafruit_itsybitsy_nrf52840
+ bootloader: adafruit_nrf52_sd140_v6
+
+logger:
diff --git a/docker/test_configs/rp2040-arduino.yaml b/docker/test_configs/rp2040-arduino.yaml
new file mode 100644
index 00000000000..4b5df11d875
--- /dev/null
+++ b/docker/test_configs/rp2040-arduino.yaml
@@ -0,0 +1,7 @@
+esphome:
+ name: docker-test-rp2040-arduino
+
+rp2040:
+ variant: rp2040
+
+logger:
diff --git a/docker/test_configs/rtl87xx-arduino.yaml b/docker/test_configs/rtl87xx-arduino.yaml
new file mode 100644
index 00000000000..e8d9cf75035
--- /dev/null
+++ b/docker/test_configs/rtl87xx-arduino.yaml
@@ -0,0 +1,7 @@
+esphome:
+ name: docker-test-rtl87xx-arduino
+
+rtl87xx:
+ board: generic-rtl8710bn-2mb-788k
+
+logger:
diff --git a/esphome/__main__.py b/esphome/__main__.py
index c29162fd23e..32cb628f447 100644
--- a/esphome/__main__.py
+++ b/esphome/__main__.py
@@ -504,6 +504,12 @@ def has_resolvable_address() -> bool:
if has_ip_address():
return True
+ # The dashboard pre-resolves the device and passes the IPs via
+ # --mdns-address-cache/--dns-address-cache; honor a cached address even when the
+ # device has mDNS disabled (e.g. a .local host found via ping).
+ if CORE.address_cache and CORE.address_cache.get_addresses(CORE.address):
+ return True
+
if has_mdns():
return True
@@ -696,6 +702,11 @@ def _wrap_to_code(name, comp, yaml_util):
def write_cpp(config: ConfigType) -> int:
from esphome import writer
+ # Refresh the storage sidecar and clean an incompatible previous build
+ # before regenerating any sources. This may full-wipe the build dir, so it
+ # has to run before write_cpp_file writes src/.
+ writer.update_storage_json()
+
if not get_bool_env(ENV_NOGITIGNORE):
writer.write_gitignore()
@@ -761,6 +772,7 @@ def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
toolchain.create_factory_bin()
toolchain.create_ota_bin()
toolchain.create_elf_copy()
+ toolchain.get_idedata()
else:
from esphome.platformio import toolchain
@@ -1351,6 +1363,19 @@ def _validate_bootloader_binary(binary: Path) -> None:
)
+def _should_subscribe_states(args: ArgsProtocol) -> bool:
+ """Determine whether entity state changes should be shown in log output.
+
+ The ``--states``/``--no-states`` command line flags take precedence. When
+ neither is given, the ``ESPHOME_LOG_STATES`` environment variable controls
+ the behavior, defaulting to showing states.
+ """
+ states = getattr(args, "states", None)
+ if states is not None:
+ return states
+ return get_bool_env("ESPHOME_LOG_STATES", True)
+
+
def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int | None:
try:
module = importlib.import_module("esphome.components." + CORE.target_platform)
@@ -1380,7 +1405,7 @@ def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int
return run_logs(
config,
network_devices,
- subscribe_states=not getattr(args, "no_states", False),
+ subscribe_states=_should_subscribe_states(args),
)
if port_type in (PortType.NETWORK, PortType.MQTT) and has_mqtt_logging():
@@ -1410,7 +1435,16 @@ def command_wizard(args: ArgsProtocol) -> int | None:
def command_config(args: ArgsProtocol, config: ConfigType) -> int | None:
from esphome import yaml_util
- if not CORE.verbose:
+ if getattr(args, "no_defaults", False):
+ user_config = getattr(config, "user_config", None)
+ if user_config is None:
+ _LOGGER.warning(
+ "--no-defaults requested but the user-only config snapshot is "
+ "unavailable; falling back to the validated configuration."
+ )
+ else:
+ config = user_config
+ elif not CORE.verbose:
config = strip_default_ids(config)
output = yaml_util.dump(config, args.show_secrets)
if not args.show_secrets:
@@ -1618,7 +1652,7 @@ def command_clean(args: ArgsProtocol, config: ConfigType) -> int | None:
from esphome import writer
try:
- writer.clean_build()
+ writer.clean_build(full=True)
except OSError as err:
_LOGGER.error("Error deleting build files: %s", err)
return 1
@@ -1831,7 +1865,7 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
ram_report = ram_analyzer.generate_report()
print()
print(ram_report)
- except Exception as e: # pylint: disable=broad-except
+ except Exception as e: # noqa: BLE001 # pylint: disable=broad-except
_LOGGER.warning("RAM strings analysis failed: %s", e)
return 0
@@ -2019,6 +2053,29 @@ SIMPLE_CONFIG_ACTIONS = [
]
+def _add_states_args(parser: argparse.ArgumentParser) -> None:
+ """Add mutually exclusive ``--states``/``--no-states`` flags to a parser.
+
+ When neither flag is given, the ``ESPHOME_LOG_STATES`` environment variable
+ controls whether entity state changes are shown (defaulting to showing them).
+ """
+ states_group = parser.add_mutually_exclusive_group()
+ states_group.add_argument(
+ "--states",
+ dest="states",
+ action="store_true",
+ default=None,
+ help="Show entity state changes in log output (overrides ESPHOME_LOG_STATES).",
+ )
+ states_group.add_argument(
+ "--no-states",
+ dest="states",
+ action="store_false",
+ default=None,
+ help="Do not show entity state changes in log output.",
+ )
+
+
def parse_args(argv):
options_parser = argparse.ArgumentParser(add_help=False)
options_parser.add_argument(
@@ -2111,6 +2168,12 @@ def parse_args(argv):
parser_config.add_argument(
"--show-secrets", help="Show secrets in output.", action="store_true"
)
+ parser_config.add_argument(
+ "--no-defaults",
+ help="Only output the user-supplied configuration without "
+ "schema defaults applied.",
+ action="store_true",
+ )
parser_config_hash = subparsers.add_parser(
"config-hash", help="Calculate the hash of the configuration."
@@ -2195,11 +2258,7 @@ def parse_args(argv):
help="Reset the device before starting serial logs.",
default=os.getenv("ESPHOME_SERIAL_LOGGING_RESET"),
)
- parser_logs.add_argument(
- "--no-states",
- action="store_true",
- help="Do not show entity state changes in log output.",
- )
+ _add_states_args(parser_logs)
parser_discover = subparsers.add_parser(
"discover",
@@ -2231,11 +2290,7 @@ def parse_args(argv):
"--no-logs", help="Disable starting logs.", action="store_true"
)
- parser_run.add_argument(
- "--no-states",
- action="store_true",
- help="Do not show entity state changes in log output.",
- )
+ _add_states_args(parser_run)
parser_run.add_argument(
"--reset",
diff --git a/esphome/async_thread.py b/esphome/async_thread.py
index 7be3c83a9a5..c5225a7a141 100644
--- a/esphome/async_thread.py
+++ b/esphome/async_thread.py
@@ -45,7 +45,7 @@ class AsyncThreadRunner(threading.Thread, Generic[_T]):
async def _runner(self) -> None:
try:
self.result = await self._coro_factory()
- except Exception as exc: # pylint: disable=broad-except
+ except Exception as exc: # noqa: BLE001 # pylint: disable=broad-except
# Capture all exceptions so ``event`` is always set — otherwise a
# crash would hang the waiter forever.
self.exception = exc
diff --git a/esphome/build_gen/espidf.py b/esphome/build_gen/espidf.py
index 0b50f723826..dec6ea04deb 100644
--- a/esphome/build_gen/espidf.py
+++ b/esphome/build_gen/espidf.py
@@ -6,8 +6,19 @@ from pathlib import Path
from esphome.components.esp32 import get_esp32_variant, idf_version
import esphome.config_validation as cv
from esphome.core import CORE
+from esphome.framework_helpers import get_project_compile_flags, get_project_link_flags
from esphome.helpers import mkdir_p, write_file_if_changed
-from esphome.writer import update_storage_json
+
+# Replaces the IDF default C++ standard (-std=gnu++2b appended to
+# CXX_COMPILE_OPTIONS by project.cmake's __build_init) with the one set via
+# cg.set_cpp_standard(). Emitted between include(project.cmake) and project(),
+# i.e. after IDF appends its default and before the options are consumed, and
+# applies project-wide like PlatformIO build_unflags.
+CPP_STANDARD_TEMPLATE = """\
+idf_build_get_property(esphome_cxx_compile_options CXX_COMPILE_OPTIONS)
+list(FILTER esphome_cxx_compile_options EXCLUDE REGEX "^-std=")
+list(APPEND esphome_cxx_compile_options "-std={standard}")
+idf_build_set_property(CXX_COMPILE_OPTIONS "${{esphome_cxx_compile_options}}")"""
def get_available_components() -> list[str] | None:
@@ -74,17 +85,18 @@ def get_project_cmakelists(minimal: bool = False) -> str:
# esphome__micro-mp3) rather than just src/. Required so suppressions
# like ``-Wno-error=maybe-uninitialized`` actually silence warnings in
# third-party components we don't author.
- project_compile_opts = [
- flag
- for flag in sorted(CORE.build_flags)
- if flag.startswith("-D")
- or (flag.startswith("-W") and not flag.startswith("-Wl,"))
- ]
+ project_compile_opts = get_project_compile_flags()
extra_compile_options = "\n".join(
f'idf_build_set_property(COMPILE_OPTIONS "{flag}" APPEND)'
for flag in project_compile_opts
)
+ cpp_standard_options = (
+ CPP_STANDARD_TEMPLATE.format(standard=CORE.cpp_standard)
+ if CORE.cpp_standard
+ else ""
+ )
+
# Per-project list exposed as a CMake variable so converted PIO libs
# can reference ${ESPHOME_PROJECT_MANAGED_COMPONENTS} without baking
# project-specific names into their cached CMakeLists.
@@ -141,6 +153,8 @@ set(EXTRA_COMPONENT_DIRS ${{CMAKE_SOURCE_DIR}}/src)
include($ENV{{IDF_PATH}}/tools/cmake/project.cmake)
+{cpp_standard_options}
+
{extra_compile_options}
{managed_components_property}
@@ -170,8 +184,8 @@ def get_component_cmakelists() -> str:
# Extract linker options (-Wl, flags). Compile flags (-D, -W) are
# emitted project-wide via idf_build_set_property in
# get_project_cmakelists so they reach every component, not just src/.
- link_opts = [flag for flag in CORE.build_flags if flag.startswith("-Wl,")]
- link_opts_str = "\n ".join(sorted(link_opts)) if link_opts else ""
+ link_opts = get_project_link_flags()
+ link_opts_str = "\n ".join(link_opts) if link_opts else ""
return f"""\
# Auto-generated by ESPHome
@@ -201,9 +215,6 @@ idf_component_register(
REQUIRES ${{ESPHOME_PROJECT_BUILTIN_COMPONENTS}}
)
-# Apply C++ standard
-target_compile_features(${{COMPONENT_LIB}} PUBLIC cxx_std_20)
-
# ESPHome linker options
target_link_options(${{COMPONENT_LIB}} PUBLIC
{link_opts_str}
@@ -213,11 +224,6 @@ target_link_options(${{COMPONENT_LIB}} PUBLIC
def write_project(minimal: bool = False) -> None:
"""Write ESP-IDF project files."""
- # Refresh /storage/.yaml.json so the dashboard's
- # /info and /downloads endpoints can locate the build (they 404
- # otherwise). This mirrors the PlatformIO build-gen path's call
- # in build_gen/platformio.py:write_ini().
- update_storage_json()
mkdir_p(CORE.build_path)
mkdir_p(CORE.relative_src_path())
diff --git a/esphome/build_gen/platformio.py b/esphome/build_gen/platformio.py
index 30dbb69d86f..a583279ea7a 100644
--- a/esphome/build_gen/platformio.py
+++ b/esphome/build_gen/platformio.py
@@ -1,7 +1,7 @@
from esphome.const import __version__
from esphome.core import CORE
from esphome.helpers import mkdir_p, read_file, write_file_if_changed
-from esphome.writer import find_begin_end, update_storage_json
+from esphome.writer import find_begin_end
INI_AUTO_GENERATE_BEGIN = "; ========== AUTO GENERATED CODE BEGIN ==========="
INI_AUTO_GENERATE_END = "; =========== AUTO GENERATED CODE END ============"
@@ -33,12 +33,27 @@ def format_ini(data: dict[str, str | list[str]]) -> str:
return content
+# All -std= variants a platform/framework may set by default, in both the GNU
+# and strict dialects; unflagged so the cg.set_cpp_standard() value is the
+# only standard left in the build.
+CPP_STD_VARIANTS = [
+ f"{prefix}{year}"
+ for year in ("11", "14", "17", "20", "23", "26", "2a", "2b", "2c")
+ for prefix in ("gnu++", "c++")
+]
+
+
def get_ini_content():
CORE.add_platformio_option(
"lib_deps",
[x.as_lib_dep for x in CORE.platformio_libraries.values()]
+ ["${common.lib_deps}"],
)
+ if CORE.cpp_standard:
+ for variant in CPP_STD_VARIANTS:
+ if variant != CORE.cpp_standard:
+ CORE.add_build_unflag(f"-std={variant}")
+ CORE.add_build_flag(f"-std={CORE.cpp_standard}")
# Sort to avoid changing build flags order
CORE.add_platformio_option("build_flags", sorted(CORE.build_flags))
@@ -58,7 +73,6 @@ def get_ini_content():
def write_ini(content):
- update_storage_json()
path = CORE.relative_build_path("platformio.ini")
if path.is_file():
diff --git a/esphome/compiled_config.py b/esphome/compiled_config.py
index 92cbb7348a4..f4fd2052852 100644
--- a/esphome/compiled_config.py
+++ b/esphome/compiled_config.py
@@ -43,7 +43,7 @@ def save_compiled_config(config: ConfigType) -> None:
try:
rendered = yaml_util.dump(config, show_secrets=True)
write_file(compiled_config_path(CORE.config_filename), rendered, private=True)
- except Exception as err: # pylint: disable=broad-except
+ except Exception as err: # noqa: BLE001 # pylint: disable=broad-except
_LOGGER.debug("Skipping compiled config cache write: %s", err)
@@ -62,7 +62,7 @@ def load_compiled_config(conf_path: Path) -> ConfigType | None:
try:
config = yaml_util.load_yaml(cache_path, clear_secrets=False)
- except Exception: # pylint: disable=broad-except
+ except Exception: # noqa: BLE001 # pylint: disable=broad-except
return None
storage = StorageJSON.load(ext_storage_path(conf_path.name))
diff --git a/esphome/components/a01nyub/a01nyub.h b/esphome/components/a01nyub/a01nyub.h
index 5c0d20bd378..69636eb8e4c 100644
--- a/esphome/components/a01nyub/a01nyub.h
+++ b/esphome/components/a01nyub/a01nyub.h
@@ -8,7 +8,7 @@
namespace esphome::a01nyub {
-class A01nyubComponent : public sensor::Sensor, public Component, public uart::UARTDevice {
+class A01nyubComponent final : public sensor::Sensor, public Component, public uart::UARTDevice {
public:
// Nothing really public.
diff --git a/esphome/components/a02yyuw/a02yyuw.h b/esphome/components/a02yyuw/a02yyuw.h
index 693bcfd03c6..2e71651301e 100644
--- a/esphome/components/a02yyuw/a02yyuw.h
+++ b/esphome/components/a02yyuw/a02yyuw.h
@@ -8,7 +8,7 @@
namespace esphome::a02yyuw {
-class A02yyuwComponent : public sensor::Sensor, public Component, public uart::UARTDevice {
+class A02yyuwComponent final : public sensor::Sensor, public Component, public uart::UARTDevice {
public:
// Nothing really public.
diff --git a/esphome/components/a4988/a4988.h b/esphome/components/a4988/a4988.h
index 04040241c0f..f50b5926c1c 100644
--- a/esphome/components/a4988/a4988.h
+++ b/esphome/components/a4988/a4988.h
@@ -6,7 +6,7 @@
namespace esphome::a4988 {
-class A4988 : public stepper::Stepper, public Component {
+class A4988 final : public stepper::Stepper, public Component {
public:
void set_step_pin(GPIOPin *step_pin) { step_pin_ = step_pin; }
void set_dir_pin(GPIOPin *dir_pin) { dir_pin_ = dir_pin; }
diff --git a/esphome/components/absolute_humidity/absolute_humidity.h b/esphome/components/absolute_humidity/absolute_humidity.h
index be28d3dc509..9989bb17fc8 100644
--- a/esphome/components/absolute_humidity/absolute_humidity.h
+++ b/esphome/components/absolute_humidity/absolute_humidity.h
@@ -13,7 +13,7 @@ enum SaturationVaporPressureEquation {
};
/// This class implements calculation of absolute humidity from temperature and relative humidity.
-class AbsoluteHumidityComponent : public sensor::Sensor, public Component {
+class AbsoluteHumidityComponent final : public sensor::Sensor, public Component {
public:
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; }
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
diff --git a/esphome/components/ac_dimmer/ac_dimmer.h b/esphome/components/ac_dimmer/ac_dimmer.h
index 6bfcf0bdb5b..783a9d7e246 100644
--- a/esphome/components/ac_dimmer/ac_dimmer.h
+++ b/esphome/components/ac_dimmer/ac_dimmer.h
@@ -41,7 +41,7 @@ struct AcDimmerDataStore {
#endif
};
-class AcDimmer : public output::FloatOutput, public Component {
+class AcDimmer final : public output::FloatOutput, public Component {
public:
void setup() override;
diff --git a/esphome/components/adc/adc_sensor.h b/esphome/components/adc/adc_sensor.h
index 676940eca12..03de6f8b4b1 100644
--- a/esphome/components/adc/adc_sensor.h
+++ b/esphome/components/adc/adc_sensor.h
@@ -54,7 +54,7 @@ template class Aggregator {
SamplingMode mode_{SamplingMode::AVG};
};
-class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage_sampler::VoltageSampler {
+class ADCSensor final : public sensor::Sensor, public PollingComponent, public voltage_sampler::VoltageSampler {
public:
/// Update the sensor's state by reading the current ADC value.
/// This method is called periodically based on the update interval.
diff --git a/esphome/components/adc128s102/adc128s102.h b/esphome/components/adc128s102/adc128s102.h
index f04ed87b2af..7d6355815e6 100644
--- a/esphome/components/adc128s102/adc128s102.h
+++ b/esphome/components/adc128s102/adc128s102.h
@@ -6,9 +6,9 @@
namespace esphome::adc128s102 {
-class ADC128S102 : public Component,
- public spi::SPIDevice {
+class ADC128S102 final : public Component,
+ public spi::SPIDevice {
public:
ADC128S102() = default;
diff --git a/esphome/components/adc128s102/sensor/adc128s102_sensor.h b/esphome/components/adc128s102/sensor/adc128s102_sensor.h
index c840102380f..3c42e709f27 100644
--- a/esphome/components/adc128s102/sensor/adc128s102_sensor.h
+++ b/esphome/components/adc128s102/sensor/adc128s102_sensor.h
@@ -9,10 +9,10 @@
namespace esphome::adc128s102 {
-class ADC128S102Sensor : public PollingComponent,
- public Parented,
- public sensor::Sensor,
- public voltage_sampler::VoltageSampler {
+class ADC128S102Sensor final : public PollingComponent,
+ public Parented,
+ public sensor::Sensor,
+ public voltage_sampler::VoltageSampler {
public:
ADC128S102Sensor(uint8_t channel);
diff --git a/esphome/components/addressable_light/addressable_light_display.h b/esphome/components/addressable_light/addressable_light_display.h
index 917d334f05f..39d62b87335 100644
--- a/esphome/components/addressable_light/addressable_light_display.h
+++ b/esphome/components/addressable_light/addressable_light_display.h
@@ -9,7 +9,7 @@
namespace esphome::addressable_light {
-class AddressableLightDisplay : public display::DisplayBuffer {
+class AddressableLightDisplay final : public display::DisplayBuffer {
public:
light::AddressableLight *get_light() const { return this->light_; }
diff --git a/esphome/components/ade7880/__init__.py b/esphome/components/ade7880/__init__.py
index aed63c7dfaa..e69de29bb2d 100644
--- a/esphome/components/ade7880/__init__.py
+++ b/esphome/components/ade7880/__init__.py
@@ -1 +0,0 @@
-CODEOWNERS = ["@kpfleming"]
diff --git a/esphome/components/ade7880/ade7880.cpp b/esphome/components/ade7880/ade7880.cpp
index 9d19770c57c..0f4189ad90c 100644
--- a/esphome/components/ade7880/ade7880.cpp
+++ b/esphome/components/ade7880/ade7880.cpp
@@ -87,14 +87,24 @@ void ADE7880::update_sensor_from_s16_register16_(sensor::Sensor *sensor, uint16_
sensor->publish_state(f(val));
}
-template
-void ADE7880::update_sensor_from_s32_register16_(sensor::Sensor *sensor, uint16_t a_register, F &&f) {
- if (sensor == nullptr) {
+void ADE7880::update_active_energy_(PowerChannel *channel, uint16_t a_register) {
+ if (channel->forward_active_energy == nullptr && channel->reverse_active_energy == nullptr) {
return;
}
- float val = this->read_s32_register16_(a_register);
- sensor->publish_state(f(val));
+ // The ADE7880 has no separate forward/reverse active energy accumulators. The xWATTHR registers
+ // accumulate signed energy since the last read (positive = imported/forward, negative = exported/
+ // reverse), so split the value by sign into the forward and reverse running totals.
+ float val = this->read_s32_register16_(a_register) / 14400.0f;
+ if (val >= 0.0f) {
+ if (channel->forward_active_energy != nullptr) {
+ channel->forward_active_energy->publish_state(channel->forward_active_energy_total += val);
+ }
+ } else {
+ if (channel->reverse_active_energy != nullptr) {
+ channel->reverse_active_energy->publish_state(channel->reverse_active_energy_total -= val);
+ }
+ }
}
void ADE7880::update() {
@@ -117,12 +127,7 @@ void ADE7880::update() {
this->update_sensor_from_s24zp_register16_(chan->apparent_power, AVA, [](float val) { return val / 100.0f; });
this->update_sensor_from_s16_register16_(chan->power_factor, APF,
[](float val) { return std::abs(val / -327.68f); });
- this->update_sensor_from_s32_register16_(chan->forward_active_energy, AFWATTHR, [&chan](float val) {
- return chan->forward_active_energy_total += val / 14400.0f;
- });
- this->update_sensor_from_s32_register16_(chan->reverse_active_energy, ARWATTHR, [&chan](float val) {
- return chan->reverse_active_energy_total += val / 14400.0f;
- });
+ this->update_active_energy_(chan, AWATTHR);
}
if (this->channel_b_ != nullptr) {
@@ -133,12 +138,7 @@ void ADE7880::update() {
this->update_sensor_from_s24zp_register16_(chan->apparent_power, BVA, [](float val) { return val / 100.0f; });
this->update_sensor_from_s16_register16_(chan->power_factor, BPF,
[](float val) { return std::abs(val / -327.68f); });
- this->update_sensor_from_s32_register16_(chan->forward_active_energy, BFWATTHR, [&chan](float val) {
- return chan->forward_active_energy_total += val / 14400.0f;
- });
- this->update_sensor_from_s32_register16_(chan->reverse_active_energy, BRWATTHR, [&chan](float val) {
- return chan->reverse_active_energy_total += val / 14400.0f;
- });
+ this->update_active_energy_(chan, BWATTHR);
}
if (this->channel_c_ != nullptr) {
@@ -149,12 +149,7 @@ void ADE7880::update() {
this->update_sensor_from_s24zp_register16_(chan->apparent_power, CVA, [](float val) { return val / 100.0f; });
this->update_sensor_from_s16_register16_(chan->power_factor, CPF,
[](float val) { return std::abs(val / -327.68f); });
- this->update_sensor_from_s32_register16_(chan->forward_active_energy, CFWATTHR, [&chan](float val) {
- return chan->forward_active_energy_total += val / 14400.0f;
- });
- this->update_sensor_from_s32_register16_(chan->reverse_active_energy, CRWATTHR, [&chan](float val) {
- return chan->reverse_active_energy_total += val / 14400.0f;
- });
+ this->update_active_energy_(chan, CWATTHR);
}
ESP_LOGD(TAG, "update took %" PRIu32 " ms", millis() - start);
diff --git a/esphome/components/ade7880/ade7880.h b/esphome/components/ade7880/ade7880.h
index 69c8e5abba2..12be0849ffa 100644
--- a/esphome/components/ade7880/ade7880.h
+++ b/esphome/components/ade7880/ade7880.h
@@ -65,7 +65,7 @@ struct ADE7880Store {
static void gpio_intr(ADE7880Store *arg);
};
-class ADE7880 : public i2c::I2CDevice, public PollingComponent {
+class ADE7880 final : public i2c::I2CDevice, public PollingComponent {
public:
void set_irq0_pin(InternalGPIOPin *pin) { this->irq0_pin_ = pin; }
void set_irq1_pin(InternalGPIOPin *pin) { this->irq1_pin_ = pin; }
@@ -105,7 +105,8 @@ class ADE7880 : public i2c::I2CDevice, public PollingComponent {
// the callable will be passed a 'float' value and is expected to return a 'float'
template void update_sensor_from_s24zp_register16_(sensor::Sensor *sensor, uint16_t a_register, F &&f);
template void update_sensor_from_s16_register16_(sensor::Sensor *sensor, uint16_t a_register, F &&f);
- template void update_sensor_from_s32_register16_(sensor::Sensor *sensor, uint16_t a_register, F &&f);
+
+ void update_active_energy_(PowerChannel *channel, uint16_t a_register);
void reset_device_();
diff --git a/esphome/components/ade7880/ade7880_registers.h b/esphome/components/ade7880/ade7880_registers.h
index aee4e424455..8b0b86fe7a5 100644
--- a/esphome/components/ade7880/ade7880_registers.h
+++ b/esphome/components/ade7880/ade7880_registers.h
@@ -84,9 +84,7 @@ constexpr uint16_t CWATTHR = 0xE402;
constexpr uint16_t AFWATTHR = 0xE403;
constexpr uint16_t BFWATTHR = 0xE404;
constexpr uint16_t CFWATTHR = 0xE405;
-constexpr uint16_t ARWATTHR = 0xE406;
-constexpr uint16_t BRWATTHR = 0xE407;
-constexpr uint16_t CRWATTHR = 0xE408;
+// 0xE406-0xE408 are reserved on the ADE7880 (it does not implement total reactive energy accumulation)
constexpr uint16_t AFVARHR = 0xE409;
constexpr uint16_t BFVARHR = 0xE40A;
constexpr uint16_t CFVARHR = 0xE40B;
diff --git a/esphome/components/ade7953_i2c/ade7953_i2c.h b/esphome/components/ade7953_i2c/ade7953_i2c.h
index 74d7e3e7cce..0b368a73ee9 100644
--- a/esphome/components/ade7953_i2c/ade7953_i2c.h
+++ b/esphome/components/ade7953_i2c/ade7953_i2c.h
@@ -10,7 +10,7 @@
namespace esphome::ade7953_i2c {
-class AdE7953I2c : public ade7953_base::ADE7953, public i2c::I2CDevice {
+class AdE7953I2c final : public ade7953_base::ADE7953, public i2c::I2CDevice {
public:
void dump_config() override;
diff --git a/esphome/components/ads1115/ads1115.h b/esphome/components/ads1115/ads1115.h
index b1eed68aff2..0b7f7ae7005 100644
--- a/esphome/components/ads1115/ads1115.h
+++ b/esphome/components/ads1115/ads1115.h
@@ -43,7 +43,7 @@ enum ADS1115Samplerate {
ADS1115_860SPS = 0b111
};
-class ADS1115Component : public Component, public i2c::I2CDevice {
+class ADS1115Component final : public Component, public i2c::I2CDevice {
public:
void setup() override;
void dump_config() override;
diff --git a/esphome/components/ads1115/sensor/ads1115_sensor.h b/esphome/components/ads1115/sensor/ads1115_sensor.h
index 3b82c153dd5..ecc8fb7af8f 100644
--- a/esphome/components/ads1115/sensor/ads1115_sensor.h
+++ b/esphome/components/ads1115/sensor/ads1115_sensor.h
@@ -11,10 +11,10 @@
namespace esphome::ads1115 {
/// Internal holder class that is in instance of Sensor so that the hub can create individual sensors.
-class ADS1115Sensor : public sensor::Sensor,
- public PollingComponent,
- public voltage_sampler::VoltageSampler,
- public Parented {
+class ADS1115Sensor final : public sensor::Sensor,
+ public PollingComponent,
+ public voltage_sampler::VoltageSampler,
+ public Parented {
public:
void update() override;
void set_multiplexer(ADS1115Multiplexer multiplexer) { this->multiplexer_ = multiplexer; }
diff --git a/esphome/components/ads1118/ads1118.h b/esphome/components/ads1118/ads1118.h
index ef125a0b44b..275933c70d7 100644
--- a/esphome/components/ads1118/ads1118.h
+++ b/esphome/components/ads1118/ads1118.h
@@ -26,9 +26,9 @@ enum ADS1118Gain {
ADS1118_GAIN_0P256 = 0b101,
};
-class ADS1118 : public Component,
- public spi::SPIDevice {
+class ADS1118 final : public Component,
+ public spi::SPIDevice {
public:
ADS1118() = default;
void setup() override;
diff --git a/esphome/components/ads1118/sensor/ads1118_sensor.h b/esphome/components/ads1118/sensor/ads1118_sensor.h
index b929e75c62d..8987dba0732 100644
--- a/esphome/components/ads1118/sensor/ads1118_sensor.h
+++ b/esphome/components/ads1118/sensor/ads1118_sensor.h
@@ -10,10 +10,10 @@
namespace esphome::ads1118 {
-class ADS1118Sensor : public PollingComponent,
- public sensor::Sensor,
- public voltage_sampler::VoltageSampler,
- public Parented {
+class ADS1118Sensor final : public PollingComponent,
+ public sensor::Sensor,
+ public voltage_sampler::VoltageSampler,
+ public Parented {
public:
void update() override;
diff --git a/esphome/components/ags10/ags10.h b/esphome/components/ags10/ags10.h
index 703acd5228c..8ebc8da544a 100644
--- a/esphome/components/ags10/ags10.h
+++ b/esphome/components/ags10/ags10.h
@@ -7,7 +7,7 @@
namespace esphome::ags10 {
-class AGS10Component : public PollingComponent, public i2c::I2CDevice {
+class AGS10Component final : public PollingComponent, public i2c::I2CDevice {
public:
/**
* Sets TVOC sensor.
@@ -100,7 +100,7 @@ class AGS10Component : public PollingComponent, public i2c::I2CDevice {
template optional> read_and_check_(uint8_t a_register);
};
-template class AGS10NewI2cAddressAction : public Action, public Parented {
+template class AGS10NewI2cAddressAction final : public Action, public Parented {
public:
TEMPLATABLE_VALUE(uint8_t, new_address)
@@ -116,7 +116,7 @@ enum AGS10SetZeroPointActionMode {
CUSTOM_VALUE,
};
-template class AGS10SetZeroPointAction : public Action, public Parented {
+template class AGS10SetZeroPointAction final : public Action, public Parented {
public:
TEMPLATABLE_VALUE(uint16_t, value)
TEMPLATABLE_VALUE(AGS10SetZeroPointActionMode, mode)
diff --git a/esphome/components/aht10/aht10.h b/esphome/components/aht10/aht10.h
index 7b9b1761c4d..e99ba6fb98a 100644
--- a/esphome/components/aht10/aht10.h
+++ b/esphome/components/aht10/aht10.h
@@ -10,7 +10,7 @@ namespace esphome::aht10 {
enum AHT10Variant { AHT10, AHT20 };
-class AHT10Component : public PollingComponent, public i2c::I2CDevice {
+class AHT10Component final : public PollingComponent, public i2c::I2CDevice {
public:
void setup() override;
void update() override;
diff --git a/esphome/components/aic3204/aic3204.h b/esphome/components/aic3204/aic3204.h
index 9b8c7928246..ae99a8f4d6a 100644
--- a/esphome/components/aic3204/aic3204.h
+++ b/esphome/components/aic3204/aic3204.h
@@ -61,7 +61,7 @@ static const uint8_t AIC3204_ADC_PTM = 0x3D; // Register 61 - ADC Power Tu
static const uint8_t AIC3204_AN_IN_CHRG = 0x47; // Register 71 - Analog Input Quick Charging Config
static const uint8_t AIC3204_REF_STARTUP = 0x7B; // Register 123 - Reference Power Up Config
-class AIC3204 : public audio_dac::AudioDac, public Component, public i2c::I2CDevice {
+class AIC3204 final : public audio_dac::AudioDac, public Component, public i2c::I2CDevice {
public:
void setup() override;
void dump_config() override;
diff --git a/esphome/components/aic3204/automation.h b/esphome/components/aic3204/automation.h
index 50ae03edbd9..f0f88566145 100644
--- a/esphome/components/aic3204/automation.h
+++ b/esphome/components/aic3204/automation.h
@@ -6,7 +6,7 @@
namespace esphome::aic3204 {
-template class SetAutoMuteAction : public Action {
+template class SetAutoMuteAction final : public Action {
public:
explicit SetAutoMuteAction(AIC3204 *aic3204) : aic3204_(aic3204) {}
diff --git a/esphome/components/airthings_ble/airthings_listener.h b/esphome/components/airthings_ble/airthings_listener.h
index 707e9c3f210..8105ac32eb1 100644
--- a/esphome/components/airthings_ble/airthings_listener.h
+++ b/esphome/components/airthings_ble/airthings_listener.h
@@ -7,7 +7,7 @@
namespace esphome::airthings_ble {
-class AirthingsListener : public esp32_ble_tracker::ESPBTDeviceListener {
+class AirthingsListener final : public esp32_ble_tracker::ESPBTDeviceListener {
public:
bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
};
diff --git a/esphome/components/airthings_wave_base/__init__.py b/esphome/components/airthings_wave_base/__init__.py
index c3f3b8f199f..dee26b524aa 100644
--- a/esphome/components/airthings_wave_base/__init__.py
+++ b/esphome/components/airthings_wave_base/__init__.py
@@ -21,7 +21,7 @@ from esphome.const import (
UNIT_VOLT,
)
-CODEOWNERS = ["@ncareau", "@jeromelaban", "@kpfleming"]
+CODEOWNERS = ["@ncareau", "@jeromelaban"]
DEPENDENCIES = ["ble_client"]
diff --git a/esphome/components/airthings_wave_mini/airthings_wave_mini.h b/esphome/components/airthings_wave_mini/airthings_wave_mini.h
index 910ac902390..c41dde15c9d 100644
--- a/esphome/components/airthings_wave_mini/airthings_wave_mini.h
+++ b/esphome/components/airthings_wave_mini/airthings_wave_mini.h
@@ -12,7 +12,7 @@ static const char *const SERVICE_UUID = "b42e3882-ade7-11e4-89d3-123b93f75cba";
static const char *const CHARACTERISTIC_UUID = "b42e3b98-ade7-11e4-89d3-123b93f75cba";
static const char *const ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID = "b42e3ef4-ade7-11e4-89d3-123b93f75cba";
-class AirthingsWaveMini : public airthings_wave_base::AirthingsWaveBase {
+class AirthingsWaveMini final : public airthings_wave_base::AirthingsWaveBase {
public:
AirthingsWaveMini();
diff --git a/esphome/components/airthings_wave_plus/airthings_wave_plus.h b/esphome/components/airthings_wave_plus/airthings_wave_plus.h
index 6f51f3c65ac..af355e45d63 100644
--- a/esphome/components/airthings_wave_plus/airthings_wave_plus.h
+++ b/esphome/components/airthings_wave_plus/airthings_wave_plus.h
@@ -19,7 +19,7 @@ static const char *const CHARACTERISTIC_UUID_WAVE_RADON_GEN2 = "b42e4dcc-ade7-11
static const char *const ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID_WAVE_RADON_GEN2 =
"b42e50d8-ade7-11e4-89d3-123b93f75cba";
-class AirthingsWavePlus : public airthings_wave_base::AirthingsWaveBase {
+class AirthingsWavePlus final : public airthings_wave_base::AirthingsWaveBase {
public:
void setup() override;
diff --git a/esphome/components/alarm_control_panel/automation.h b/esphome/components/alarm_control_panel/automation.h
index 022d2650d2d..dcb5121c60f 100644
--- a/esphome/components/alarm_control_panel/automation.h
+++ b/esphome/components/alarm_control_panel/automation.h
@@ -27,7 +27,7 @@ static_assert(std::is_trivially_copyable_v);
static_assert(sizeof(StateEnterForwarder) <= sizeof(void *));
static_assert(std::is_trivially_copyable_v>);
-template class ArmAwayAction : public Action {
+template class ArmAwayAction final : public Action {
public:
explicit ArmAwayAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {}
@@ -39,7 +39,7 @@ template class ArmAwayAction : public Action {
AlarmControlPanel *alarm_control_panel_;
};
-template class ArmHomeAction : public Action {
+template class ArmHomeAction final : public Action {
public:
explicit ArmHomeAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {}
@@ -51,7 +51,7 @@ template class ArmHomeAction : public Action {
AlarmControlPanel *alarm_control_panel_;
};
-template class ArmNightAction : public Action {
+template class ArmNightAction final : public Action {
public:
explicit ArmNightAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {}
@@ -63,7 +63,7 @@ template class ArmNightAction : public Action {
AlarmControlPanel *alarm_control_panel_;
};
-template class DisarmAction : public Action {
+template class DisarmAction final : public Action {
public:
explicit DisarmAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {}
@@ -75,7 +75,7 @@ template class DisarmAction : public Action {
AlarmControlPanel *alarm_control_panel_;
};
-template class PendingAction : public Action {
+template class PendingAction final : public Action {
public:
explicit PendingAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {}
@@ -85,7 +85,7 @@ template class PendingAction : public Action {
AlarmControlPanel *alarm_control_panel_;
};
-template class TriggeredAction : public Action {
+template class TriggeredAction final : public Action {
public:
explicit TriggeredAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {}
@@ -95,7 +95,7 @@ template class TriggeredAction : public Action {
AlarmControlPanel *alarm_control_panel_;
};
-template class AlarmControlPanelCondition : public Condition {
+template class AlarmControlPanelCondition final : public Condition {
public:
AlarmControlPanelCondition(AlarmControlPanel *parent) : parent_(parent) {}
bool check(const Ts &...x) override {
diff --git a/esphome/components/alpha3/alpha3.h b/esphome/components/alpha3/alpha3.h
index c63129031ad..5a5b01ac0b3 100644
--- a/esphome/components/alpha3/alpha3.h
+++ b/esphome/components/alpha3/alpha3.h
@@ -31,7 +31,7 @@ static const int16_t GENI_RESPONSE_POWER_OFFSET = 12;
static const int16_t GENI_RESPONSE_MOTOR_POWER_OFFSET = 16; // not sure
static const int16_t GENI_RESPONSE_MOTOR_SPEED_OFFSET = 20;
-class Alpha3 : public esphome::ble_client::BLEClientNode, public PollingComponent {
+class Alpha3 final : public esphome::ble_client::BLEClientNode, public PollingComponent {
public:
void setup() override;
void update() override;
diff --git a/esphome/components/am2315c/am2315c.h b/esphome/components/am2315c/am2315c.h
index 5a959af4c37..73dc0d87587 100644
--- a/esphome/components/am2315c/am2315c.h
+++ b/esphome/components/am2315c/am2315c.h
@@ -27,7 +27,7 @@
namespace esphome::am2315c {
-class AM2315C : public PollingComponent, public i2c::I2CDevice {
+class AM2315C final : public PollingComponent, public i2c::I2CDevice {
public:
void dump_config() override;
void update() override;
diff --git a/esphome/components/am2320/am2320.h b/esphome/components/am2320/am2320.h
index ddb5c6f1653..f92156b1542 100644
--- a/esphome/components/am2320/am2320.h
+++ b/esphome/components/am2320/am2320.h
@@ -6,7 +6,7 @@
namespace esphome::am2320 {
-class AM2320Component : public PollingComponent, public i2c::I2CDevice {
+class AM2320Component final : public PollingComponent, public i2c::I2CDevice {
public:
void setup() override;
void dump_config() override;
diff --git a/esphome/components/am43/cover/am43_cover.h b/esphome/components/am43/cover/am43_cover.h
index aa48aced158..be7af59adeb 100644
--- a/esphome/components/am43/cover/am43_cover.h
+++ b/esphome/components/am43/cover/am43_cover.h
@@ -14,7 +14,7 @@ namespace esphome::am43 {
namespace espbt = esphome::esp32_ble_tracker;
-class Am43Component : public cover::Cover, public esphome::ble_client::BLEClientNode, public Component {
+class Am43Component final : public cover::Cover, public esphome::ble_client::BLEClientNode, public Component {
public:
void setup() override;
void loop() override;
diff --git a/esphome/components/am43/sensor/am43_sensor.h b/esphome/components/am43/sensor/am43_sensor.h
index 9198a5cbcbd..944681bb607 100644
--- a/esphome/components/am43/sensor/am43_sensor.h
+++ b/esphome/components/am43/sensor/am43_sensor.h
@@ -14,7 +14,7 @@ namespace esphome::am43 {
namespace espbt = esphome::esp32_ble_tracker;
-class Am43 : public esphome::ble_client::BLEClientNode, public PollingComponent {
+class Am43 final : public esphome::ble_client::BLEClientNode, public PollingComponent {
public:
void setup() override;
void update() override;
diff --git a/esphome/components/analog_threshold/analog_threshold_binary_sensor.h b/esphome/components/analog_threshold/analog_threshold_binary_sensor.h
index c768f1f82d6..a4df00ff05f 100644
--- a/esphome/components/analog_threshold/analog_threshold_binary_sensor.h
+++ b/esphome/components/analog_threshold/analog_threshold_binary_sensor.h
@@ -7,7 +7,7 @@
namespace esphome::analog_threshold {
-class AnalogThresholdBinarySensor : public Component, public binary_sensor::BinarySensor {
+class AnalogThresholdBinarySensor final : public Component, public binary_sensor::BinarySensor {
public:
void dump_config() override;
void setup() override;
diff --git a/esphome/components/animation/__init__.py b/esphome/components/animation/__init__.py
index e9630f5266b..9c9c7e38711 100644
--- a/esphome/components/animation/__init__.py
+++ b/esphome/components/animation/__init__.py
@@ -2,6 +2,7 @@ import logging
from esphome import automation
import esphome.codegen as cg
+from esphome.components.const import CONF_LOOP
import esphome.components.image as espImage
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_REPEAT
@@ -14,7 +15,6 @@ DEPENDENCIES = ["display"]
MULTI_CONF = True
MULTI_CONF_NO_DEFAULT = True
-CONF_LOOP = "loop"
CONF_START_FRAME = "start_frame"
CONF_END_FRAME = "end_frame"
CONF_FRAME = "frame"
diff --git a/esphome/components/animation/animation.h b/esphome/components/animation/animation.h
index ca800ad9311..64cddbf09c9 100644
--- a/esphome/components/animation/animation.h
+++ b/esphome/components/animation/animation.h
@@ -5,7 +5,7 @@
namespace esphome::animation {
-class Animation : public image::Image {
+class Animation final : public image::Image {
public:
Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, image::ImageType type,
image::Transparency transparent);
@@ -35,7 +35,7 @@ class Animation : public image::Image {
int loop_current_iteration_;
};
-template class AnimationNextFrameAction : public Action {
+template class AnimationNextFrameAction final : public Action {
public:
AnimationNextFrameAction(Animation *parent) : parent_(parent) {}
void play(const Ts &...x) override { this->parent_->next_frame(); }
@@ -44,7 +44,7 @@ template class AnimationNextFrameAction : public Action {
Animation *parent_;
};
-template class AnimationPrevFrameAction : public Action {
+template class AnimationPrevFrameAction final : public Action {
public:
AnimationPrevFrameAction(Animation *parent) : parent_(parent) {}
void play(const Ts &...x) override { this->parent_->prev_frame(); }
@@ -53,7 +53,7 @@ template class AnimationPrevFrameAction : public Action {
Animation *parent_;
};
-template class AnimationSetFrameAction : public Action {
+template class AnimationSetFrameAction final : public Action {
public:
AnimationSetFrameAction(Animation *parent) : parent_(parent) {}
TEMPLATABLE_VALUE(uint16_t, frame)
diff --git a/esphome/components/anova/anova.h b/esphome/components/anova/anova.h
index a3e175be280..49b1100c372 100644
--- a/esphome/components/anova/anova.h
+++ b/esphome/components/anova/anova.h
@@ -17,7 +17,7 @@ namespace espbt = esphome::esp32_ble_tracker;
static const uint16_t ANOVA_SERVICE_UUID = 0xFFE0;
static const uint16_t ANOVA_CHARACTERISTIC_UUID = 0xFFE1;
-class Anova : public climate::Climate, public esphome::ble_client::BLEClientNode, public PollingComponent {
+class Anova final : public climate::Climate, public esphome::ble_client::BLEClientNode, public PollingComponent {
public:
void setup() override;
void loop() override;
diff --git a/esphome/components/apds9306/apds9306.h b/esphome/components/apds9306/apds9306.h
index 093ec55bc63..f971290cdd5 100644
--- a/esphome/components/apds9306/apds9306.h
+++ b/esphome/components/apds9306/apds9306.h
@@ -39,7 +39,7 @@ enum AmbientLightGain : uint8_t {
};
static const uint8_t AMBIENT_LIGHT_GAIN_VALUES[] = {1, 3, 6, 9, 18};
-class APDS9306 : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice {
+class APDS9306 final : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice {
public:
void setup() override;
float get_setup_priority() const override { return setup_priority::BUS; }
diff --git a/esphome/components/apds9960/apds9960.h b/esphome/components/apds9960/apds9960.h
index 2823294207b..bfa64bcc745 100644
--- a/esphome/components/apds9960/apds9960.h
+++ b/esphome/components/apds9960/apds9960.h
@@ -12,7 +12,7 @@
namespace esphome::apds9960 {
-class APDS9960 : public PollingComponent, public i2c::I2CDevice {
+class APDS9960 final : public PollingComponent, public i2c::I2CDevice {
#ifdef USE_SENSOR
SUB_SENSOR(red)
SUB_SENSOR(green)
diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp
index c880e036cbf..2b1458e2aee 100644
--- a/esphome/components/api/api_connection.cpp
+++ b/esphome/components/api/api_connection.cpp
@@ -1306,6 +1306,9 @@ void APIConnection::on_voice_assistant_announce_request(const VoiceAssistantAnno
bool APIConnection::send_voice_assistant_get_configuration_response_(const VoiceAssistantConfigurationRequest &msg) {
VoiceAssistantConfigurationResponse resp;
if (!this->check_voice_assistant_api_connection_()) {
+ // send_message encodes synchronously, so this stack local outlives the encode
+ const std::vector empty_wake_words;
+ resp.active_wake_words = &empty_wake_words;
return this->send_message(resp);
}
diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp
index 031fa342c1b..ddd03ace4ac 100644
--- a/esphome/components/api/api_server.cpp
+++ b/esphome/components/api/api_server.cpp
@@ -186,8 +186,12 @@ void APIServer::remove_client_(uint8_t client_index) {
if (client_index < last_index) {
std::swap(this->clients_[client_index], this->clients_[last_index]);
}
- this->clients_[last_index].reset();
+ // Drop the count before resetting the slot. reset() runs ~APIConnection(), which can reenter the
+ // server (e.g. voice_assistant unsubscribes in its disconnect trigger, publishing entity state ->
+ // on_*_update iterating active_clients()). Excluding the dying slot from the active range first
+ // keeps that reentrant iteration from dereferencing the now-null slot.
this->api_connection_count_--;
+ this->clients_[last_index].reset();
// Last client disconnected - set warning and start tracking for reboot timeout
if (this->api_connection_count_ == 0 && this->reboot_timeout_ != 0) {
diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h
index fbc81150917..16b5762f683 100644
--- a/esphome/components/api/api_server.h
+++ b/esphome/components/api/api_server.h
@@ -342,7 +342,7 @@ class APIServer final : public Component,
extern APIServer *global_api_server; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
-template class APIConnectedCondition : public Condition {
+template class APIConnectedCondition final : public Condition {
TEMPLATABLE_VALUE(bool, state_subscription_only)
public:
bool check(const Ts &...x) override {
diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h
index aef046fbb04..9e0faf98819 100644
--- a/esphome/components/api/homeassistant_service.h
+++ b/esphome/components/api/homeassistant_service.h
@@ -104,7 +104,7 @@ class ActionResponse {
template using ActionResponseCallback = std::function;
#endif
-template class HomeAssistantServiceCallAction : public Action {
+template class HomeAssistantServiceCallAction final : public Action {
public:
explicit HomeAssistantServiceCallAction(APIServer *parent, bool is_event) : parent_(parent) {
this->flags_.is_event = is_event;
diff --git a/esphome/components/api/user_services.h b/esphome/components/api/user_services.h
index 29eadda927a..ea57d0944bc 100644
--- a/esphome/components/api/user_services.h
+++ b/esphome/components/api/user_services.h
@@ -164,7 +164,8 @@ template class UserServiceTrig
// Specialization for NONE - no extra trigger arguments
template
-class UserServiceTrigger : public UserServiceBase, public Trigger {
+class UserServiceTrigger final : public UserServiceBase,
+ public Trigger {
public:
UserServiceTrigger(const char *name, const std::array &arg_names)
: UserServiceBase(name, arg_names, enums::SUPPORTS_RESPONSE_NONE) {}
@@ -175,8 +176,8 @@ class UserServiceTrigger : public UserServ
// Specialization for OPTIONAL - call_id and return_response trigger arguments
template
-class UserServiceTrigger : public UserServiceBase,
- public Trigger {
+class UserServiceTrigger final : public UserServiceBase,
+ public Trigger {
public:
UserServiceTrigger(const char *name, const std::array &arg_names)
: UserServiceBase(name, arg_names, enums::SUPPORTS_RESPONSE_OPTIONAL) {}
@@ -189,8 +190,8 @@ class UserServiceTrigger : public User
// Specialization for ONLY - just call_id trigger argument
template
-class UserServiceTrigger : public UserServiceBase,
- public Trigger {
+class UserServiceTrigger final : public UserServiceBase,
+ public Trigger {
public:
UserServiceTrigger(const char *name, const std::array &arg_names)
: UserServiceBase(name, arg_names, enums::SUPPORTS_RESPONSE_ONLY) {}
@@ -201,8 +202,8 @@ class UserServiceTrigger : public UserServ
// Specialization for STATUS - just call_id trigger argument (reports success/error without data)
template
-class UserServiceTrigger : public UserServiceBase,
- public Trigger {
+class UserServiceTrigger final : public UserServiceBase,
+ public Trigger {
public:
UserServiceTrigger(const char *name, const std::array &arg_names)
: UserServiceBase(name, arg_names, enums::SUPPORTS_RESPONSE_STATUS) {}
@@ -221,7 +222,7 @@ class UserServiceTrigger : public UserSe
namespace esphome::api {
-template class APIRespondAction : public Action {
+template class APIRespondAction final : public Action {
public:
explicit APIRespondAction(APIServer *parent) : parent_(parent) {}
@@ -286,7 +287,7 @@ template class APIRespondAction : public Action {
// Action to unregister a service call after execution completes
// Automatically appended to the end of action lists for non-none response modes
-template class APIUnregisterServiceCallAction : public Action {
+template class APIUnregisterServiceCallAction final : public Action {
public:
explicit APIUnregisterServiceCallAction(APIServer *parent) : parent_(parent) {}
diff --git a/esphome/components/aqi/aqi_sensor.h b/esphome/components/aqi/aqi_sensor.h
index 2e526ca8252..aa64fa5a4dc 100644
--- a/esphome/components/aqi/aqi_sensor.h
+++ b/esphome/components/aqi/aqi_sensor.h
@@ -6,7 +6,7 @@
namespace esphome::aqi {
-class AQISensor : public sensor::Sensor, public Component {
+class AQISensor final : public sensor::Sensor, public Component {
public:
void setup() override;
void dump_config() override;
diff --git a/esphome/components/as3935_i2c/as3935_i2c.h b/esphome/components/as3935_i2c/as3935_i2c.h
index c43ec4afd5b..c15f2d6e3e4 100644
--- a/esphome/components/as3935_i2c/as3935_i2c.h
+++ b/esphome/components/as3935_i2c/as3935_i2c.h
@@ -5,7 +5,7 @@
namespace esphome::as3935_i2c {
-class I2CAS3935Component : public as3935::AS3935Component, public i2c::I2CDevice {
+class I2CAS3935Component final : public as3935::AS3935Component, public i2c::I2CDevice {
public:
void dump_config() override;
diff --git a/esphome/components/as3935_spi/as3935_spi.h b/esphome/components/as3935_spi/as3935_spi.h
index 935707a18c0..053e34b3d0e 100644
--- a/esphome/components/as3935_spi/as3935_spi.h
+++ b/esphome/components/as3935_spi/as3935_spi.h
@@ -8,9 +8,9 @@ namespace esphome::as3935_spi {
enum AS3935RegisterMasks { SPI_READ_M = 0x40 };
-class SPIAS3935Component : public as3935::AS3935Component,
- public spi::SPIDevice {
+class SPIAS3935Component final : public as3935::AS3935Component,
+ public spi::SPIDevice {
public:
void setup() override;
void dump_config() override;
diff --git a/esphome/components/as5600/as5600.h b/esphome/components/as5600/as5600.h
index 414633f978b..a385322b70a 100644
--- a/esphome/components/as5600/as5600.h
+++ b/esphome/components/as5600/as5600.h
@@ -43,7 +43,7 @@ enum AS5600MagnetStatus : uint8_t {
MAGNET_WEAK = 6, // 0b110 / magnet too weak
};
-class AS5600Component : public Component, public i2c::I2CDevice {
+class AS5600Component final : public Component, public i2c::I2CDevice {
public:
/// Set up the internal sensor array.
void setup() override;
diff --git a/esphome/components/as5600/sensor/as5600_sensor.h b/esphome/components/as5600/sensor/as5600_sensor.h
index 0086fe54ccd..170ff6d86b8 100644
--- a/esphome/components/as5600/sensor/as5600_sensor.h
+++ b/esphome/components/as5600/sensor/as5600_sensor.h
@@ -9,7 +9,7 @@
namespace esphome::as5600 {
-class AS5600Sensor : public PollingComponent, public Parented, public sensor::Sensor {
+class AS5600Sensor final : public PollingComponent, public Parented, public sensor::Sensor {
public:
void update() override;
void dump_config() override;
diff --git a/esphome/components/as7341/as7341.h b/esphome/components/as7341/as7341.h
index 8bc157fe79a..2d72987f1cd 100644
--- a/esphome/components/as7341/as7341.h
+++ b/esphome/components/as7341/as7341.h
@@ -73,7 +73,7 @@ enum AS7341Gain {
AS7341_GAIN_512X,
};
-class AS7341Component : public PollingComponent, public i2c::I2CDevice {
+class AS7341Component final : public PollingComponent, public i2c::I2CDevice {
public:
void setup() override;
void dump_config() override;
diff --git a/esphome/components/at581x/at581x.h b/esphome/components/at581x/at581x.h
index e7f8ee36923..594395e96d8 100644
--- a/esphome/components/at581x/at581x.h
+++ b/esphome/components/at581x/at581x.h
@@ -12,7 +12,7 @@
namespace esphome::at581x {
-class AT581XComponent : public Component, public i2c::I2CDevice {
+class AT581XComponent final : public Component, public i2c::I2CDevice {
public:
#ifdef USE_SWITCH
void set_rf_power_switch(switch_::Switch *s) {
diff --git a/esphome/components/at581x/automation.h b/esphome/components/at581x/automation.h
index eb8b1b25628..a732d2bcc79 100644
--- a/esphome/components/at581x/automation.h
+++ b/esphome/components/at581x/automation.h
@@ -7,12 +7,12 @@
namespace esphome::at581x {
-template class AT581XResetAction : public Action, public Parented {
+template class AT581XResetAction final : public Action, public Parented {
public:
void play(const Ts &...x) { this->parent_->reset_hardware_frontend(); }
};
-template class AT581XSettingsAction : public Action, public Parented {
+template class AT581XSettingsAction final : public Action, public Parented {
public:
TEMPLATABLE_VALUE(int8_t, hw_frontend_reset)
TEMPLATABLE_VALUE(int, frequency)
diff --git a/esphome/components/at581x/switch/rf_switch.h b/esphome/components/at581x/switch/rf_switch.h
index 47367fad45f..0e251b8baaf 100644
--- a/esphome/components/at581x/switch/rf_switch.h
+++ b/esphome/components/at581x/switch/rf_switch.h
@@ -5,7 +5,7 @@
namespace esphome::at581x {
-class RFSwitch : public switch_::Switch, public Parented {
+class RFSwitch final : public switch_::Switch, public Parented {
protected:
void write_state(bool state) override;
};
diff --git a/esphome/components/atc_mithermometer/atc_mithermometer.h b/esphome/components/atc_mithermometer/atc_mithermometer.h
index 8f62f05bc13..3dde5f18680 100644
--- a/esphome/components/atc_mithermometer/atc_mithermometer.h
+++ b/esphome/components/atc_mithermometer/atc_mithermometer.h
@@ -18,7 +18,7 @@ struct ParseResult {
int raw_offset;
};
-class ATCMiThermometer : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
+class ATCMiThermometer final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
public:
void set_address(uint64_t address) { address_ = address; };
diff --git a/esphome/components/atm90e26/atm90e26.h b/esphome/components/atm90e26/atm90e26.h
index 657f8f3c433..0381d8e5c16 100644
--- a/esphome/components/atm90e26/atm90e26.h
+++ b/esphome/components/atm90e26/atm90e26.h
@@ -6,9 +6,9 @@
namespace esphome::atm90e26 {
-class ATM90E26Component : public PollingComponent,
- public spi::SPIDevice {
+class ATM90E26Component final : public PollingComponent,
+ public spi::SPIDevice {
public:
void setup() override;
void dump_config() override;
diff --git a/esphome/components/atm90e32/atm90e32.h b/esphome/components/atm90e32/atm90e32.h
index 5fa224b3535..c636e5065a5 100644
--- a/esphome/components/atm90e32/atm90e32.h
+++ b/esphome/components/atm90e32/atm90e32.h
@@ -13,9 +13,9 @@
namespace esphome::atm90e32 {
-class ATM90E32Component : public PollingComponent,
- public spi::SPIDevice {
+class ATM90E32Component final : public PollingComponent,
+ public spi::SPIDevice {
public:
static const uint8_t PHASEA = 0;
static const uint8_t PHASEB = 1;
diff --git a/esphome/components/atm90e32/button/atm90e32_button.h b/esphome/components/atm90e32/button/atm90e32_button.h
index 0cfce622934..988c6d5c167 100644
--- a/esphome/components/atm90e32/button/atm90e32_button.h
+++ b/esphome/components/atm90e32/button/atm90e32_button.h
@@ -6,7 +6,7 @@
namespace esphome::atm90e32 {
-class ATM90E32GainCalibrationButton : public button::Button, public Parented {
+class ATM90E32GainCalibrationButton final : public button::Button, public Parented {
public:
ATM90E32GainCalibrationButton() = default;
@@ -14,7 +14,7 @@ class ATM90E32GainCalibrationButton : public button::Button, public Parented {
+class ATM90E32ClearGainCalibrationButton final : public button::Button, public Parented {
public:
ATM90E32ClearGainCalibrationButton() = default;
@@ -22,7 +22,7 @@ class ATM90E32ClearGainCalibrationButton : public button::Button, public Parente
void press_action() override;
};
-class ATM90E32OffsetCalibrationButton : public button::Button, public Parented {
+class ATM90E32OffsetCalibrationButton final : public button::Button, public Parented {
public:
ATM90E32OffsetCalibrationButton() = default;
@@ -30,7 +30,7 @@ class ATM90E32OffsetCalibrationButton : public button::Button, public Parented {
+class ATM90E32ClearOffsetCalibrationButton final : public button::Button, public Parented {
public:
ATM90E32ClearOffsetCalibrationButton() = default;
@@ -38,7 +38,7 @@ class ATM90E32ClearOffsetCalibrationButton : public button::Button, public Paren
void press_action() override;
};
-class ATM90E32PowerOffsetCalibrationButton : public button::Button, public Parented {
+class ATM90E32PowerOffsetCalibrationButton final : public button::Button, public Parented {
public:
ATM90E32PowerOffsetCalibrationButton() = default;
@@ -46,7 +46,7 @@ class ATM90E32PowerOffsetCalibrationButton : public button::Button, public Paren
void press_action() override;
};
-class ATM90E32ClearPowerOffsetCalibrationButton : public button::Button, public Parented {
+class ATM90E32ClearPowerOffsetCalibrationButton final : public button::Button, public Parented {
public:
ATM90E32ClearPowerOffsetCalibrationButton() = default;
diff --git a/esphome/components/audio/__init__.py b/esphome/components/audio/__init__.py
index c9775ab6012..091f496e333 100644
--- a/esphome/components/audio/__init__.py
+++ b/esphome/components/audio/__init__.py
@@ -335,7 +335,7 @@ async def to_code(config):
add_idf_component(
name="esphome/esp-audio-libs",
- ref="3.1.0",
+ ref="3.2.1",
)
data = _get_data()
@@ -395,11 +395,11 @@ async def to_code(config):
)
if data.mp3_support:
cg.add_define("USE_AUDIO_MP3_SUPPORT")
- add_idf_component(name="esphome/micro-mp3", ref="0.2.1")
+ add_idf_component(name="esphome/micro-mp3", ref="0.3.0")
_emit_memory_pair(
data.mp3.buffer_memory,
- "CONFIG_MP3_DECODER_PREFER_PSRAM",
- "CONFIG_MP3_DECODER_PREFER_INTERNAL",
+ "CONFIG_MICRO_MP3_PREFER_PSRAM",
+ "CONFIG_MICRO_MP3_PREFER_INTERNAL",
)
if data.opus_support:
cg.add_define("USE_AUDIO_OPUS_SUPPORT")
diff --git a/esphome/components/audio/audio.h b/esphome/components/audio/audio.h
index 62c57b18cf7..36780a3055b 100644
--- a/esphome/components/audio/audio.h
+++ b/esphome/components/audio/audio.h
@@ -1,6 +1,7 @@
#pragma once
#include "esphome/core/defines.h"
+#include "esphome/core/helpers.h" // for ESPDEPRECATED
#include
#include
@@ -143,6 +144,8 @@ AudioFileType detect_audio_file_type(const char *content_type, const char *url);
/// @param output_buffer Buffer to store the scaled samples
/// @param scale_factor Q15 fixed point scaling factor
/// @param samples_to_scale Number of samples to scale
+// Remove before 2026.12.0
+ESPDEPRECATED("Use esp_audio_libs::gain::apply() (from ) instead. Removed in 2026.12.0.", "2026.6.0")
void scale_audio_samples(const int16_t *audio_samples, int16_t *output_buffer, int16_t scale_factor,
size_t samples_to_scale);
diff --git a/esphome/components/audio_adc/automation.h b/esphome/components/audio_adc/automation.h
index e74e0232036..fc7af256228 100644
--- a/esphome/components/audio_adc/automation.h
+++ b/esphome/components/audio_adc/automation.h
@@ -6,7 +6,7 @@
namespace esphome::audio_adc {
-template class SetMicGainAction : public Action {
+template class SetMicGainAction final : public Action {
public:
explicit SetMicGainAction(AudioAdc *audio_adc) : audio_adc_(audio_adc) {}
diff --git a/esphome/components/audio_dac/automation.h b/esphome/components/audio_dac/automation.h
index 67bbc78ac21..9c5348271c2 100644
--- a/esphome/components/audio_dac/automation.h
+++ b/esphome/components/audio_dac/automation.h
@@ -6,7 +6,7 @@
namespace esphome::audio_dac {
-template class MuteOffAction : public Action {
+template class MuteOffAction final : public Action {
public:
explicit MuteOffAction(AudioDac *audio_dac) : audio_dac_(audio_dac) {}
@@ -16,7 +16,7 @@ template class MuteOffAction : public Action {
AudioDac *audio_dac_;
};
-template class MuteOnAction : public Action {
+template class MuteOnAction final : public Action {
public:
explicit MuteOnAction(AudioDac *audio_dac) : audio_dac_(audio_dac) {}
@@ -26,7 +26,7 @@ template class MuteOnAction : public Action {
AudioDac *audio_dac_;
};
-template class SetVolumeAction : public Action {
+template class SetVolumeAction final : public Action {
public:
explicit SetVolumeAction(AudioDac *audio_dac) : audio_dac_(audio_dac) {}
diff --git a/esphome/components/audio_file/media_source/audio_file_media_source.h b/esphome/components/audio_file/media_source/audio_file_media_source.h
index 2c6189f2727..d269f77c357 100644
--- a/esphome/components/audio_file/media_source/audio_file_media_source.h
+++ b/esphome/components/audio_file/media_source/audio_file_media_source.h
@@ -23,7 +23,9 @@ namespace esphome::audio_file {
// (the orchestrator calls set_listener() on us with a MediaSourceListener*).
// - micro_decoder::DecoderListener: the underlying decoder calls back *into* us with decoded
// audio and state changes (we call decoder_->set_listener(this) in setup()).
-class AudioFileMediaSource : public Component, public media_source::MediaSource, public micro_decoder::DecoderListener {
+class AudioFileMediaSource final : public Component,
+ public media_source::MediaSource,
+ public micro_decoder::DecoderListener {
public:
void setup() override;
void loop() override;
diff --git a/esphome/components/audio_http/audio_http_media_source.h b/esphome/components/audio_http/audio_http_media_source.h
index e4bd69e9e6f..f794aa1f027 100644
--- a/esphome/components/audio_http/audio_http_media_source.h
+++ b/esphome/components/audio_http/audio_http_media_source.h
@@ -23,7 +23,9 @@ namespace esphome::audio_http {
// - micro_decoder::DecoderListener: the underlying decoder calls back *into* us with decoded
// audio and state changes (we call decoder_->set_listener(this) in setup()).
// The two set_listener() methods live on different base classes and serve opposite directions.
-class AudioHTTPMediaSource : public Component, public media_source::MediaSource, public micro_decoder::DecoderListener {
+class AudioHTTPMediaSource final : public Component,
+ public media_source::MediaSource,
+ public micro_decoder::DecoderListener {
public:
void setup() override;
void loop() override;
diff --git a/esphome/components/axs15231/touchscreen/axs15231_touchscreen.h b/esphome/components/axs15231/touchscreen/axs15231_touchscreen.h
index 94d232777c6..43bd3799256 100644
--- a/esphome/components/axs15231/touchscreen/axs15231_touchscreen.h
+++ b/esphome/components/axs15231/touchscreen/axs15231_touchscreen.h
@@ -7,7 +7,7 @@
namespace esphome::axs15231 {
-class AXS15231Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice {
+class AXS15231Touchscreen final : public touchscreen::Touchscreen, public i2c::I2CDevice {
public:
void setup() override;
void dump_config() override;
diff --git a/esphome/components/ballu/ballu.h b/esphome/components/ballu/ballu.h
index 8a45d39c703..cb40f415ad2 100644
--- a/esphome/components/ballu/ballu.h
+++ b/esphome/components/ballu/ballu.h
@@ -10,7 +10,7 @@ namespace esphome::ballu {
const float YKR_K_002E_TEMP_MIN = 16.0;
const float YKR_K_002E_TEMP_MAX = 32.0;
-class BalluClimate : public climate_ir::ClimateIR {
+class BalluClimate final : public climate_ir::ClimateIR {
public:
BalluClimate()
: climate_ir::ClimateIR(YKR_K_002E_TEMP_MIN, YKR_K_002E_TEMP_MAX, 1.0f, true, true,
diff --git a/esphome/components/bang_bang/bang_bang_climate.h b/esphome/components/bang_bang/bang_bang_climate.h
index 1e5ff84883f..d83257f9f34 100644
--- a/esphome/components/bang_bang/bang_bang_climate.h
+++ b/esphome/components/bang_bang/bang_bang_climate.h
@@ -16,7 +16,7 @@ struct BangBangClimateTargetTempConfig {
float default_temperature_high{NAN};
};
-class BangBangClimate : public climate::Climate, public Component {
+class BangBangClimate final : public climate::Climate, public Component {
public:
BangBangClimate();
void setup() override;
diff --git a/esphome/components/bedjet/bedjet_hub.h b/esphome/components/bedjet/bedjet_hub.h
index 9f25f7a4660..32ddd94cff7 100644
--- a/esphome/components/bedjet/bedjet_hub.h
+++ b/esphome/components/bedjet/bedjet_hub.h
@@ -33,7 +33,7 @@ static const espbt::ESPBTUUID BEDJET_NAME_UUID = espbt::ESPBTUUID::from_raw("000
/**
* Hub component connecting to the BedJet device over Bluetooth.
*/
-class BedJetHub : public esphome::ble_client::BLEClientNode, public PollingComponent {
+class BedJetHub final : public esphome::ble_client::BLEClientNode, public PollingComponent {
public:
/* BedJet functionality exposed to `BedJetClient` children and/or accessible from action lambdas. */
diff --git a/esphome/components/bedjet/climate/bedjet_climate.h b/esphome/components/bedjet/climate/bedjet_climate.h
index f59e67eeb7b..6f81b872898 100644
--- a/esphome/components/bedjet/climate/bedjet_climate.h
+++ b/esphome/components/bedjet/climate/bedjet_climate.h
@@ -12,7 +12,7 @@
namespace esphome::bedjet {
-class BedJetClimate : public climate::Climate, public BedJetClient, public PollingComponent {
+class BedJetClimate final : public climate::Climate, public BedJetClient, public PollingComponent {
public:
void setup() override;
void loop() override;
diff --git a/esphome/components/bedjet/fan/bedjet_fan.h b/esphome/components/bedjet/fan/bedjet_fan.h
index 03f42f1438a..814a87d8b9c 100644
--- a/esphome/components/bedjet/fan/bedjet_fan.h
+++ b/esphome/components/bedjet/fan/bedjet_fan.h
@@ -12,7 +12,7 @@
namespace esphome::bedjet {
-class BedJetFan : public fan::Fan, public BedJetClient, public PollingComponent {
+class BedJetFan final : public fan::Fan, public BedJetClient, public PollingComponent {
public:
void update() override;
void dump_config() override;
diff --git a/esphome/components/bedjet/sensor/bedjet_sensor.h b/esphome/components/bedjet/sensor/bedjet_sensor.h
index 0c3f713579d..c387e9d5fd3 100644
--- a/esphome/components/bedjet/sensor/bedjet_sensor.h
+++ b/esphome/components/bedjet/sensor/bedjet_sensor.h
@@ -7,7 +7,7 @@
namespace esphome::bedjet {
-class BedjetSensor : public BedJetClient, public Component {
+class BedjetSensor final : public BedJetClient, public Component {
public:
void dump_config() override;
diff --git a/esphome/components/beken_spi_led_strip/led_strip.h b/esphome/components/beken_spi_led_strip/led_strip.h
index 4ed640a3bc4..909634e266e 100644
--- a/esphome/components/beken_spi_led_strip/led_strip.h
+++ b/esphome/components/beken_spi_led_strip/led_strip.h
@@ -19,7 +19,7 @@ enum RGBOrder : uint8_t {
ORDER_BRG,
};
-class BekenSPILEDStripLightOutput : public light::AddressableLight {
+class BekenSPILEDStripLightOutput final : public light::AddressableLight {
public:
void setup() override;
void write_state(light::LightState *state) override;
diff --git a/esphome/components/bh1750/bh1750.h b/esphome/components/bh1750/bh1750.h
index 39dbd1d6a99..092a21359bc 100644
--- a/esphome/components/bh1750/bh1750.h
+++ b/esphome/components/bh1750/bh1750.h
@@ -13,7 +13,7 @@ enum BH1750Mode : uint8_t {
};
/// This class implements support for the i2c-based BH1750 ambient light sensor.
-class BH1750Sensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice {
+class BH1750Sensor final : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice {
public:
// ========== INTERNAL METHODS ==========
// (In most use cases you won't need these)
diff --git a/esphome/components/bh1900nux/bh1900nux.h b/esphome/components/bh1900nux/bh1900nux.h
index 61d1bac268e..f1d62d16472 100644
--- a/esphome/components/bh1900nux/bh1900nux.h
+++ b/esphome/components/bh1900nux/bh1900nux.h
@@ -6,7 +6,7 @@
namespace esphome::bh1900nux {
-class BH1900NUXSensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice {
+class BH1900NUXSensor final : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice {
public:
void setup() override;
void update() override;
diff --git a/esphome/components/binary/fan/binary_fan.h b/esphome/components/binary/fan/binary_fan.h
index 17157dd29ca..601f4cb641a 100644
--- a/esphome/components/binary/fan/binary_fan.h
+++ b/esphome/components/binary/fan/binary_fan.h
@@ -6,7 +6,7 @@
namespace esphome::binary {
-class BinaryFan : public Component, public fan::Fan {
+class BinaryFan final : public Component, public fan::Fan {
public:
void setup() override;
void dump_config() override;
diff --git a/esphome/components/binary/light/binary_light_output.h b/esphome/components/binary/light/binary_light_output.h
index f6be7e162e9..32707e8b0c8 100644
--- a/esphome/components/binary/light/binary_light_output.h
+++ b/esphome/components/binary/light/binary_light_output.h
@@ -6,7 +6,7 @@
namespace esphome::binary {
-class BinaryLightOutput : public light::LightOutput {
+class BinaryLightOutput final : public light::LightOutput {
public:
void set_output(output::BinaryOutput *output) { output_ = output; }
light::LightTraits get_traits() override {
diff --git a/esphome/components/binary_sensor/automation.h b/esphome/components/binary_sensor/automation.h
index 1875910affd..d5a85ca9c42 100644
--- a/esphome/components/binary_sensor/automation.h
+++ b/esphome/components/binary_sensor/automation.h
@@ -18,7 +18,7 @@ struct MultiClickTriggerEvent {
uint32_t max_length;
};
-class PressTrigger : public Trigger<> {
+class PressTrigger final : public Trigger<> {
public:
explicit PressTrigger(BinarySensor *parent) {
parent->add_on_state_callback([this](bool state) {
@@ -28,7 +28,7 @@ class PressTrigger : public Trigger<> {
}
};
-class ReleaseTrigger : public Trigger<> {
+class ReleaseTrigger final : public Trigger<> {
public:
explicit ReleaseTrigger(BinarySensor *parent) {
parent->add_on_state_callback([this](bool state) {
@@ -40,7 +40,7 @@ class ReleaseTrigger : public Trigger<> {
bool match_interval(uint32_t min_length, uint32_t max_length, uint32_t length);
-class ClickTrigger : public Trigger<> {
+class ClickTrigger final : public Trigger<> {
public:
explicit ClickTrigger(BinarySensor *parent, uint32_t min_length, uint32_t max_length)
: min_length_(min_length), max_length_(max_length) {
@@ -61,7 +61,7 @@ class ClickTrigger : public Trigger<> {
uint32_t max_length_; /// Maximum length of click. 0 means no maximum.
};
-class DoubleClickTrigger : public Trigger<> {
+class DoubleClickTrigger final : public Trigger<> {
public:
explicit DoubleClickTrigger(BinarySensor *parent, uint32_t min_length, uint32_t max_length)
: min_length_(min_length), max_length_(max_length) {
@@ -127,7 +127,7 @@ class MultiClickTriggerBase : public Trigger<>, public Component {
/// Template wrapper that provides inline std::array storage for timing events.
/// N is set by code generation to match the exact number of timing events configured in YAML.
-template class MultiClickTrigger : public MultiClickTriggerBase {
+template class MultiClickTrigger final : public MultiClickTriggerBase {
public:
MultiClickTrigger(BinarySensor *parent, std::initializer_list timing)
: MultiClickTriggerBase(parent) {
@@ -140,14 +140,14 @@ template class MultiClickTrigger : public MultiClickTriggerBase {
std::array timing_storage_{};
};
-class StateTrigger : public Trigger {
+class StateTrigger final : public Trigger {
public:
explicit StateTrigger(BinarySensor *parent) {
parent->add_on_state_callback([this](bool state) { this->trigger(state); });
}
};
-class StateChangeTrigger : public Trigger, optional > {
+class StateChangeTrigger final : public Trigger, optional > {
public:
explicit StateChangeTrigger(BinarySensor *parent) {
parent->add_full_state_callback(
@@ -155,7 +155,7 @@ class StateChangeTrigger : public Trigger, optional > {
}
};
-template class BinarySensorCondition : public Condition {
+template class BinarySensorCondition final : public Condition {
public:
BinarySensorCondition(BinarySensor *parent, bool state) : parent_(parent), state_(state) {}
bool check(const Ts &...x) override { return this->parent_->state == this->state_; }
@@ -165,7 +165,7 @@ template class BinarySensorCondition : public Condition {
bool state_;
};
-template class BinarySensorPublishAction : public Action {
+template class BinarySensorPublishAction final : public Action {
public:
explicit BinarySensorPublishAction(BinarySensor *sensor) : sensor_(sensor) {}
TEMPLATABLE_VALUE(bool, state)
@@ -179,7 +179,7 @@ template class BinarySensorPublishAction : public Action
BinarySensor *sensor_;
};
-template class BinarySensorInvalidateAction : public Action {
+template class BinarySensorInvalidateAction final : public Action {
public:
explicit BinarySensorInvalidateAction(BinarySensor *sensor) : sensor_(sensor) {}
diff --git a/esphome/components/binary_sensor_map/binary_sensor_map.h b/esphome/components/binary_sensor_map/binary_sensor_map.h
index 60224242db6..bb2c2739574 100644
--- a/esphome/components/binary_sensor_map/binary_sensor_map.h
+++ b/esphome/components/binary_sensor_map/binary_sensor_map.h
@@ -29,7 +29,7 @@ struct BinarySensorMapChannel {
*
* Each binary sensor has configured parameters that each mapping type uses to compute the single numerical result
*/
-class BinarySensorMap : public sensor::Sensor, public Component {
+class BinarySensorMap final : public sensor::Sensor, public Component {
public:
void dump_config() override;
diff --git a/esphome/components/bl0906/bl0906.h b/esphome/components/bl0906/bl0906.h
index 821aac476c4..54de9f9b0cc 100644
--- a/esphome/components/bl0906/bl0906.h
+++ b/esphome/components/bl0906/bl0906.h
@@ -53,7 +53,7 @@ class BL0906;
using ActionCallbackFuncPtr = void (BL0906::*)();
-class BL0906 : public PollingComponent, public uart::UARTDevice {
+class BL0906 final : public PollingComponent, public uart::UARTDevice {
SUB_SENSOR(voltage)
SUB_SENSOR(current_1)
SUB_SENSOR(current_2)
@@ -103,7 +103,7 @@ class BL0906 : public PollingComponent, public uart::UARTDevice {
std::vector action_queue_{};
};
-template class ResetEnergyAction : public Action, public Parented {
+template class ResetEnergyAction final : public Action, public Parented