Compare commits

..

7 Commits

Author SHA1 Message Date
J. Nick Koston
36250682b0 [core] Split hal.h into per-platform headers under core/hal/
Mirror the wake.{h,cpp} → wake/wake_<platform>.{h,cpp} decomposition
that PR #15978 did. After this change esphome/core/hal.h is a thin
dispatcher and each platform's HAL bits (IRAM_ATTR / PROGMEM macros,
in_isr_context(), the inline yield/delay/micros/millis/millis_64
wrappers, plus ESP8266's progmem_read_*) live in their own header
under esphome/core/hal/.

Scope is headers only — there is no esphome/core/hal.cpp today (every
out-of-line implementation lives in esphome/components/<platform>/core.cpp
alongside platform-specific concerns) so no new .cpp files are added
and no FILTER_SOURCE_FILES entries are needed in core/config.py.
recursive_sources=True on the core manifest already picks up the new
.h files automatically.

No public API moves, no symbol renames, no behavior change. Pure code
motion. The only observable difference is the dispatcher #errors when
no USE_* is set (today an unknown platform silently fell through to
the else branch with empty IRAM_ATTR/PROGMEM); this matches wake.h's
behavior.
2026-04-28 21:19:21 -05:00
J. Nick Koston
4f75647f63 Merge upstream/dev into inline-micros-esp32
Resolves conflict in esphome/components/esp8266/core.cpp per PR #15977 plan:
keep #15662's fast millis() accumulator and optimistic_yield delay() body;
drop the upstream wrappers for yield()/millis_64()/micros() since those
are now always-inlined in hal.h.
2026-04-28 20:54:39 -05:00
J. Nick Koston
9f5121e271 [core] Suppress redundant-declaration warning for ESP32 esp_timer_get_time forward decl 2026-04-24 12:33:41 -05:00
J. Nick Koston
a1e3ec7118 [core] Keep esp8266 millis/delay out-of-line for #15662 compatibility 2026-04-24 12:02:57 -05:00
J. Nick Koston
d3bae21d13 [core] Extend HAL inlining to yield/delay/millis_64 + libretiny + rp2040
Extends the prior commit to cover more wrappers and platforms:

- ESP32: also inline yield() and delay()
- ESP8266: also inline yield(), delay(), millis(), millis_64()
- LibreTiny: inline yield(), delay(), micros(), per-variant millis()
  fast paths, and millis_64() (via Millis64Impl::compute, now reachable
  from hal.h since time_64.h dropped its helpers.h dep)
- RP2040: also inline yield(), delay(), micros()

Consolidates the ESP8266/LibreTiny/RP2040 Arduino-flavored ::yield /
::delay / ::micros wrappers into a single shared block in hal.h.

LibreTiny note: the prior IRAM_ATTR on the wrapper was decorative —
::micros(), ::yield(), ::delay() and xTaskGetTickCount all live in
flash on every libretiny family (realtek-amb, beken-72xx,
lightning-ln882h all checked), so an IRAM ISR call would have crashed
the same way an inlined direct call does.

Also drops the helpers.h include from time_64.h (only used for the
ESPHOME_ALWAYS_INLINE macro, replaced with the raw attribute) so
time_64.h is light enough for hal.h to include.
2026-04-24 11:51:23 -05:00
J. Nick Koston
9d138e73c9 [core] Suppress redundant-declaration warning for ESP8266 micros() forward decl 2026-04-24 11:35:03 -05:00
J. Nick Koston
e23a6bf59f [core] Inline micros()/millis_64() at the HAL layer
Replaces the per-function ``__attribute__((optimize("O2")))`` approach in
#15693 with a direct inline definition of micros() / millis_64() in hal.h
for ESP32, plus inline definitions for ESP8266 micros() and RP2040
millis()/millis_64().

The original goal of #15693 was to inline micros() into the main loop so
that ``call esphome::micros() → call esp_timer_get_time()`` collapses to
a single ``call esp_timer_get_time``. That wrapper-collapse benefits
every micros() call site, not just loop_task — so the real fix is to
mark the wrapper inline, not to bump the loop's optimization level.

Doing this at the HAL layer also makes runtime_stats measurements more
accurate: each timing read no longer hides a wrapper call/return between
the component end-time capture and the underlying clock read.

Refactor: move ``micros_to_millis<>()`` from helpers.h to a new
lightweight ``time_conversion.h`` so hal.h can include it without
pulling the rest of helpers.h into every TU that includes hal.h.
2026-04-24 11:23:16 -05:00
2553 changed files with 16166 additions and 52355 deletions

View File

@@ -398,23 +398,13 @@ This document provides essential context for AI models interacting with this pro
│ ├── i2c/ # I2C bus
│ └── spi/ # SPI bus
└── components/[component]/
├── common.yaml # Component-only config (no bus definitions)
├── test.esp32-idf.yaml # config + compile
├── test.esp8266-ard.yaml # config + compile
── test-variant.esp32-idf.yaml # variant test, config + compile
├── validate.esp32-idf.yaml # config-only (never compiled)
└── validate-legacy.esp32-idf.yaml # config-only variant
├── common.yaml # Component-only config (no bus definitions)
├── test.esp32-idf.yaml
├── test.esp8266-ard.yaml
── test.rp2040-ard.yaml
```
Run them using `script/test_build_components`. Use `-c <component>` to test specific components and `-t <target>` for specific platforms.
* **Config-only test files (`validate.*.yaml`):** Use this prefix when a YAML file only needs to exercise schema/validation paths and does not need to be compiled. CI runs `validate.*.yaml` files with `esphome config` only and skips them during compile. The grammar mirrors `test.*.yaml`:
- `validate.<platform>.yaml` — base config-only test
- `validate-<variant>.<platform>.yaml` — config-only variant
Use this for things like deprecated-syntax migration tests, schema edge cases, or platform-specific validation branches where building firmware adds no signal. A component may have any mix of `test.*.yaml` and `validate.*.yaml` files. Validate files never participate in bus-grouping; each one runs as its own `esphome config` invocation.
When a PR's only edits to a component are `validate.*.yaml` files (no source changes, no `test.*.yaml` changes, and the component isn't pulled in as a dependency of another changed component), CI skips the compile stage for that component entirely and only runs config validation. This is decided in `script/determine-jobs.py` via `_component_change_is_validate_only` and surfaced as the `validate_only_components` output that the `test-build-components-split` job consumes.
* **Test Grouping with Packages:** Components that use shared bus packages can be grouped together in CI to reduce build count. **Never define buses (uart, i2c, spi, modbus) directly in test YAML files** — always use packages from `test_build_components/common/`:
```yaml
# test.esp32-idf.yaml — use packages for buses
@@ -462,7 +452,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.io` repository.
* Documentation is hosted in the separate `esphome/esphome-docs` 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 +671,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.io
- [ ] Updated all internal usage and esphome-docs
- [ ] Tested backward compatibility during deprecation period
* **Deprecation Pattern (C++):**

View File

@@ -5,30 +5,24 @@ Checks: >-
-altera-*,
-android-*,
-boost-*,
-bugprone-derived-method-shadowing-base-method,
-bugprone-easily-swappable-parameters,
-bugprone-implicit-widening-of-multiplication-result,
-bugprone-invalid-enum-default-initialization,
-bugprone-multi-level-implicit-pointer-conversion,
-bugprone-narrowing-conversions,
-bugprone-tagged-union-member-count,
-bugprone-signed-char-misuse,
-bugprone-switch-missing-default-case,
-cert-dcl50-cpp,
-cert-err33-c,
-cert-err58-cpp,
-cert-int09-c,
-cert-oop57-cpp,
-cert-str34-c,
-clang-analyzer-optin.core.EnumCastOutOfRange,
-clang-analyzer-optin.cplusplus.UninitializedObject,
-clang-analyzer-osx.*,
-clang-analyzer-security.ArrayBound,
-clang-diagnostic-delete-abstract-non-virtual-dtor,
-clang-diagnostic-delete-non-abstract-non-virtual-dtor,
-clang-diagnostic-deprecated-declarations,
-clang-diagnostic-ignored-optimization-argument,
-clang-diagnostic-missing-designated-field-initializers,
-clang-diagnostic-missing-field-initializers,
-clang-diagnostic-shadow-field,
-clang-diagnostic-unused-const-variable,
@@ -48,7 +42,6 @@ Checks: >-
-cppcoreguidelines-owning-memory,
-cppcoreguidelines-prefer-member-initializer,
-cppcoreguidelines-pro-bounds-array-to-pointer-decay,
-cppcoreguidelines-pro-bounds-avoid-unchecked-container-access,
-cppcoreguidelines-pro-bounds-constant-array-index,
-cppcoreguidelines-pro-bounds-pointer-arithmetic,
-cppcoreguidelines-pro-type-const-cast,
@@ -61,13 +54,12 @@ Checks: >-
-cppcoreguidelines-rvalue-reference-param-not-moved,
-cppcoreguidelines-special-member-functions,
-cppcoreguidelines-use-default-member-init,
-cppcoreguidelines-use-enum-class,
-cppcoreguidelines-virtual-class-destructor,
-fuchsia-default-arguments-calls,
-fuchsia-default-arguments-declarations,
-fuchsia-multiple-inheritance,
-fuchsia-overloaded-operator,
-fuchsia-statically-constructed-objects,
-fuchsia-default-arguments-declarations,
-fuchsia-default-arguments-calls,
-google-build-using-namespace,
-google-explicit-constructor,
-google-readability-braces-around-statements,
@@ -79,63 +71,49 @@ Checks: >-
-llvm-else-after-return,
-llvm-header-guard,
-llvm-include-order,
-llvm-prefer-static-over-anonymous-namespace,
-llvm-qualified-auto,
-llvm-use-ranges,
-llvmlibc-*,
-misc-const-correctness,
-misc-include-cleaner,
-misc-multiple-inheritance,
-misc-no-recursion,
-misc-non-private-member-variables-in-classes,
-misc-override-with-different-visibility,
-misc-unused-parameters,
-misc-use-anonymous-namespace,
-misc-use-internal-linkage,
-modernize-avoid-bind,
-modernize-avoid-variadic-functions,
-modernize-avoid-c-arrays,
-modernize-avoid-c-style-cast,
-modernize-concat-nested-namespaces,
-modernize-macro-to-enum,
-modernize-return-braced-init-list,
-modernize-type-traits,
-modernize-use-auto,
-modernize-use-constraints,
-modernize-use-default-member-init,
-modernize-use-designated-initializers,
-modernize-use-equals-default,
-modernize-use-integer-sign-comparison,
-modernize-use-nodiscard,
-modernize-use-nullptr,
-modernize-use-ranges,
-modernize-use-nodiscard,
-modernize-use-nullptr,
-modernize-use-trailing-return-type,
-mpi-*,
-objc-*,
-performance-enum-size,
-portability-avoid-pragma-once,
-portability-template-virtual-member-function,
-readability-ambiguous-smartptr-reset-call,
-readability-avoid-nested-conditional-operator,
-readability-container-contains,
-readability-container-data-pointer,
-readability-convert-member-functions-to-static,
-readability-else-after-return,
-readability-enum-initial-value,
-readability-function-cognitive-complexity,
-readability-implicit-bool-conversion,
-readability-isolate-declaration,
-readability-magic-numbers,
-readability-make-member-function-const,
-readability-math-missing-parentheses,
-readability-named-parameter,
-readability-redundant-casting,
-readability-redundant-inline-specifier,
-readability-redundant-member-init,
-readability-redundant-parentheses,
-readability-redundant-typename,
-readability-redundant-string-init,
-readability-uppercase-literal-suffix,
-readability-use-anyofallof,
-readability-use-std-min-max,
-readability-use-concise-preprocessor-directives,
WarningsAsErrors: '*'
FormatStyle: google
CheckOptions:

View File

@@ -1 +1 @@
72f02816e288b68ff4ef4b3d6fb66432c893b187a80ad3ebaa29afa443ff9ea6
1b1ce6324c50c4595703c7df0a8a479b4fe84b71ff1a8793cce1a16f17a33324

View File

@@ -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 <link>` syntax if applicable
- **Pull request in esphome.io**: Link if docs are needed
- **Pull request in esphome-docs**: Link if docs are needed
- **Test Environment**: Check platforms you tested on
- **Example config.yaml**: Include working example YAML
- **Checklist**: Verify code is tested and tests added
@@ -54,9 +54,9 @@ Required fields:
- fixes https://github.com/esphome/esphome/issues/XXX
**Pull request in [esphome.io](https://github.com/esphome/esphome.io) with documentation (if applicable):**
**Pull request in [esphome-docs](https://github.com/esphome/esphome-docs) with documentation (if applicable):**
- esphome/esphome.io#XXX
- esphome/esphome-docs#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.io](https://github.com/esphome/esphome.io).
- [ ] Documentation added/updated in [esphome-docs](https://github.com/esphome/esphome-docs).
```
## 5. Push and Create PR

View File

@@ -115,4 +115,4 @@ examples/
Dockerfile
.git/
tests/
.?*
.*

View File

@@ -2,7 +2,7 @@
blank_issues_enabled: false
contact_links:
- name: Report an issue with the ESPHome documentation
url: https://github.com/esphome/esphome.io/issues/new/choose
url: https://github.com/esphome/esphome-docs/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

View File

@@ -16,9 +16,9 @@
- fixes <link to issue>
**Pull request in [esphome.io](https://github.com/esphome/esphome.io) with documentation (if applicable):**
**Pull request in [esphome-docs](https://github.com/esphome/esphome-docs) with documentation (if applicable):**
- esphome/esphome.io#<esphome.io PR number goes here>
- esphome/esphome-docs#<esphome-docs PR number goes here>
## 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.io](https://github.com/esphome/esphome.io).
- [ ] Documentation added/updated in [esphome-docs](https://github.com/esphome/esphome-docs).

View File

@@ -15,6 +15,11 @@ 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:
@@ -42,7 +47,7 @@ runs:
- name: Build and push to ghcr by digest
id: build-ghcr
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
env:
DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false
@@ -55,6 +60,7 @@ 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
@@ -67,7 +73,7 @@ runs:
- name: Build and push to dockerhub by digest
id: build-dockerhub
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
env:
DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false
@@ -80,6 +86,7 @@ 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

View File

@@ -1,46 +0,0 @@
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 native-IDF component build)
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
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'
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'
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.esphome-idf
key: ${{ runner.os }}-esphome-idf-${{ steps.version.outputs.version }}

View File

@@ -27,18 +27,6 @@ runs:
path: venv
# yamllint disable-line rule:line-length
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ inputs.cache-key }}
- name: Set up uv
# Only needed on cache miss to populate the venv. ``uv pip install``
# 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@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
# Pin uv version so the action does not have to fetch the
# manifest from raw.githubusercontent.com on every cache
# miss; that fetch flakes on Windows runners.
version: "0.11.15"
- name: Create Python virtual environment
if: steps.cache-venv.outputs.cache-hit != 'true' && runner.os != 'Windows'
shell: bash
@@ -46,8 +34,8 @@ runs:
python -m venv venv
source venv/bin/activate
python --version
uv pip install -r requirements.txt -r requirements_test.txt
uv pip install -e .
pip install -r requirements.txt -r requirements_test.txt
pip install -e .
- name: Create Python virtual environment
if: steps.cache-venv.outputs.cache-hit != 'true' && runner.os == 'Windows'
shell: bash
@@ -55,5 +43,5 @@ runs:
python -m venv venv
source ./venv/Scripts/activate
python --version
uv pip install -r requirements.txt -r requirements_test.txt
uv pip install -e .
pip install -r requirements.txt -r requirements_test.txt
pip install -e .

View File

@@ -1 +1 @@
../AGENTS.md
../.ai/instructions.md

View File

@@ -5,7 +5,6 @@ updates:
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 10
ignore:
# Hypotehsis is only used for testing and is updated quite often
- dependency-name: hypothesis

View File

@@ -35,9 +35,6 @@ 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+/
]

View File

@@ -1,3 +1,4 @@
const fs = require('fs');
const { DOCS_PR_PATTERNS } = require('./constants');
const {
COMPONENT_REGEX,
@@ -8,31 +9,6 @@ const {
} = require('../detect-tags');
const { loadCodeowners, getEffectiveOwners } = require('../codeowners');
// Top-level `CONFIG_SCHEMA = ...` (assignment) or `CONFIG_SCHEMA: ConfigType = ...` (annotation).
// Ruff/Black enforce exactly one space around `=` and no space before `:`,
// so we can match strictly: `CONFIG_SCHEMA ` or `CONFIG_SCHEMA:`.
const CONFIG_SCHEMA_REGEX = /^CONFIG_SCHEMA[ :]/m;
// Fetch a file's contents from the PR head SHA via the GitHub API.
// The auto-label workflow runs on `pull_request_target`, which checks out the
// base branch — files added by the PR don't exist in the workspace, so we have
// to fetch them from the head SHA. Returns null if the file can't be fetched.
async function fetchPrFileContent(github, context, path) {
try {
const { owner, repo } = context.repo;
const { data } = await github.rest.repos.getContent({
owner,
repo,
path,
ref: context.payload.pull_request.head.sha,
});
return Buffer.from(data.content, 'base64').toString('utf8');
} catch (error) {
console.log(`Failed to fetch ${path} from PR head:`, error.message);
return null;
}
}
// Strategy: Merge branch detection
async function detectMergeBranch(context) {
const labels = new Set();
@@ -69,72 +45,52 @@ async function detectComponentPlatforms(changedFiles, apiData) {
}
// Strategy: New component detection
async function detectNewComponents(github, context, prFiles) {
async function detectNewComponents(prFiles) {
const labels = new Set();
let hasYamlLoadable = false;
const addedFiles = prFiles.filter(file => file.status === 'added').map(file => file.filename);
for (const file of addedFiles) {
const componentMatch = file.match(/^esphome\/components\/([^\/]+)\/__init__\.py$/);
if (!componentMatch) continue;
labels.add('new-component');
const content = await fetchPrFileContent(github, context, file);
if (content === null) {
// Safe default: assume YAML-loadable so needs-docs behaviour is unchanged on fetch failure
hasYamlLoadable = true;
continue;
}
if (content.includes('IS_TARGET_PLATFORM = True')) {
labels.add('new-target-platform');
}
if (CONFIG_SCHEMA_REGEX.test(content)) {
hasYamlLoadable = true;
if (componentMatch) {
try {
const content = fs.readFileSync(file, 'utf8');
if (content.includes('IS_TARGET_PLATFORM = True')) {
labels.add('new-target-platform');
}
} catch (error) {
console.log(`Failed to read content of ${file}:`, error.message);
}
labels.add('new-component');
}
}
return { labels, hasYamlLoadable };
return labels;
}
// Strategy: New platform detection
async function detectNewPlatforms(github, context, prFiles, apiData) {
async function detectNewPlatforms(prFiles, apiData) {
const labels = new Set();
let hasYamlLoadable = false;
const addedFiles = prFiles.filter(file => file.status === 'added').map(file => file.filename);
const platformPathPatterns = [
/^esphome\/components\/([^\/]+)\/([^\/]+)\.py$/,
/^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);
if (!match) continue;
const platform = match[2];
if (!apiData.platformComponents.includes(platform)) break;
// Skip if this is a restructure between flat and subdirectory forms (either direction):
// <component>/<platform>.py <-> <component>/<platform>/__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) {
// Safe default: assume YAML-loadable so needs-docs behaviour is unchanged on fetch failure
hasYamlLoadable = true;
} else if (CONFIG_SCHEMA_REGEX.test(content)) {
hasYamlLoadable = true;
const platformFileMatch = file.match(/^esphome\/components\/([^\/]+)\/([^\/]+)\.py$/);
if (platformFileMatch) {
const [, component, platform] = platformFileMatch;
if (apiData.platformComponents.includes(platform)) {
labels.add('new-platform');
}
}
const platformDirMatch = file.match(/^esphome\/components\/([^\/]+)\/([^\/]+)\/__init__\.py$/);
if (platformDirMatch) {
const [, component, platform] = platformDirMatch;
if (apiData.platformComponents.includes(platform)) {
labels.add('new-platform');
}
break;
}
}
return { labels, hasYamlLoadable };
return labels;
}
// Strategy: Core files detection
@@ -344,7 +300,7 @@ function detectMaintainerAccess(context) {
}
// Strategy: Requirements detection
async function detectRequirements(allLabels, prFiles, context, hasYamlLoadable) {
async function detectRequirements(allLabels, prFiles, context) {
const labels = new Set();
// Check for missing tests
@@ -352,15 +308,8 @@ async function detectRequirements(allLabels, prFiles, context, hasYamlLoadable)
labels.add('needs-tests');
}
// Check for missing docs.
// `new-feature` (PR-body checkbox) always counts. `new-component` / `new-platform`
// only count when at least one newly added file defines a top-level CONFIG_SCHEMA,
// i.e. the new component/platform is actually loadable from YAML.
const docsEligible =
allLabels.has('new-feature') ||
((allLabels.has('new-component') || allLabels.has('new-platform')) && hasYamlLoadable);
if (docsEligible) {
// Check for missing docs
if (allLabels.has('new-component') || allLabels.has('new-platform') || allLabels.has('new-feature')) {
const prBody = context.payload.pull_request.body || '';
const hasDocsLink = DOCS_PR_PATTERNS.some(pattern => pattern.test(prBody));

View File

@@ -106,8 +106,8 @@ module.exports = async ({ github, context }) => {
const [
branchLabels,
componentLabels,
newComponentResult,
newPlatformResult,
newComponentLabels,
newPlatformLabels,
coreLabels,
sizeLabels,
dashboardLabels,
@@ -120,8 +120,8 @@ module.exports = async ({ github, context }) => {
] = await Promise.all([
detectMergeBranch(context),
detectComponentPlatforms(changedFiles, apiData),
detectNewComponents(github, context, prFiles),
detectNewPlatforms(github, context, prFiles, apiData),
detectNewComponents(prFiles),
detectNewPlatforms(prFiles, apiData),
detectCoreChanges(changedFiles),
detectPRSize(prFiles, totalAdditions, totalDeletions, totalChanges, isMegaPR, SMALL_PR_THRESHOLD, MEDIUM_PR_THRESHOLD, TOO_BIG_THRESHOLD),
detectDashboardChanges(changedFiles),
@@ -133,13 +133,6 @@ module.exports = async ({ github, context }) => {
detectMaintainerAccess(context)
]);
// Extract new-component / new-platform results
const newComponentLabels = newComponentResult.labels;
const newPlatformLabels = newPlatformResult.labels;
// Eligible for needs-docs only if any newly added component or platform file
// defines a top-level CONFIG_SCHEMA (i.e. is actually loadable from YAML).
const hasYamlLoadable = newComponentResult.hasYamlLoadable || newPlatformResult.hasYamlLoadable;
// Extract deprecated component info
const deprecatedLabels = deprecatedResult.labels;
const deprecatedInfo = deprecatedResult.deprecatedInfo;
@@ -161,7 +154,7 @@ module.exports = async ({ github, context }) => {
]);
// Detect requirements based on all other labels
const requirementLabels = await detectRequirements(allLabels, prFiles, context, hasYamlLoadable);
const requirementLabels = await detectRequirements(allLabels, prFiles, context);
for (const label of requirementLabels) {
allLabels.add(label);
}

View File

@@ -1,7 +0,0 @@
{
"name": "auto-label-pr",
"private": true,
"scripts": {
"test": "node --test tests/*.test.js"
}
}

View File

@@ -1,147 +0,0 @@
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);
});
});

View File

@@ -6,10 +6,9 @@ on:
pull_request_target:
types: [labeled, opened, reopened, synchronize, edited]
# All PR/label/review writes are performed with the App token minted below,
# so the workflow's GITHUB_TOKEN only needs read access for checkout.
permissions:
contents: read # actions/checkout reads the workflow source
pull-requests: write
contents: read
env:
SMALL_PR_THRESHOLD: 30
@@ -24,18 +23,14 @@ jobs:
if: github.event.pull_request.state == 'open' && (github.event.action != 'labeled' || github.event.sender.type != 'Bot')
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v2
with:
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
# Scope the minted App token to the minimum needed by auto-label-pr/*.js.
permission-contents: read # repos.getContent for CODEOWNERS and file lookups in detectors.js
permission-issues: write # listLabelsOnIssue, addLabels, removeLabel, list/createComment
permission-pull-requests: write # pulls.listFiles, list/create/update/dismissReview
- name: Auto Label PR
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0

View File

@@ -12,8 +12,8 @@ on:
- ".github/workflows/ci-api-proto.yml"
permissions:
contents: read # actions/checkout for the PR head
pull-requests: write # pulls.createReview / listReviews / dismissReview when generated proto files are stale
contents: read
pull-requests: write
jobs:
check:
@@ -21,21 +21,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
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: 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@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
# Pin uv version so the action does not have to fetch the
# manifest from raw.githubusercontent.com on every cache
# miss; that fetch flakes on Windows runners.
version: "0.11.15"
- name: Install apt dependencies
run: |
@@ -44,7 +34,7 @@ jobs:
sudo apt install -y protobuf-compiler
protoc --version
- name: Install python dependencies
run: uv pip install --system aioesphomeapi -c requirements.txt -r requirements_dev.txt
run: pip install aioesphomeapi -c requirements.txt -r requirements_dev.txt
- name: Generate files
run: script/api_protobuf/api_protobuf.py
- name: Check for changes

View File

@@ -12,8 +12,8 @@ on:
- ".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
contents: read
pull-requests: write
jobs:
verify-hash:
@@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0

View File

@@ -22,7 +22,8 @@ on:
- "script/platformio_install_deps.py"
permissions:
contents: read # actions/checkout only
contents: read
packages: read
concurrency:
# yamllint disable-line rule:line-length
@@ -33,9 +34,6 @@ 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:
@@ -44,94 +42,23 @@ jobs:
- "ha-addon"
- "docker"
# - "lint"
outputs:
tag: ${{ steps.tag.outputs.tag }}
push: ${{ steps.tag.outputs.push }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- 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: Set up Docker Buildx
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Determine tag and whether to push
id: tag
- name: Set TAG
run: |
# 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 }}
echo "TAG=check" >> $GITHUB_ENV
- name: Run build
run: |
docker/build.py \
--tag "${{ steps.tag.outputs.tag }}" \
--tag "${TAG}" \
--arch "${{ matrix.os == 'ubuntu-24.04-arm' && 'aarch64' || 'amd64' }}" \
--build-type "${{ matrix.build_type }}" \
--registry ghcr \
build ${{ steps.tag.outputs.push == 'true' && '--push --no-cache-to' || '' }}
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
build

View File

@@ -1,27 +0,0 @@
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

View File

@@ -7,9 +7,9 @@ on:
types: [completed]
permissions:
contents: read # actions/checkout of the base repo at the PR's target branch
pull-requests: write # gh api to look up the PR by head SHA and post/update the memory-impact comment
actions: read # gh run download for the memory-analysis artifacts produced by the CI workflow run
contents: read
pull-requests: write
actions: read
jobs:
memory-impact-comment:
@@ -49,7 +49,7 @@ jobs:
- name: Check out code from base repository
if: steps.pr.outputs.skip != 'true'
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Always check out from the base repository (esphome/esphome), never from forks
# Use the PR's target branch to ensure we run trusted code from the main repo

View File

@@ -6,10 +6,18 @@ on:
branches: [dev, beta, release]
pull_request:
paths:
- "**"
- "!.github/workflows/*.yml"
- "!.github/actions/build-image/*"
- ".github/workflows/ci.yml"
- "!.yamllint"
- "!.github/dependabot.yml"
- "!docker/**"
merge_group:
permissions:
contents: read # actions/checkout for all jobs; individual jobs add their own scopes when they need to write
contents: read
env:
DEFAULT_PYTHON: "3.11"
@@ -28,7 +36,7 @@ jobs:
cache-key: ${{ steps.cache-key.outputs.key }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- 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
@@ -44,26 +52,14 @@ jobs:
path: venv
# yamllint disable-line rule:line-length
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ steps.cache-key.outputs.key }}
- name: Set up uv
# Only needed on cache miss to populate the venv. ``uv pip install``
# 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@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
# Pin uv version so the action does not have to fetch the
# manifest from raw.githubusercontent.com on every cache
# miss; that fetch flakes on Windows runners.
version: "0.11.15"
- name: Create Python virtual environment
if: steps.cache-venv.outputs.cache-hit != 'true'
run: |
python -m venv venv
. venv/bin/activate
python --version
uv pip install -r requirements.txt -r requirements_dev.txt -r requirements_test.txt pre-commit
uv pip install -e .
pip install -r requirements.txt -r requirements_dev.txt -r requirements_test.txt pre-commit
pip install -e .
pylint:
name: Check pylint
@@ -74,7 +70,7 @@ jobs:
if: needs.determine-jobs.outputs.python-linters == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -93,11 +89,9 @@ jobs:
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
if: needs.determine-jobs.outputs.core-ci == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -113,7 +107,6 @@ 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
@@ -124,7 +117,7 @@ jobs:
if: needs.determine-jobs.outputs.import-time == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -143,60 +136,6 @@ jobs:
if-no-files-found: ignore
retention-days: 14
device-builder:
name: Test downstream esphome/device-builder
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
if: needs.determine-jobs.outputs.device-builder == 'true'
steps:
- name: Check out esphome (this PR)
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
path: esphome
- name: Check out esphome/device-builder
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
repository: esphome/device-builder
ref: main
path: device-builder
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.13"
- name: Set up uv
# Mirrors the install shape device-builder's own CI uses
# (esphome/device-builder#192): uv replaces pip for the
# 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@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
# Pin uv version so the action does not have to fetch the
# manifest from raw.githubusercontent.com on every cache
# miss; that fetch flakes on Windows runners.
version: "0.11.15"
- name: Install device-builder + esphome from PR
# Install device-builder with its esphome + test extras
# first so its pinned versions of pytest/etc. land, then
# overlay the PR's esphome so the downstream tests run
# against this PR's Python code. ``--system`` installs into
# the runner's Python instead of a venv.
run: |
uv pip install --system -e './device-builder[esphome,test]'
uv pip install --system -e ./esphome
- 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. ``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 --ignore=tests/e2e/slow
pytest:
name: Run pytest
strategy:
@@ -221,11 +160,9 @@ jobs:
runs-on: ${{ matrix.os }}
needs:
- common
- determine-jobs
if: needs.determine-jobs.outputs.core-ci == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Restore Python
id: restore-python
uses: ./.github/actions/restore-python
@@ -238,14 +175,14 @@ jobs:
if: matrix.os == 'windows-latest'
run: |
. ./venv/Scripts/activate.ps1
pytest -vv --cov-report=xml --tb=native --durations=30 -n auto tests --ignore=tests/integration/
pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/
- name: Run pytest
if: matrix.os == 'ubuntu-latest' || matrix.os == 'macOS-latest'
run: |
. venv/bin/activate
pytest -vv --cov-report=xml --tb=native --durations=30 -n auto tests --ignore=tests/integration/
pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/
- name: Upload coverage to Codecov
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
- name: Save Python virtual environment cache
@@ -261,17 +198,13 @@ jobs:
needs:
- common
outputs:
core-ci: ${{ steps.determine.outputs.core-ci }}
integration-tests: ${{ steps.determine.outputs.integration-tests }}
integration-test-buckets: ${{ steps.determine.outputs.integration-test-buckets }}
integration-tests-run-all: ${{ steps.determine.outputs.integration-tests-run-all }}
integration-test-files: ${{ steps.determine.outputs.integration-test-files }}
clang-tidy: ${{ steps.determine.outputs.clang-tidy }}
clang-tidy-mode: ${{ steps.determine.outputs.clang-tidy-mode }}
clang-tidy-full-scan: ${{ steps.determine.outputs.clang-tidy-full-scan }}
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 }}
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,11 +214,10 @@ jobs:
cpp-unit-tests-run-all: ${{ steps.determine.outputs.cpp-unit-tests-run-all }}
cpp-unit-tests-components: ${{ steps.determine.outputs.cpp-unit-tests-components }}
component-test-batches: ${{ steps.determine.outputs.component-test-batches }}
validate-only-components: ${{ steps.determine.outputs.validate-only-components }}
benchmarks: ${{ steps.determine.outputs.benchmarks }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Fetch enough history to find the merge base
fetch-depth: 2
@@ -305,27 +237,18 @@ jobs:
GH_TOKEN: ${{ github.token }}
run: |
. venv/bin/activate
EXTRA_ARGS=""
if [[ "${{ contains(github.event.pull_request.labels.*.name, 'ci-run-all') }}" == "true" ]]; then
EXTRA_ARGS="--force-all"
echo "::notice::ci-run-all label detected -- forcing every CI job to run"
fi
output=$(python script/determine-jobs.py $EXTRA_ARGS)
output=$(python script/determine-jobs.py)
echo "Test determination output:"
echo "$output" | jq
# Extract individual fields
echo "core-ci=$(echo "$output" | jq -r '.core_ci')" >> $GITHUB_OUTPUT
echo "integration-tests=$(echo "$output" | jq -r '.integration_tests')" >> $GITHUB_OUTPUT
echo "integration-test-buckets=$(echo "$output" | jq -c '.integration_test_buckets')" >> $GITHUB_OUTPUT
echo "integration-tests-run-all=$(echo "$output" | jq -r '.integration_tests_run_all')" >> $GITHUB_OUTPUT
echo "integration-test-files=$(echo "$output" | jq -c '.integration_test_files')" >> $GITHUB_OUTPUT
echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT
echo "clang-tidy-mode=$(echo "$output" | jq -r '.clang_tidy_mode')" >> $GITHUB_OUTPUT
echo "clang-tidy-full-scan=$(echo "$output" | jq -r '.clang_tidy_full_scan')" >> $GITHUB_OUTPUT
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 "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
@@ -335,7 +258,6 @@ jobs:
echo "cpp-unit-tests-run-all=$(echo "$output" | jq -r '.cpp_unit_tests_run_all')" >> $GITHUB_OUTPUT
echo "cpp-unit-tests-components=$(echo "$output" | jq -c '.cpp_unit_tests_components')" >> $GITHUB_OUTPUT
echo "component-test-batches=$(echo "$output" | jq -c '.component_test_batches')" >> $GITHUB_OUTPUT
echo "validate-only-components=$(echo "$output" | jq -c '.validate_only_components')" >> $GITHUB_OUTPUT
echo "benchmarks=$(echo "$output" | jq -r '.benchmarks')" >> $GITHUB_OUTPUT
- name: Save components graph cache
if: github.ref == 'refs/heads/dev'
@@ -345,19 +267,15 @@ jobs:
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
integration-tests:
name: Run integration tests (${{ matrix.bucket.name }})
name: Run integration tests
runs-on: ubuntu-latest
needs:
- common
- determine-jobs
if: needs.determine-jobs.outputs.integration-tests == 'true'
strategy:
fail-fast: false
matrix:
bucket: ${{ fromJson(needs.determine-jobs.outputs.integration-test-buckets) }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Python 3.13
id: python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
@@ -369,36 +287,31 @@ jobs:
with:
path: venv
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
- 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@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
# Pin uv version so the action does not have to fetch the
# manifest from raw.githubusercontent.com on every cache
# miss; that fetch flakes on Windows runners.
version: "0.11.15"
- name: Create Python virtual environment
if: steps.cache-venv.outputs.cache-hit != 'true'
run: |
python -m venv venv
. venv/bin/activate
python --version
uv pip install -r requirements.txt -r requirements_test.txt
uv pip install -e .
pip install -r requirements.txt -r requirements_test.txt
pip install -e .
- name: Register matcher
run: echo "::add-matcher::.github/workflows/matchers/pytest.json"
- name: Run integration tests
env:
# JSON array of test paths; parsed into a bash array below to avoid
# shell word-splitting / glob hazards.
BUCKET_TESTS: ${{ toJson(matrix.bucket.tests) }}
INTEGRATION_TEST_FILES: ${{ needs.determine-jobs.outputs.integration-test-files }}
INTEGRATION_TESTS_RUN_ALL: ${{ needs.determine-jobs.outputs.integration-tests-run-all }}
run: |
. venv/bin/activate
mapfile -t test_files < <(echo "$BUCKET_TESTS" | jq -r '.[]')
echo "Bucket ${{ matrix.bucket.name }}: running ${#test_files[@]} integration tests"
pytest -vv --no-cov --tb=native --durations=30 -n auto "${test_files[@]}"
if [[ "$INTEGRATION_TESTS_RUN_ALL" == "true" ]]; then
echo "Running all integration tests"
pytest -vv --no-cov --tb=native -n auto tests/integration/
else
# Parse JSON array into bash array to avoid shell expansion issues
mapfile -t test_files < <(echo "$INTEGRATION_TEST_FILES" | jq -r '.[]')
echo "Running ${#test_files[@]} specific integration tests"
pytest -vv --no-cov --tb=native -n auto "${test_files[@]}"
fi
cpp-unit-tests:
name: Run C++ unit tests
@@ -409,7 +322,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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Restore Python
uses: ./.github/actions/restore-python
@@ -438,7 +351,7 @@ jobs:
(github.event_name == 'pull_request' && needs.determine-jobs.outputs.benchmarks == 'true')
steps:
- name: Check out code from GitHub
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Restore Python
uses: ./.github/actions/restore-python
@@ -456,12 +369,9 @@ jobs:
echo "binary=$BINARY" >> $GITHUB_OUTPUT
- name: Run CodSpeed benchmarks
uses: CodSpeedHQ/action@9d332c4d90b43981c3e55ae8e38e68709996240f # v4.17.0
uses: CodSpeedHQ/action@c381be0bfd20e844fb45594f6aa182ffcd94545c # v4.15.0
with:
run: |
. venv/bin/activate
${{ steps.build.outputs.binary }}
pytest tests/benchmarks/python/ --codspeed --no-cov
run: ${{ steps.build.outputs.binary }}
mode: simulation
clang-tidy-single:
@@ -473,8 +383,6 @@ 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
@@ -485,9 +393,9 @@ jobs:
options: --environment esp8266-arduino-tidy --grep USE_ESP8266
pio_cache_key: tidyesp8266
- id: clang-tidy
name: Run script/clang-tidy for ESP32 Arduino
options: --environment esp32-arduino-tidy --grep USE_ARDUINO
cache_idf: true
name: Run script/clang-tidy for ESP32 IDF
options: --environment esp32-idf-tidy --grep USE_ESP_IDF
pio_cache_key: tidyesp32-idf
- id: clang-tidy
name: Run script/clang-tidy for ZEPHYR
options: --environment nrf52-tidy --grep USE_ZEPHYR --grep USE_NRF52
@@ -496,7 +404,7 @@ jobs:
steps:
- name: Check out code from GitHub
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
@@ -508,42 +416,36 @@ jobs:
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache platformio
if: github.ref == 'refs/heads/dev' && matrix.pio_cache_key
if: github.ref == 'refs/heads/dev'
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' && matrix.pio_cache_key
if: github.ref != 'refs/heads/dev'
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
# Shared with the IDF tidy + native-IDF build jobs (same 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.
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
if python script/clang_tidy_hash.py --check; then
echo "full_scan=true" >> $GITHUB_OUTPUT
echo "reason=hash_changed" >> $GITHUB_OUTPUT
else
@@ -555,7 +457,7 @@ jobs:
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 }})"
echo "Running FULL clang-tidy scan (hash changed)"
script/clang-tidy --all-headers --fix ${{ matrix.options }} ${{ matrix.ignore_errors && '|| true' || '' }}
else
echo "Running clang-tidy on changed files only"
@@ -571,7 +473,7 @@ jobs:
if: always()
clang-tidy-nosplit:
name: Run script/clang-tidy for ESP32 IDF
name: Run script/clang-tidy for ESP32 Arduino
runs-on: ubuntu-24.04
needs:
- common
@@ -579,11 +481,9 @@ 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
@@ -594,9 +494,19 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache ESP-IDF install
# Shared with the Arduino tidy + native-IDF build jobs (same install).
uses: ./.github/actions/cache-esp-idf
- 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: Register problem matchers
run: |
@@ -607,13 +517,7 @@ 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.
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
if python script/clang_tidy_hash.py --check; then
echo "full_scan=true" >> $GITHUB_OUTPUT
echo "reason=hash_changed" >> $GITHUB_OUTPUT
else
@@ -625,11 +529,11 @@ jobs:
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 --all-headers --fix --environment esp32-idf-tidy
echo "Running FULL clang-tidy scan (hash changed)"
script/clang-tidy --all-headers --fix --environment esp32-arduino-tidy
else
echo "Running clang-tidy on changed files only"
script/clang-tidy --all-headers --fix --changed --environment esp32-idf-tidy
script/clang-tidy --all-headers --fix --changed --environment esp32-arduino-tidy
fi
env:
# Also cache libdeps, store them in a ~/.platformio subfolder
@@ -648,26 +552,27 @@ 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: 3
max-parallel: 2
matrix:
include:
- id: clang-tidy
name: Run script/clang-tidy for ESP32 IDF 1/3
options: --environment esp32-idf-tidy --split-num 3 --split-at 1
name: Run script/clang-tidy for ESP32 Arduino 1/4
options: --environment esp32-arduino-tidy --split-num 4 --split-at 1
- id: clang-tidy
name: Run script/clang-tidy for ESP32 IDF 2/3
options: --environment esp32-idf-tidy --split-num 3 --split-at 2
name: Run script/clang-tidy for ESP32 Arduino 2/4
options: --environment esp32-arduino-tidy --split-num 4 --split-at 2
- id: clang-tidy
name: Run script/clang-tidy for ESP32 IDF 3/3
options: --environment esp32-idf-tidy --split-num 3 --split-at 3
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
steps:
- name: Check out code from GitHub
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
@@ -678,9 +583,19 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache ESP-IDF install
# Shared with the Arduino tidy + native-IDF build jobs (same install).
uses: ./.github/actions/cache-esp-idf
- 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: Register problem matchers
run: |
@@ -691,13 +606,7 @@ 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.
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
if python script/clang_tidy_hash.py --check; then
echo "full_scan=true" >> $GITHUB_OUTPUT
echo "reason=hash_changed" >> $GITHUB_OUTPUT
else
@@ -709,7 +618,7 @@ jobs:
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 }})"
echo "Running FULL clang-tidy scan (hash changed)"
script/clang-tidy --all-headers --fix ${{ matrix.options }}
else
echo "Running clang-tidy on changed files only"
@@ -723,93 +632,6 @@ 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
# Shared with the IDF/Arduino clang-tidy jobs + native-IDF build (same 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++ 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.
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
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
@@ -838,7 +660,7 @@ jobs:
version: 1.0
- name: Check out code from GitHub
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -895,7 +717,7 @@ jobs:
fi
echo ""
# Show disk space before validation
# Show disk space before validation (after bind mounts setup)
echo "Disk space before config validation:"
df -h
echo ""
@@ -907,141 +729,23 @@ jobs:
echo "Config validation passed! Starting compilation..."
echo ""
# Compute the compile-stage component list. Components whose only
# changes are validate.*.yaml files are config-only -- their source
# and test fixtures didn't move, so rebuilding firmware adds no
# signal. Subtract them from this batch before invoking compile.
validate_only_json='${{ needs.determine-jobs.outputs.validate-only-components }}'
if [ -z "$validate_only_json" ]; then
validate_only_json='[]'
fi
if ! validate_only_csv=$(echo "$validate_only_json" | jq -r 'join(",")'); then
echo "::error::Failed to render validate-only-components as CSV from: $validate_only_json"
exit 1
fi
if [ -z "$validate_only_csv" ]; then
compile_csv="$components_csv"
else
components_sorted=$(echo "$components_csv" | tr ',' '\n' | sort -u)
validate_sorted=$(echo "$validate_only_csv" | tr ',' '\n' | sort -u)
if ! diff_out=$(comm -23 <(echo "$components_sorted") <(echo "$validate_sorted")); then
echo "::error::Failed to compute compile component subset."
exit 1
fi
compile_csv=$(echo "$diff_out" | paste -sd ',' -)
skipped=$(comm -12 <(echo "$components_sorted") <(echo "$validate_sorted") | paste -sd ',' -)
if [ -n "$skipped" ]; then
echo "Validate-only components in this batch (skipping compile): $skipped"
fi
fi
# Show disk space before compilation
echo "Disk space before compilation:"
df -h
echo ""
if [ -n "$compile_csv" ]; then
# Run compilation with grouping and isolation
python3 script/test_build_components.py -e compile -c "$compile_csv" -f --isolate "$directly_changed_csv"
else
echo "All components in this batch are validate-only -- skipping compile stage."
fi
test-native-idf:
name: Test components with native ESP-IDF
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
if: github.event_name == 'pull_request' && needs.determine-jobs.outputs.native-idf == '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).
# 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 }}
steps:
- name: Check out code from GitHub
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: Prepare build storage on /mnt
# Bind-mount the larger /mnt disk over the IDF install + build dirs BEFORE
# restoring the cache, so the ~4.5GB restore lands on the roomier volume
# instead of being shadowed by a mount set up later in the run step.
run: |
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"
if [ -n "$mnt_avail" ] && [ "$mnt_avail" -gt "$root_avail" ]; then
echo "Using /mnt for build files (more space available)"
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
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
- name: Cache ESP-IDF install
# Shared with the IDF/Arduino clang-tidy jobs (same install); restores
# into the /mnt bind-mount prepared above when present.
uses: ./.github/actions/cache-esp-idf
- name: Run native ESP-IDF compile test
run: |
. venv/bin/activate
echo "Testing components: $TEST_COMPONENTS"
echo ""
# Show disk space before validation
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
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 }}
# Run compilation with grouping and isolation
python3 script/test_build_components.py -e compile -c "$components_csv" -f --isolate "$directly_changed_csv"
pre-commit-ci-lite:
name: pre-commit.ci lite
runs-on: ubuntu-latest
needs:
- common
- determine-jobs
if: github.event_name == 'pull_request' && !startsWith(github.base_ref, 'beta') && !startsWith(github.base_ref, 'release') && needs.determine-jobs.outputs.core-ci == 'true'
if: github.event_name == 'pull_request' && !startsWith(github.base_ref, 'beta') && !startsWith(github.base_ref, 'release')
steps:
- name: Check out code from GitHub
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -1067,7 +771,7 @@ jobs:
skip: ${{ steps.check-script.outputs.skip || steps.check-tests.outputs.skip }}
steps:
- name: Check out target branch
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.base_ref }}
@@ -1249,7 +953,7 @@ jobs:
flash_usage: ${{ steps.extract.outputs.flash_usage }}
steps:
- name: Check out PR branch
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -1312,13 +1016,13 @@ jobs:
- memory-impact-pr-branch
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && fromJSON(needs.determine-jobs.outputs.memory_impact).should_run == 'true' && needs.memory-impact-target-branch.outputs.skip != 'true'
permissions:
contents: read # actions/checkout to load the comment-posting script
pull-requests: write # ci_memory_impact_comment.py posts/updates the memory-impact comment on the PR
contents: read
pull-requests: write
env:
GH_TOKEN: ${{ github.token }}
steps:
- name: Check out code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -1361,11 +1065,8 @@ 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
- pre-commit-ci-lite
- memory-impact-target-branch
- memory-impact-pr-branch

View File

@@ -6,8 +6,8 @@ on:
types: [opened, reopened]
permissions:
pull-requests: write # pulls.update to close the PR opened from a fork's default branch
issues: write # issues.createComment to explain to the contributor why the PR was closed
pull-requests: write
issues: write
jobs:
close:

View File

@@ -15,9 +15,9 @@ on:
- beta
permissions:
issues: write # issues.addLabels / removeLabel to manage the 'code-owner-approved' label on the PR
pull-requests: read # listReviews to determine whether a codeowner has approved
contents: read # actions/checkout to read CODEOWNERS and the shared codeowners.js helper
issues: write
pull-requests: read
contents: read
jobs:
codeowner-approved:
@@ -26,7 +26,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout base branch
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.pull_request.base.sha }}
sparse-checkout: |

View File

@@ -17,10 +17,9 @@ on:
- release
- beta
# PR/review writes (requestReviewers, issues.createComment) are performed with the App token minted below,
# so the workflow's GITHUB_TOKEN only needs read access for checkout.
permissions:
contents: read # actions/checkout to read CODEOWNERS and the shared codeowners.js helper
pull-requests: write
contents: read
jobs:
request-codeowner-reviews:
@@ -29,24 +28,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout base branch
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.pull_request.base.sha }}
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
# Scope the minted App token to the minimum needed by the github-script step below.
permission-pull-requests: write # pulls.listFiles, pulls.get, pulls.listReviews, pulls.requestReviewers
permission-issues: write # issues.listComments and issues.createComment (PR comments use the issues API)
- name: Request reviews from component codeowners
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ steps.generate-token.outputs.token }}
script: |
const { loadCodeowners, getEffectiveOwners } = require('./.github/scripts/codeowners.js');

View File

@@ -16,9 +16,6 @@ on:
schedule:
- cron: "30 18 * * 4"
# Deny by default; the analyze job opts in to exactly what it needs.
permissions: {}
jobs:
analyze:
name: Analyze (${{ matrix.language }})
@@ -29,10 +26,15 @@ jobs:
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
permissions:
security-events: write # upload CodeQL SARIF results to the Code Scanning API
packages: read # fetch internal or private CodeQL query packs
actions: read # required by codeql-action when run from a private repo
contents: read # actions/checkout to scan the repository
# required for all workflows
security-events: write
# required to fetch internal or private CodeQL packs
packages: read
# only required for workflows in private repositories
actions: read
contents: read
strategy:
fail-fast: false
@@ -52,11 +54,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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -84,6 +86,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
with:
category: "/language:${{matrix.language}}"

View File

@@ -1,119 +0,0 @@
name: Add Dashboard Deprecation Comment
on:
pull_request_target:
types: [opened, synchronize]
# All API calls (pulls.listFiles + issues.{list,create,update}Comment) are performed with
# the App token minted below, so the workflow's GITHUB_TOKEN does not need any scopes.
permissions: {}
jobs:
dashboard-deprecation-comment:
name: Dashboard deprecation comment
runs-on: ubuntu-latest
# Release-bump PRs (bump-X.Y.Z -> beta, beta -> release) inevitably
# roll up everything merged into dev since the last cut, which can
# include dashboard changes that have already been reviewed once.
# The bot's purpose is to warn new contributors before they invest
# time -- that only applies to PRs entering dev.
if: github.event.pull_request.base.ref == 'dev'
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
# pulls.listFiles + issues.{list,create,update}Comment on PRs. For PR resources
# the issues.*Comment APIs require the pull-requests scope, not issues.
permission-pull-requests: write
- name: Add dashboard deprecation comment
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ steps.generate-token.outputs.token }}
script: |
const commentMarker = "<!-- This comment was generated automatically by the dashboard-deprecation-comment workflow. -->";
const commentBody = `Thanks for opening this PR!
Heads up: the legacy ESPHome dashboard (\`esphome/dashboard/\` and \`tests/dashboard/\`) is **deprecated** and is being replaced by [ESPHome Device Builder](https://github.com/esphome/device-builder). We are not adding new features to the legacy dashboard and it will eventually be removed from this repository.
What this means for your PR:
- **New features / enhancements**: please port the change to [esphome/device-builder](https://github.com/esphome/device-builder) instead. We are unlikely to review or merge new dashboard features here.
- **Bug fixes**: small fixes may still be considered, but please check first whether the same issue exists in Device Builder, where the fix will have a longer life.
- **Security issues**: please do not file a public PR. Report privately via [GitHub security advisories](https://github.com/esphome/esphome/security/advisories/new) so we can coordinate a fix.
We appreciate the contribution and apologize for the friction; flagging this early so your time isn't spent on a change that may not land.
---
(Added by the PR bot)
${commentMarker}`;
async function getDashboardChanges(github, owner, repo, prNumber) {
const changedFiles = await github.paginate(
github.rest.pulls.listFiles,
{
owner: owner,
repo: repo,
pull_number: prNumber,
per_page: 100,
}
);
return changedFiles.filter(file =>
file.filename.startsWith('esphome/dashboard/') ||
file.filename.startsWith('tests/dashboard/')
);
}
async function findBotComment(github, owner, repo, prNumber) {
const comments = await github.paginate(
github.rest.issues.listComments,
{
owner: owner,
repo: repo,
issue_number: prNumber,
per_page: 100,
}
);
return comments.find(comment =>
comment.body.includes(commentMarker) && comment.user.type === "Bot"
);
}
const prNumber = context.payload.pull_request.number;
const { owner, repo } = context.repo;
const dashboardChanges = await getDashboardChanges(github, owner, repo, prNumber);
const existingComment = await findBotComment(github, owner, repo, prNumber);
if (dashboardChanges.length === 0) {
// PR doesn't (or no longer) touches the legacy dashboard. If we previously
// commented (e.g. files were removed in a later push), leave the comment in
// place for history rather than thrash on edit/delete.
return;
}
if (existingComment) {
if (existingComment.body === commentBody) {
return;
}
await github.rest.issues.updateComment({
owner: owner,
repo: repo,
comment_id: existingComment.id,
body: commentBody,
});
} else {
await github.rest.issues.createComment({
owner: owner,
repo: repo,
issue_number: prNumber,
body: commentBody,
});
}

View File

@@ -4,29 +4,20 @@ on:
pull_request_target:
types: [opened, synchronize]
# All API calls (pulls.listFiles + issues.{list,create,update}Comment) are performed with
# the App token minted below, so the workflow's GITHUB_TOKEN does not need any scopes.
permissions: {}
permissions:
contents: read # Needed to fetch PR details
issues: write # Needed to create and update comments (PR comments are managed via the issues REST API)
pull-requests: write # also needed?
jobs:
external-comment:
name: External component comment
runs-on: ubuntu-latest
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
# pulls.listFiles + issues.{list,create,update}Comment on PRs. For PR resources
# the issues.*Comment APIs require the pull-requests scope, not issues.
permission-pull-requests: write
- name: Add external component comment
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ steps.generate-token.outputs.token }}
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
// Generate external component usage instructions
function generateExternalComponentInstructions(prNumber, componentNames, owner, repo) {

View File

@@ -9,8 +9,8 @@ on:
types: [labeled]
permissions:
issues: write # issues.createComment to mention component codeowners on the newly labelled issue
contents: read # repos.getContent to fetch CODEOWNERS from the default branch
issues: write
contents: read
jobs:
notify-codeowners:

View File

@@ -6,12 +6,6 @@ on:
- cron: "30 0 * * *" # Run daily at 00:30 UTC
workflow_dispatch:
# Deny by default; the lock job opts in to exactly what the reusable workflow needs.
permissions: {}
jobs:
lock:
permissions:
issues: write # issues.lock on closed issues
pull-requests: write # issues.lock on closed pull requests
uses: esphome/workflows/.github/workflows/lock.yml@025a1e6255610c498ed590403b7e510b69e474df # 2026.4.1
uses: esphome/workflows/.github/workflows/lock.yml@3c4e8446aa1029f1c346a482034b3ee1489077ca # 2026.4.0

View File

@@ -8,15 +8,15 @@ on:
- beta
permissions:
contents: read # actions/checkout to load detect-tags.js
pull-requests: read # pulls.listFiles to map changed files to component/core/dashboard/ci tags
contents: read
pull-requests: read
jobs:
check:
name: Validate PR title
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
@@ -29,11 +29,10 @@ jobs:
} = require('./.github/scripts/detect-tags.js');
const title = context.payload.pull_request.title;
const user = context.payload.pull_request.user;
const author = context.payload.pull_request.user.login;
// Skip bot PRs (e.g. dependabot, esphome[bot] device-class sync) -
// they have their own title formats.
if (user.type === 'Bot') {
// Skip bot PRs (e.g. dependabot) - they have their own title format
if (author === 'dependabot[bot]') {
return;
}
@@ -69,15 +68,14 @@ jobs:
return;
}
// Check for MDX syntax characters not wrapped in backticks.
// Astro docs MDX treats bare `<` as JSX component opening tags and
// bare `{` as JS expressions, so both must be escaped in changelog entries.
// Check for angle brackets not wrapped in backticks.
// Astro docs MDX treats bare < as JSX component opening tags.
const stripped = title.replace(/`[^`]*`/g, '');
if (/[<>{}]/.test(stripped)) {
if (/[<>]/.test(stripped)) {
core.setFailed(
'PR title contains `<`, `>`, `{`, or `}` not wrapped in backticks.\n' +
'Astro docs MDX interprets bare `<` as JSX components and bare `{` as JS expressions.\n' +
'Please wrap these characters with backticks, e.g.: [component] Add `<feature>` support'
'PR title contains `<` or `>` not wrapped in backticks.\n' +
'Astro docs MDX interprets bare `<` as JSX components.\n' +
'Please wrap angle brackets with backticks, e.g.: [component] Add `<feature>` support'
);
return;
}

View File

@@ -9,7 +9,7 @@ on:
- cron: "0 2 * * *"
permissions:
contents: read # actions/checkout for all jobs; deploy jobs add their own scopes when they need to write
contents: read
jobs:
init:
@@ -20,7 +20,7 @@ jobs:
branch_build: ${{ steps.tag.outputs.branch_build }}
deploy_env: ${{ steps.tag.outputs.deploy_env }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Get tag
id: tag
# yamllint disable rule:line-length
@@ -57,10 +57,10 @@ jobs:
if: github.repository == 'esphome/esphome' && github.event_name == 'release'
runs-on: ubuntu-latest
permissions:
contents: read # actions/checkout to build the sdist/wheel
id-token: write # OIDC token for PyPI Trusted Publishing (pypa/gh-action-pypi-publish)
contents: read
id-token: write
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
@@ -78,8 +78,8 @@ jobs:
name: Build ESPHome ${{ matrix.platform.arch }}
if: github.repository == 'esphome/esphome'
permissions:
contents: read # actions/checkout to load Dockerfile and build context
packages: write # docker/login-action + build-push-action push image digests to ghcr.io
contents: read
packages: write
runs-on: ${{ matrix.platform.os }}
needs: [init]
strategy:
@@ -92,22 +92,22 @@ jobs:
os: "ubuntu-24.04-arm"
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- 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: Set up Docker Buildx
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Log in to docker hub
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the GitHub container registry
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -152,8 +152,8 @@ jobs:
- deploy-docker
if: github.repository == 'esphome/esphome'
permissions:
contents: read # actions/checkout to load Dockerfile and build context
packages: write # docker/login-action + build-push-action push image digests to ghcr.io
contents: read
packages: write
strategy:
fail-fast: false
matrix:
@@ -168,7 +168,7 @@ jobs:
- ghcr
- dockerhub
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Download digests
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
@@ -178,17 +178,17 @@ jobs:
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Log in to docker hub
if: matrix.registry == 'dockerhub'
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the GitHub container registry
if: matrix.registry == 'ghcr'
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -212,6 +212,72 @@ jobs:
docker buildx imagetools create $(jq -Rcnr 'inputs | . / "," | map("-t " + .) | join(" ")' <<< "${{ steps.tags.outputs.tags}}") \
$(printf '${{ steps.tags.outputs.image }}@sha256:%s ' *)
deploy-ha-addon-repo:
if: github.repository == 'esphome/esphome' && needs.init.outputs.branch_build == 'false'
runs-on: ubuntu-latest
needs:
- init
- deploy-manifest
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
owner: esphome
repositories: home-assistant-addon
- name: Trigger Workflow
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ steps.generate-token.outputs.token }}
script: |
let description = "ESPHome";
if (context.eventName == "release") {
description = ${{ toJSON(github.event.release.body) }};
}
github.rest.actions.createWorkflowDispatch({
owner: "esphome",
repo: "home-assistant-addon",
workflow_id: "bump-version.yml",
ref: "main",
inputs: {
version: "${{ needs.init.outputs.tag }}",
content: description
}
})
deploy-esphome-schema:
if: github.repository == 'esphome/esphome' && needs.init.outputs.branch_build == 'false'
runs-on: ubuntu-latest
needs: [init]
environment: ${{ needs.init.outputs.deploy_env }}
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
owner: esphome
repositories: esphome-schema
- name: Trigger Workflow
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ steps.generate-token.outputs.token }}
script: |
github.rest.actions.createWorkflowDispatch({
owner: "esphome",
repo: "esphome-schema",
workflow_id: "generate-schemas.yml",
ref: "main",
inputs: {
version: "${{ needs.init.outputs.tag }}",
}
})
version-notifier:
if: github.repository == 'esphome/esphome' && needs.init.outputs.branch_build == 'false'
runs-on: ubuntu-latest
@@ -221,20 +287,19 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
with:
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
owner: esphome
repositories: version-notifier
permission-actions: write # actions.createWorkflowDispatch on the target repo (only API call made with this token)
- name: Trigger Workflow
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ steps.generate-token.outputs.token }}
script: |
await github.rest.actions.createWorkflowDispatch({
github.rest.actions.createWorkflowDispatch({
owner: "esphome",
repo: "version-notifier",
workflow_id: "notify.yml",

View File

@@ -7,8 +7,8 @@ on:
workflow_dispatch:
permissions:
issues: write # actions/stale labels, comments on, and closes stale issues
pull-requests: write # actions/stale labels, comments on, and closes stale pull requests
issues: write
pull-requests: write
concurrency:
group: lock
@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Stale
uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
with:
debug-only: ${{ github.ref != 'refs/heads/dev' }} # Dry-run when not run on dev branch
remove-stale-when-updated: true

View File

@@ -4,9 +4,6 @@ on:
pull_request:
types: [opened, reopened, labeled, unlabeled, synchronize]
permissions:
pull-requests: read # issues.listLabelsOnIssue to detect blocking labels (needs-docs, merge-after-release, chained-pr)
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true

View File

@@ -6,32 +6,17 @@ on:
schedule:
- cron: "45 6 * * *"
# Repo writes (branch push, PR open) happen via the App token minted below,
# so the workflow's GITHUB_TOKEN does not need any write scopes.
permissions:
contents: read # actions/checkout for this repo and home-assistant/core
jobs:
sync:
name: Sync Device Classes
runs-on: ubuntu-latest
if: github.repository == 'esphome/esphome'
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
# Scope the minted App token to the minimum needed by peter-evans/create-pull-request.
permission-contents: write # git.createCommit + refs.create/update to push the sync/device-classes branch
permission-pull-requests: write # pulls.create / pulls.update to open or refresh the sync PR
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Checkout Home Assistant
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
repository: home-assistant/core
path: lib/home-assistant
@@ -41,56 +26,19 @@ jobs:
with:
python-version: "3.14"
- name: Set up uv
# An order of magnitude faster than pip on cold boots, with its
# own wheel cache. ``--system`` (below) installs into the
# 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@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
# Pin uv version so the action does not have to fetch the
# manifest from raw.githubusercontent.com on every cache
# miss; that fetch flakes on Windows runners.
version: "0.11.15"
- name: Install Home Assistant
run: |
uv pip install --system -e lib/home-assistant
uv pip install --system -r requirements.txt -r requirements_test.txt pre-commit
python -m pip install --upgrade pip
pip install -e lib/home-assistant
pip install -r requirements_test.txt pre-commit
- name: Sync
run: |
python ./script/sync-device_class.py
- name: Apply pre-commit auto-fixes
# First pass: let formatters (ruff, end-of-file-fixer, etc.) modify
# files. pre-commit exits non-zero whenever a hook touches anything,
# which would otherwise abort the workflow before the auto-fixes
# can flow into the sync PR.
#
# SKIP:
# - no-commit-to-branch is a local guard against committing on
# dev/release/beta; CI runs on dev by definition, and
# peter-evans/create-pull-request creates the branch itself.
# - pylint surfaces import-error / relative-beyond-top-level
# noise here because this workflow installs only a subset of
# the runtime deps (HA + requirements*.txt); main CI already
# gates pylint on real PRs.
env:
SKIP: pylint,no-commit-to-branch
run: python script/run-in-env.py pre-commit run --all-files || true
- name: Verify pre-commit clean
# Second pass: re-run all hooks against the now-fixed tree.
# Auto-fixers exit 0 (nothing to change); any remaining failure
# from a check-only hook (flake8 / yamllint / ci-custom) is a
# real issue and fails the workflow loudly. Same SKIP list as
# above for the same reasons.
env:
SKIP: pylint,no-commit-to-branch
run: python script/run-in-env.py pre-commit run --all-files
- name: Run pre-commit hooks
run: |
python script/run-in-env.py pre-commit run --all-files
- name: Commit changes
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
@@ -102,4 +50,4 @@ jobs:
delete-branch: true
title: "Synchronise Device Classes from Home Assistant"
body-path: .github/PULL_REQUEST_TEMPLATE.md
token: ${{ steps.generate-token.outputs.token }}
token: ${{ secrets.DEVICE_CLASS_SYNC_TOKEN }}

1
.gitignore vendored
View File

@@ -141,7 +141,6 @@ tests/.esphome/
sdkconfig.*
!sdkconfig.defaults
!sdkconfig.defaults.*
.tests/

View File

@@ -11,7 +11,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.15.15
rev: v0.15.12
hooks:
# Run the linter.
- id: ruff
@@ -55,7 +55,7 @@ repos:
hooks:
- id: pylint
name: pylint
entry: python script/run-in-env.py pylint
entry: python3 script/run-in-env.py pylint
language: system
types: [python]
files: ^esphome/.+\.py$
@@ -63,10 +63,10 @@ repos:
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|sdkconfig\.defaults|esphome/idf_component\.yml)$
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
entry: python3 script/run-in-env.py script/ci-custom.py
language: system

View File

@@ -1 +1 @@
AGENTS.md
.ai/instructions.md

View File

@@ -19,6 +19,7 @@ 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
@@ -27,7 +28,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 @ncareau
esphome/components/airthings_wave_base/* @jeromelaban @kpfleming @ncareau
esphome/components/airthings_wave_mini/* @ncareau
esphome/components/airthings_wave_plus/* @jeromelaban @precurse
esphome/components/alarm_control_panel/* @grahambrown11 @hwstar
@@ -83,7 +84,6 @@ 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/* @latonita @PolarGoose @SimonFischer04 @Tomer27cz
esphome/components/dlms_meter/* @SimonFischer04
esphome/components/dps310/* @kbx81
esphome/components/ds1307/* @badbadc0ffee
esphome/components/ds2484/* @mrk-its
@@ -291,7 +291,6 @@ 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
@@ -352,7 +351,6 @@ 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
@@ -381,7 +379,6 @@ 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
@@ -419,8 +416,6 @@ esphome/components/resampler/speaker/* @kahrendt
esphome/components/restart/* @esphome/core
esphome/components/rf_bridge/* @jesserockz
esphome/components/rgbct/* @jesserockz
esphome/components/ring_buffer/* @kahrendt
esphome/components/router/speaker/* @kahrendt
esphome/components/rp2040/* @jesserockz
esphome/components/rp2040_ble/* @bdraco
esphome/components/rp2040_pio_led_strip/* @Papa-DMan
@@ -598,7 +593,6 @@ 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

View File

@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version
# control system is used.
PROJECT_NUMBER = 2026.6.2
PROJECT_NUMBER = 2026.5.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

View File

@@ -1 +1 @@
AGENTS.md
.ai/instructions.md

View File

@@ -1,18 +0,0 @@
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

View File

@@ -1,9 +1,10 @@
ARG BUILD_VERSION=dev
ARG BUILD_BASE_VERSION=2026.06.0
ARG BUILD_OS=alpine
ARG BUILD_BASE_VERSION=2025.04.0
ARG BUILD_TYPE=docker
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
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
ARG BUILD_TYPE
FROM base-source-${BUILD_TYPE} AS base
@@ -12,14 +13,14 @@ RUN git config --system --add safe.directory "*" \
&& git config --system advice.detachedHead false
# Install build tools for Python packages that require compilation
# (e.g., ruamel.yaml.clib used by ESP-IDF's idf-component-manager).
# Also install libusb-1.0 at runtime so the ESP-IDF tools installer can
# 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 apt-get update \
&& apt-get install -y --no-install-recommends build-essential libusb-1.0-0 \
&& rm -rf /var/lib/apt/lists/*
# (e.g., ruamel.yaml.clibz used by ESP-IDF's idf-component-manager)
RUN if command -v apk > /dev/null; then \
apk add --no-cache build-base; \
else \
apt-get update \
&& apt-get install -y --no-install-recommends build-essential \
&& rm -rf /var/lib/apt/lists/*; \
fi
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
@@ -31,9 +32,6 @@ 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.12
RUN \
platformio settings set enable_telemetry No \
&& platformio settings set check_platformio_interval 1000000 \

View File

@@ -20,10 +20,6 @@ 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(
@@ -38,12 +34,6 @@ 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"
)
@@ -55,11 +45,6 @@ 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"
)
@@ -110,14 +95,11 @@ 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:
# Custom tag (e.g. a branch name) -- push only the tag itself
channel = None
channel = CHANNEL_DEV
elif match.group(2) is None:
major_minor_version = match.group(1)
channel = CHANNEL_RELEASE
@@ -146,18 +128,11 @@ def main():
CHANNEL_DEV: "cache-dev",
CHANNEL_BETA: "cache-beta",
CHANNEL_RELEASE: "cache-latest",
}.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}"
}[channel]
cache_img = f"ghcr.io/{params.build_to}:{cache_tag}"
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]
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]
# 3. build
cmd = [
@@ -180,9 +155,7 @@ def main():
for img in imgs:
cmd += ["--tag", img]
if args.push:
cmd += ["--push"]
if not args.no_cache_to:
cmd += ["--cache-to", f"type=registry,ref={cache_img},mode=max"]
cmd += ["--push", "--cache-to", f"type=registry,ref={cache_img},mode=max"]
if args.load:
cmd += ["--load"]
@@ -190,22 +163,20 @@ def main():
elif args.command == "manifest":
manifest = DockerParams.for_type_arch(args.build_type, ARCH_AMD64).manifest_to
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.
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
for target in targets:
cmd = ["docker", "buildx", "imagetools", "create", "--tag", target]
cmd = ["docker", "manifest", "create", 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__":

View File

@@ -27,12 +27,4 @@ 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 "$@"

View File

@@ -0,0 +1,96 @@
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;
}

View File

@@ -0,0 +1,16 @@
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 "";

View File

@@ -0,0 +1,8 @@
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;

View File

@@ -0,0 +1,8 @@
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;

View File

@@ -0,0 +1,3 @@
upstream esphome {
server unix:/var/run/esphome.sock;
}

View File

@@ -0,0 +1,30 @@
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;
}

View File

@@ -0,0 +1 @@
Without requirements or design, programming is the art of adding bugs to an empty text file. (Louis Srygley)

View File

@@ -0,0 +1,28 @@
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;
}
}

View File

@@ -0,0 +1,18 @@
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;
}
}

View File

@@ -16,7 +16,7 @@ fi
port=$(bashio::addon.ingress_port)
# Wait for the ESPHome Device Builder to become available
# Wait for NGINX to become available
bashio::net.wait_for "${port}" "127.0.0.1" 300
config=$(\

View File

@@ -2,7 +2,7 @@
# shellcheck shell=bash
# ==============================================================================
# Home Assistant Community Add-on: ESPHome
# Take down the S6 supervision tree when ESPHome Device Builder fails
# Take down the S6 supervision tree when ESPHome dashboard fails
# ==============================================================================
declare exit_code
readonly exit_code_container=$(</run/s6-linux-init-container-results/exitcode)
@@ -10,7 +10,7 @@ readonly exit_code_service="${1}"
readonly exit_code_signal="${2}"
bashio::log.info \
"Service ESPHome Device Builder exited with code ${exit_code_service}" \
"Service ESPHome dashboard exited with code ${exit_code_service}" \
"(by signal ${exit_code_signal})"
if [[ "${exit_code_service}" -eq 256 ]]; then

View File

@@ -2,7 +2,7 @@
# shellcheck shell=bash
# ==============================================================================
# Community Hass.io Add-ons: ESPHome
# Runs the ESPHome Device Builder
# Runs the ESPHome dashboard
# ==============================================================================
readonly pio_cache_base=/data/cache/platformio
@@ -49,21 +49,5 @@ if bashio::fs.directory_exists '/config/esphome/.esphome'; then
rm -rf /config/esphome/.esphome
fi
# Only signal device-builder to expose the public LAN port when the operator
# mapped port 6052, matching the legacy dashboard where nginx listened on the
# fixed port 6052 only when it was configured. We use the mapping purely as a
# presence check and don't forward the published value; device-builder binds
# its default port 6052 (the fixed container port, as the legacy
# "listen 6052" did). --ha-addon-allow-public is inert on its own: the no-auth
# gate is the DISABLE_HA_AUTHENTICATION env var set above, so both opt-ins are
# required to bind 6052 unauthenticated; either alone stays ingress-only.
set --
if bashio::var.has_value "$(bashio::addon.port 6052)"; then
set -- --ha-addon-allow-public
fi
bashio::log.info "Starting ESPHome Device Builder..."
exec esphome-device-builder /config/esphome \
--ha-addon \
--ingress-port "$(bashio::addon.ingress_port)" \
"$@"
bashio::log.info "Starting ESPHome dashboard..."
exec esphome dashboard /config/esphome --socket /var/run/esphome.sock --ha-addon

View File

@@ -0,0 +1,27 @@
#!/command/with-contenv bashio
# shellcheck shell=bash
# ==============================================================================
# Community Hass.io Add-ons: ESPHome
# Configures NGINX for use with ESPHome
# ==============================================================================
mkdir -p /var/log/nginx
# Generate Ingress configuration
bashio::var.json \
interface "$(bashio::addon.ip_address)" \
port "^$(bashio::addon.ingress_port)" \
| tempio \
-template /etc/nginx/templates/ingress.gtpl \
-out /etc/nginx/servers/ingress.conf
# Generate direct access configuration, if enabled.
if bashio::var.has_value "$(bashio::addon.port 6052)"; then
bashio::config.require.ssl
bashio::var.json \
certfile "$(bashio::config 'certfile')" \
keyfile "$(bashio::config 'keyfile')" \
ssl "^$(bashio::config 'ssl')" \
| tempio \
-template /etc/nginx/templates/direct.gtpl \
-out /etc/nginx/servers/direct.conf
fi

View File

@@ -0,0 +1 @@
oneshot

View File

@@ -0,0 +1 @@
/etc/s6-overlay/s6-rc.d/init-nginx/run

View File

@@ -0,0 +1,25 @@
#!/command/with-contenv bashio
# ==============================================================================
# Community Hass.io Add-ons: ESPHome
# Take down the S6 supervision tree when NGINX fails
# ==============================================================================
declare exit_code
readonly exit_code_container=$(</run/s6-linux-init-container-results/exitcode)
readonly exit_code_service="${1}"
readonly exit_code_signal="${2}"
bashio::log.info \
"Service NGINX exited with code ${exit_code_service}" \
"(by signal ${exit_code_signal})"
if [[ "${exit_code_service}" -eq 256 ]]; then
if [[ "${exit_code_container}" -eq 0 ]]; then
echo $((128 + $exit_code_signal)) > /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

View File

@@ -0,0 +1,15 @@
#!/command/with-contenv bashio
# shellcheck shell=bash
# ==============================================================================
# Community Hass.io Add-ons: ESPHome
# Runs the NGINX proxy
# ==============================================================================
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

View File

@@ -0,0 +1 @@
longrun

File diff suppressed because it is too large Load Diff

View File

@@ -24,7 +24,7 @@ from .helpers import (
from .toolchain import find_tool, resolve_tool_path, run_tool
if TYPE_CHECKING:
from esphome.platformio.toolchain import IDEData
from esphome.platformio_api import IDEData
_LOGGER = logging.getLogger(__name__)
@@ -793,11 +793,8 @@ class MemoryAnalyzer:
"""Scan ESPHome source object files to map extern "C" symbols to components.
When no linker map file is available, this uses ``nm`` to scan ``.o`` files
under ``src/`` (including ``src/main.cpp.o`` and everything beneath
``src/esphome/``) and build a symbol-to-component mapping. This catches
``extern "C"`` functions, the ESPHome-generated ``setup()``/``loop()``
entry points in ``main.cpp``, and other symbols that lack C++ namespace
prefixes.
under ``src/esphome/`` and build a symbol-to-component mapping. This catches
``extern "C"`` functions and other symbols that lack C++ namespace prefixes.
Skips scanning if ``_source_symbol_map`` was already populated by
``_parse_map_file()``.
@@ -809,12 +806,12 @@ class MemoryAnalyzer:
if obj_dir is None:
return
# Scan all ESPHome-owned source object files: src/main.cpp.o and src/esphome/...
src_dir = obj_dir / "src"
if not src_dir.is_dir():
# Find ESPHome source object files
esphome_src_dir = obj_dir / "src" / "esphome"
if not esphome_src_dir.is_dir():
return
obj_files = sorted(src_dir.rglob("*.o"))
obj_files = sorted(esphome_src_dir.rglob("*.o"))
if not obj_files:
return
@@ -1067,10 +1064,6 @@ class MemoryAnalyzer:
if component_name in self.external_components:
return f"{_COMPONENT_PREFIX_EXTERNAL}{component_name}"
# ESPHome-generated entry point: src/main.cpp.o (contains setup()/loop())
if len(parts) >= 2 and parts[-2:] == ("src", "main.cpp.o"):
return _COMPONENT_CORE
# ESPHome core: src/esphome/core/... or src/esphome/...
if "core" in parts and "esphome" in parts:
return _COMPONENT_CORE

View File

@@ -6,7 +6,6 @@ from collections import defaultdict
from collections.abc import Callable
import heapq
from operator import itemgetter
from pathlib import Path
import sys
from typing import TYPE_CHECKING
@@ -510,7 +509,7 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
lines.append(
f"{_COMPONENT_CORE} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B ({len(large_core_symbols)} symbols):"
)
for i, (_symbol, demangled, size) in enumerate(large_core_symbols):
for i, (symbol, demangled, size) in enumerate(large_core_symbols):
# Core symbols only track (symbol, demangled, size) without section info,
# so we don't show section labels here
lines.append(
@@ -602,7 +601,7 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
lines.append(
f"{comp_name} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B & storage ({len(large_symbols)} symbols):"
)
for i, (_symbol, demangled, size, section) in enumerate(large_symbols):
for i, (symbol, demangled, size, section) in enumerate(large_symbols):
lines.append(
f"{i + 1}. {self._format_symbol_with_section(demangled, size, section)}"
)
@@ -641,7 +640,7 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
lines.append(
f" Symbols > {self.RAM_SYMBOL_SIZE_THRESHOLD} B ({len(large_ram_syms)}):"
)
for _symbol, demangled, size, section in large_ram_syms[:10]:
for symbol, demangled, size, section in large_ram_syms[:10]:
# Format section label consistently by stripping leading dot
section_label = section.lstrip(".") if section else ""
display_name = _format_pstorage_name(demangled)
@@ -700,7 +699,7 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
content = "\n".join(lines)
if output_file:
with Path(output_file).open("w", encoding="utf-8") as f:
with open(output_file, "w", encoding="utf-8") as f:
f.write(content)
else:
print(content)
@@ -738,8 +737,9 @@ def main():
# Load build directory
import json
from pathlib import Path
from esphome.platformio.toolchain import IDEData
from esphome.platformio_api import IDEData
build_path = Path(build_dir)
@@ -785,7 +785,7 @@ def main():
if not idedata_path.exists():
continue
try:
with idedata_path.open(encoding="utf-8") as f:
with open(idedata_path, encoding="utf-8") as f:
raw_data = json.load(f)
idedata = IDEData(raw_data)
print(f"Loaded idedata from: {idedata_path}", file=sys.stderr)

View File

@@ -154,7 +154,7 @@ def batch_demangle(
failed_count = 0
for original, stripped, prefix, demangled in zip(
symbols, symbols_stripped, symbols_prefixes, demangled_lines, strict=True
symbols, symbols_stripped, symbols_prefixes, demangled_lines
):
# Add back any prefix that was removed
demangled = _restore_symbol_prefix(prefix, stripped, demangled)

View File

@@ -3,6 +3,7 @@
from __future__ import annotations
import logging
import os
from pathlib import Path
import subprocess
from typing import TYPE_CHECKING
@@ -36,7 +37,7 @@ def _find_in_platformio_packages(tool_name: str) -> str | None:
Full path to the tool or None if not found
"""
# Get PlatformIO packages directory
platformio_home = Path("~/.platformio/packages").expanduser()
platformio_home = Path(os.path.expanduser("~/.platformio/packages"))
if not platformio_home.exists():
return None

View File

@@ -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: # noqa: BLE001 # pylint: disable=broad-except
except Exception as exc: # pylint: disable=broad-except
# Capture all exceptions so ``event`` is always set — otherwise a
# crash would hang the waiter forever.
self.exception = exc

View File

@@ -127,7 +127,7 @@ def validate_potentially_or_condition(value):
return validate_condition(value)
DelayAction = cg.esphome_ns.class_("DelayAction", Action)
DelayAction = cg.esphome_ns.class_("DelayAction", Action, cg.Component)
LambdaAction = cg.esphome_ns.class_("LambdaAction", Action)
StatelessLambdaAction = cg.esphome_ns.class_("StatelessLambdaAction", Action)
IfAction = cg.esphome_ns.class_("IfAction", Action)
@@ -396,6 +396,7 @@ async def delay_action_to_code(
args: TemplateArgsType,
) -> MockObj:
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_component(var, {})
template_ = await cg.templatable(config, args, cg.uint32)
cg.add(var.set_delay(template_))
return var

View File

@@ -3,38 +3,23 @@
import json
from pathlib import Path
from esphome.components.esp32 import get_esp32_variant, idf_version
import esphome.config_validation as cv
from esphome.components.esp32 import get_esp32_variant
from esphome.core import CORE
from esphome.helpers import mkdir_p, write_file_if_changed
# 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:
"""Get list of built-in ESP-IDF components from project_description.json.
"""Get list of available ESP-IDF components from project_description.json.
Excludes ``src``, IDF-managed components (``managed_components/``), and
converted PIO libs (``pio_components/``). Returns ``None`` if the build
dir or ``project_description.json`` isn't ready yet.
Returns only internal ESP-IDF components, excluding external/managed
components (from idf_component.yml).
"""
if CORE.build_path is None:
return None
project_desc = Path(CORE.build_path) / "build" / "project_description.json"
if not project_desc.exists():
return None
try:
with project_desc.open(encoding="utf-8") as f:
with open(project_desc, encoding="utf-8") as f:
data = json.load(f)
component_info = data.get("build_component_info", {})
@@ -45,9 +30,9 @@ def get_available_components() -> list[str] | None:
if name == "src":
continue
# Exclude IDF-managed and converted-PIO components (external).
# Exclude managed/external components
comp_dir = info.get("dir", "")
if "managed_components" in comp_dir or "pio_components" in comp_dir:
if "managed_components" in comp_dir:
continue
result.append(name)
@@ -62,161 +47,72 @@ def has_discovered_components() -> bool:
return get_available_components() is not None
def get_project_cmakelists(minimal: bool = False) -> str:
"""Generate the top-level CMakeLists.txt for ESP-IDF project.
When ``minimal`` is true, omit ``ESPHOME_PROJECT_BUILTIN_COMPONENTS``
since ``project_description.json`` may be stale on the first write.
"""
def get_project_cmakelists() -> str:
"""Generate the top-level CMakeLists.txt for ESP-IDF project."""
# Get IDF target from ESP32 variant (e.g., ESP32S3 -> esp32s3)
variant = get_esp32_variant()
idf_target = variant.lower().replace("-", "")
# esp_idf_size 2.x (bundled with IDF >=6.0) made NG the default and
# removed the --ng flag; on 1.x (IDF 5.5) --ng is required to get
# --format=raw because the legacy mode doesn't support it.
size_ng_flag = "--ng" if idf_version() < cv.Version(6, 0, 0) else ""
# Project-wide compile options: -D defines and -W warning flags (skip
# -Wl, linker flags — those go on the src component via
# target_link_options below). Emitted via idf_build_set_property so the
# flags propagate to every IDF component (including managed ones like
# 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,"))
]
# Extract compile definitions from build flags (-DXXX -> XXX)
compile_defs = [flag for flag in CORE.build_flags if flag.startswith("-D")]
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.
#
# Emit via idf_build_set_property (not plain set()) so the value is
# serialised into build_properties.temp.cmake and visible to IDF's
# early requirements-expansion pass (component_get_requirements.cmake
# runs as a separate CMake script invocation that doesn't load the
# project's top-level CMakeLists; without this, ${ESPHOME_PROJECT_
# MANAGED_COMPONENTS} in a converted-lib REQUIRES expands to empty).
from esphome.components.esp32 import get_managed_component_require_names
managed_components_property = "\n".join(
f"idf_build_set_property(ESPHOME_PROJECT_MANAGED_COMPONENTS {name} APPEND)"
for name in get_managed_component_require_names()
)
# Built-in IDF components exposed via our own property (not IDF's
# __COMPONENT_REQUIRES_COMMON, which would append them to every
# component's REQUIRES including real IDF components). Referenced by
# src/CMakeLists and by each converted PIO lib's CMakeLists. Skipped
# on minimal writes because project_description.json may be stale.
builtin_components_property = (
""
if minimal
else "\n".join(
f"idf_build_set_property(ESPHOME_PROJECT_BUILTIN_COMPONENTS {name} APPEND)"
for name in sorted(get_available_components() or [])
)
f'idf_build_set_property(COMPILE_OPTIONS "{compile_def}" APPEND)'
for compile_def in compile_defs
)
return f"""\
# Auto-generated by ESPHome
cmake_minimum_required(VERSION 3.16)
# On Windows, Ninja can fail with:
# "CreateProcess: The parameter is incorrect (is the command line too long?)"
# when compiler/linker command lines exceed the OS length limit.
#
# The following settings force CMake/Ninja to use *response files* (@file.rsp)
# to pass long lists of includes, objects, and other arguments indirectly,
# avoiding command-line length limits and fixing the build failure.
#
# This is especially useful for large ESP-IDF / ESPHome projects with many
# source files or include directories.
set(CMAKE_C_USE_RESPONSE_FILE_FOR_INCLUDES 1)
set(CMAKE_CXX_USE_RESPONSE_FILE_FOR_INCLUDES 1)
set(CMAKE_C_USE_RESPONSE_FILE_FOR_OBJECTS 1)
set(CMAKE_CXX_USE_RESPONSE_FILE_FOR_OBJECTS 1)
set(CMAKE_NINJA_FORCE_RESPONSE_FILE 1)
set(IDF_TARGET {idf_target})
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}
{builtin_components_property}
project({CORE.name})
# Emit raw JSON size data for ESPHome to read post-build.
add_custom_command(
TARGET ${{CMAKE_PROJECT_NAME}}.elf POST_BUILD
COMMAND ${{PYTHON}} -m esp_idf_size {size_ng_flag} --format=raw
-o ${{CMAKE_BINARY_DIR}}/esp_idf_size.json
${{CMAKE_PROJECT_NAME}}.map
WORKING_DIRECTORY ${{CMAKE_BINARY_DIR}}
VERBATIM
)
"""
def get_component_cmakelists() -> str:
"""Generate the main component CMakeLists.txt.
def get_component_cmakelists(minimal: bool = False) -> str:
"""Generate the main component CMakeLists.txt."""
idf_requires = [] if minimal else (get_available_components() or [])
requires_str = " ".join(idf_requires)
REQUIRES pulls in the discovered built-in IDF components via the
project-level variables set in the top-level CMakeLists.
"""
# 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/.
# Extract compile options (-W flags, excluding linker flags)
compile_opts = [
flag
for flag in CORE.build_flags
if flag.startswith("-W") and not flag.startswith("-Wl,")
]
compile_opts_str = "\n ".join(sorted(compile_opts)) if compile_opts else ""
# Extract linker options (-Wl, flags)
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 ""
return f"""\
# Auto-generated by ESPHome
# CONFIGURE_DEPENDS asks CMake to re-check the glob each build so test
# runs that reuse the build dir don't compile stale source paths. It's
# invalid in script mode (cmake -P), which is how IDF's
# component_get_requirements.cmake includes us, so skip it there.
if(CMAKE_SCRIPT_MODE_FILE)
file(GLOB_RECURSE app_sources
"${{CMAKE_CURRENT_SOURCE_DIR}}/*.cpp"
"${{CMAKE_CURRENT_SOURCE_DIR}}/*.c"
"${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.cpp"
"${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.c"
)
else()
file(GLOB_RECURSE app_sources CONFIGURE_DEPENDS
"${{CMAKE_CURRENT_SOURCE_DIR}}/*.cpp"
"${{CMAKE_CURRENT_SOURCE_DIR}}/*.c"
"${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.cpp"
"${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.c"
)
endif()
file(GLOB_RECURSE app_sources
"${{CMAKE_CURRENT_SOURCE_DIR}}/*.cpp"
"${{CMAKE_CURRENT_SOURCE_DIR}}/*.c"
"${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.cpp"
"${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.c"
)
idf_component_register(
SRCS ${{app_sources}}
INCLUDE_DIRS "." "esphome"
REQUIRES ${{ESPHOME_PROJECT_BUILTIN_COMPONENTS}}
REQUIRES {requires_str}
)
# Apply C++ standard
target_compile_features(${{COMPONENT_LIB}} PUBLIC cxx_std_20)
# ESPHome compile options
target_compile_options(${{COMPONENT_LIB}} PUBLIC
{compile_opts_str}
)
# ESPHome linker options
@@ -234,11 +130,11 @@ def write_project(minimal: bool = False) -> None:
# Write top-level CMakeLists.txt
write_file_if_changed(
CORE.relative_build_path("CMakeLists.txt"),
get_project_cmakelists(minimal=minimal),
get_project_cmakelists(),
)
# Write component CMakeLists.txt in src/
write_file_if_changed(
CORE.relative_src_path("CMakeLists.txt"),
get_component_cmakelists(),
get_component_cmakelists(minimal=minimal),
)

View File

@@ -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
from esphome.writer import find_begin_end, update_storage_json
INI_AUTO_GENERATE_BEGIN = "; ========== AUTO GENERATED CODE BEGIN ==========="
INI_AUTO_GENERATE_END = "; =========== AUTO GENERATED CODE END ============"
@@ -33,27 +33,12 @@ 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))
@@ -73,6 +58,7 @@ def get_ini_content():
def write_ini(content):
update_storage_json()
path = CORE.relative_build_path("platformio.ini")
if path.is_file():

View File

@@ -98,13 +98,11 @@ _KNOWN_FILE_EXTENSIONS = frozenset(
)
# Matches !secret references in YAML text. An optional surrounding
# quote pair around the key is allowed and ignored: YAML treats
# ``!secret 'foo'`` and ``!secret foo`` as the same key. This is
# intentionally a simple regex scan rather than a YAML parse — it may
# match inside comments or multi-line strings, which is the conservative
# direction (include more secrets rather than fewer).
_SECRET_RE = re.compile(r"""!secret\s+['"]?([^\s'"]+)""")
# Matches !secret references in YAML text. This is intentionally a simple
# regex scan rather than a YAML parse — it may match inside comments or
# multi-line strings, which is the conservative direction (include more
# secrets rather than fewer).
_SECRET_RE = re.compile(r"!secret\s+(\S+)")
def _find_used_secret_keys(yaml_files: list[Path]) -> set[str]:
@@ -260,20 +258,42 @@ class ConfigBundleCreator:
def _discover_yaml_includes(self) -> None:
"""Discover YAML files loaded during config parsing.
Delegates to :func:`yaml_util.discover_user_yaml_files`, which does a
fresh re-parse and force-loads every deferred ``IncludeFile`` so that
*all* potentially-reachable includes are captured (even branches not
selected by local substitutions). Bundles are meant to be compiled on
another system where command-line substitution overrides may choose a
different branch — e.g. ``!include network/${eth_model}/config.yaml``
must ship every candidate so the remote build can pick any one.
Deliberately uses a fresh re-parse and force-loads every deferred
``IncludeFile`` to include *all* potentially-reachable includes,
even branches not selected by the local substitutions. Bundles are
meant to be compiled on another system where command-line
substitution overrides may choose a different branch — e.g.
``!include network/${eth_model}/config.yaml`` must ship every
candidate so the remote build can pick any one.
Entries with unresolved substitution variables in the filename
path are skipped with a warning (they cannot be resolved without
the substitution pass).
Secrets files are tracked separately so we can filter them to
only include the keys this config actually references.
"""
discovered = yaml_util.discover_user_yaml_files(self._config_path)
self._secrets_paths.update(discovered.secrets)
config_resolved = self._config_path.resolve()
for fpath in discovered.files:
if fpath == config_resolved:
# Must be a fresh parse: IncludeFile.load() caches its result in
# _content, and we discover files by listening for loader calls. On
# an already-parsed tree the cache is populated, .load() returns
# without calling the loader, the listener never fires, and the
# referenced files would be silently dropped from the bundle.
with yaml_util.track_yaml_loads() as loaded_files:
try:
data = yaml_util.load_yaml(self._config_path)
except EsphomeError:
_LOGGER.debug(
"Bundle: re-loading YAML for include discovery failed, "
"proceeding with partial file list"
)
else:
_force_load_include_files(data)
for fpath in loaded_files:
if fpath == self._config_path.resolve():
continue # Already added as config
if fpath.name in const.SECRETS_FILES:
self._secrets_paths.add(fpath)
self._add_file(fpath)
def _discover_component_files(self) -> None:
@@ -412,7 +432,7 @@ class ConfigBundleCreator:
@staticmethod
def _add_to_tar(tar: tarfile.TarFile, bf: BundleFile) -> None:
"""Add a BundleFile to the tar archive with deterministic metadata."""
with bf.source.open("rb") as f:
with open(bf.source, "rb") as f:
_add_bytes_to_tar(tar, bf.path, f.read())
@@ -603,6 +623,57 @@ def _add_bytes_to_tar(tar: tarfile.TarFile, name: str, data: bytes) -> None:
tar.addfile(info, io.BytesIO(data))
def _force_load_include_files(obj: Any, _seen: set[int] | None = None) -> None:
"""Recursively resolve any ``IncludeFile`` instances in a YAML tree.
Nested ``!include`` returns a deferred ``IncludeFile`` that is only
resolved during the substitution pass. During bundle discovery we need
the referenced files to actually load so the ``track_yaml_loads``
listener fires for them.
``IncludeFile`` instances with unresolved substitution variables in the
filename cannot be loaded — we skip and warn about those.
"""
if _seen is None:
_seen = set()
if isinstance(obj, yaml_util.IncludeFile):
if id(obj) in _seen:
return
_seen.add(id(obj))
if obj.has_unresolved_expressions():
_LOGGER.warning(
"Bundle: cannot resolve !include %s (referenced from %s) "
"with substitutions in path",
obj.file,
obj.parent_file,
)
return
try:
loaded = obj.load()
except EsphomeError as err:
_LOGGER.warning(
"Bundle: failed to load !include %s (referenced from %s): %s",
obj.file,
obj.parent_file,
err,
)
return
_force_load_include_files(loaded, _seen)
elif isinstance(obj, dict):
if id(obj) in _seen:
return
_seen.add(id(obj))
for value in obj.values():
_force_load_include_files(value, _seen)
elif isinstance(obj, (list, tuple)):
if id(obj) in _seen:
return
_seen.add(id(obj))
for item in obj:
_force_load_include_files(item, _seen)
def _resolve_include_path(include_path: Any) -> Path | None:
"""Resolve an include path to absolute, skipping system includes."""
if isinstance(include_path, str) and include_path.startswith("<"):

View File

@@ -1,76 +0,0 @@
"""Validated-config cache for the upload/logs fast path.
compile dumps the validated config to <data_dir>/storage/<file>.validated.yaml;
the next upload/logs for that YAML reuses it instead of running the full
read_config pipeline. YAML round-trip (yaml_util.dump/load_yaml) keeps
!lambda/!include/IDs/paths intact; mtime gates staleness.
"""
from __future__ import annotations
import logging
from pathlib import Path
from esphome.core import CORE
from esphome.helpers import write_file
from esphome.storage_json import StorageJSON, ext_storage_path
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
def compiled_config_path(config_filename: str) -> Path:
"""Path to the cached validated config alongside the storage sidecar."""
return CORE.data_dir / "storage" / f"{config_filename}.validated.yaml"
def _cache_is_fresh(cache_path: Path, source_path: Path) -> bool:
"""True iff the cache file exists and isn't older than the source."""
try:
return cache_path.stat().st_mtime >= source_path.stat().st_mtime
except OSError:
return False
def save_compiled_config(config: ConfigType) -> None:
"""Write the validated-config cache. Always-write so mtime stays fresh.
Mode 0600 because show_secrets=True resolves !secret inline.
Failures are non-fatal: the fast path falls back to read_config.
"""
from esphome import yaml_util
try:
rendered = yaml_util.dump(config, show_secrets=True)
write_file(compiled_config_path(CORE.config_filename), rendered, private=True)
except Exception as err: # noqa: BLE001 # pylint: disable=broad-except
_LOGGER.debug("Skipping compiled config cache write: %s", err)
def load_compiled_config(conf_path: Path) -> ConfigType | None:
"""Load the cached validated config and apply storage metadata to CORE.
Returns None (caller falls back to read_config) when the cache is
missing, older than the source YAML, unparseable, or the sidecar
is incomplete.
"""
cache_path = compiled_config_path(conf_path.name)
if not _cache_is_fresh(cache_path, conf_path):
return None
from esphome import yaml_util
try:
config = yaml_util.load_yaml(cache_path, clear_secrets=False)
except Exception: # noqa: BLE001 # pylint: disable=broad-except
return None
storage = StorageJSON.load(ext_storage_path(conf_path.name))
if storage is None:
return None
# apply_to_core assumes a real compile wrote the sidecar; wizard-only
# sidecars leave both of these unset and can't drive upload/logs.
if not storage.core_platform and not storage.target_platform:
return None
storage.apply_to_core()
return config

View File

@@ -4,7 +4,8 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::a01nyub {
namespace esphome {
namespace a01nyub {
static const char *const TAG = "a01nyub.sensor";
@@ -41,4 +42,5 @@ void A01nyubComponent::check_buffer_() {
void A01nyubComponent::dump_config() { LOG_SENSOR("", "A01nyub Sensor", this); }
} // namespace esphome::a01nyub
} // namespace a01nyub
} // namespace esphome

View File

@@ -6,7 +6,8 @@
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/uart/uart.h"
namespace esphome::a01nyub {
namespace esphome {
namespace a01nyub {
class A01nyubComponent : public sensor::Sensor, public Component, public uart::UARTDevice {
public:
@@ -22,4 +23,5 @@ class A01nyubComponent : public sensor::Sensor, public Component, public uart::U
std::vector<uint8_t> buffer_;
};
} // namespace esphome::a01nyub
} // namespace a01nyub
} // namespace esphome

View File

@@ -4,7 +4,8 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::a02yyuw {
namespace esphome {
namespace a02yyuw {
static const char *const TAG = "a02yyuw.sensor";
@@ -40,4 +41,5 @@ void A02yyuwComponent::check_buffer_() {
void A02yyuwComponent::dump_config() { LOG_SENSOR("", "A02yyuw Sensor", this); }
} // namespace esphome::a02yyuw
} // namespace a02yyuw
} // namespace esphome

View File

@@ -6,7 +6,8 @@
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/uart/uart.h"
namespace esphome::a02yyuw {
namespace esphome {
namespace a02yyuw {
class A02yyuwComponent : public sensor::Sensor, public Component, public uart::UARTDevice {
public:
@@ -22,4 +23,5 @@ class A02yyuwComponent : public sensor::Sensor, public Component, public uart::U
std::vector<uint8_t> buffer_;
};
} // namespace esphome::a02yyuw
} // namespace a02yyuw
} // namespace esphome

View File

@@ -1,7 +1,8 @@
#include "a4988.h"
#include "esphome/core/log.h"
namespace esphome::a4988 {
namespace esphome {
namespace a4988 {
static const char *const TAG = "a4988.stepper";
@@ -50,4 +51,5 @@ void A4988::loop() {
this->step_pin_->digital_write(false);
}
} // namespace esphome::a4988
} // namespace a4988
} // namespace esphome

View File

@@ -4,7 +4,8 @@
#include "esphome/core/hal.h"
#include "esphome/components/stepper/stepper.h"
namespace esphome::a4988 {
namespace esphome {
namespace a4988 {
class A4988 : public stepper::Stepper, public Component {
public:
@@ -24,4 +25,5 @@ class A4988 : public stepper::Stepper, public Component {
HighFrequencyLoopRequester high_freq_;
};
} // namespace esphome::a4988
} // namespace a4988
} // namespace esphome

View File

@@ -1,7 +1,8 @@
#include "adalight_light_effect.h"
#include "esphome/core/log.h"
namespace esphome::adalight {
namespace esphome {
namespace adalight {
static const char *const TAG = "adalight_light_effect";
@@ -128,7 +129,7 @@ AdalightLightEffect::Frame AdalightLightEffect::parse_frame_(light::AddressableL
uint8_t *led_data = &frame_[6];
for (int led = 0; led < accepted_led_count; led++, led_data += 3) {
auto white = std::min({led_data[0], led_data[1], led_data[2]});
auto white = std::min(std::min(led_data[0], led_data[1]), led_data[2]);
it[led].set(Color(led_data[0], led_data[1], led_data[2], white));
}
@@ -137,4 +138,5 @@ AdalightLightEffect::Frame AdalightLightEffect::parse_frame_(light::AddressableL
return CONSUMED;
}
} // namespace esphome::adalight
} // namespace adalight
} // namespace esphome

View File

@@ -6,7 +6,8 @@
#include <vector>
namespace esphome::adalight {
namespace esphome {
namespace adalight {
class AdalightLightEffect : public light::AddressableLightEffect, public uart::UARTDevice {
public:
@@ -34,4 +35,5 @@ class AdalightLightEffect : public light::AddressableLightEffect, public uart::U
std::vector<uint8_t> frame_;
};
} // namespace esphome::adalight
} // namespace adalight
} // namespace esphome

View File

@@ -17,7 +17,8 @@
#include <zephyr/drivers/adc.h>
#endif
namespace esphome::adc {
namespace esphome {
namespace adc {
#ifdef USE_ESP32
// clang-format off
@@ -161,4 +162,5 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
#endif
};
} // namespace esphome::adc
} // namespace adc
} // namespace esphome

View File

@@ -1,7 +1,8 @@
#include "adc_sensor.h"
#include "esphome/core/log.h"
namespace esphome::adc {
namespace esphome {
namespace adc {
static const char *const TAG = "adc.common";
@@ -78,4 +79,5 @@ void ADCSensor::set_sample_count(uint8_t sample_count) {
void ADCSensor::set_sampling_mode(SamplingMode sampling_mode) { this->sampling_mode_ = sampling_mode; }
} // namespace esphome::adc
} // namespace adc
} // namespace esphome

View File

@@ -4,7 +4,8 @@
#include "esphome/core/log.h"
#include <cinttypes>
namespace esphome::adc {
namespace esphome {
namespace adc {
static const char *const TAG = "adc.esp32";
@@ -363,6 +364,7 @@ float ADCSensor::sample_autorange_() {
return final_result;
}
} // namespace esphome::adc
} // namespace adc
} // namespace esphome
#endif // USE_ESP32

View File

@@ -11,7 +11,8 @@ ADC_MODE(ADC_VCC)
#include <Arduino.h>
#endif // USE_ADC_SENSOR_VCC
namespace esphome::adc {
namespace esphome {
namespace adc {
static const char *const TAG = "adc.esp8266";
@@ -54,6 +55,7 @@ float ADCSensor::sample() {
return aggr.aggregate() / 1024.0f;
}
} // namespace esphome::adc
} // namespace adc
} // namespace esphome
#endif // USE_ESP8266

View File

@@ -3,7 +3,8 @@
#include "adc_sensor.h"
#include "esphome/core/log.h"
namespace esphome::adc {
namespace esphome {
namespace adc {
static const char *const TAG = "adc.libretiny";
@@ -47,6 +48,7 @@ float ADCSensor::sample() {
return aggr.aggregate() / 1000.0f;
}
} // namespace esphome::adc
} // namespace adc
} // namespace esphome
#endif // USE_LIBRETINY

View File

@@ -15,7 +15,8 @@
#define PICO_VSYS_PIN 29 // NOLINT(cppcoreguidelines-macro-usage)
#endif
namespace esphome::adc {
namespace esphome {
namespace adc {
static const char *const TAG = "adc.rp2040";
@@ -97,6 +98,7 @@ float ADCSensor::sample() {
return aggr.aggregate() * 3.3f / 4096.0f * coeff;
}
} // namespace esphome::adc
} // namespace adc
} // namespace esphome
#endif // USE_RP2040

View File

@@ -5,7 +5,8 @@
#include "hal/nrf_saadc.h"
namespace esphome::adc {
namespace esphome {
namespace adc {
static const char *const TAG = "adc.zephyr";
@@ -201,5 +202,6 @@ float ADCSensor::sample() {
return val_mv / 1000.0f;
}
} // namespace esphome::adc
} // namespace adc
} // namespace esphome
#endif

View File

@@ -1,7 +1,8 @@
#include "adc128s102.h"
#include "esphome/core/log.h"
namespace esphome::adc128s102 {
namespace esphome {
namespace adc128s102 {
static const char *const TAG = "adc128s102";
@@ -27,4 +28,5 @@ uint16_t ADC128S102::read_data(uint8_t channel) {
return digital_value;
}
} // namespace esphome::adc128s102
} // namespace adc128s102
} // namespace esphome

View File

@@ -4,7 +4,8 @@
#include "esphome/core/hal.h"
#include "esphome/components/spi/spi.h"
namespace esphome::adc128s102 {
namespace esphome {
namespace adc128s102 {
class ADC128S102 : public Component,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
@@ -18,4 +19,5 @@ class ADC128S102 : public Component,
uint16_t read_data(uint8_t channel);
};
} // namespace esphome::adc128s102
} // namespace adc128s102
} // namespace esphome

View File

@@ -2,7 +2,8 @@
#include "esphome/core/log.h"
namespace esphome::adc128s102 {
namespace esphome {
namespace adc128s102 {
static const char *const TAG = "adc128s102.sensor";
@@ -17,4 +18,5 @@ void ADC128S102Sensor::dump_config() {
float ADC128S102Sensor::sample() { return this->parent_->read_data(this->channel_); }
void ADC128S102Sensor::update() { this->publish_state(this->sample()); }
} // namespace esphome::adc128s102
} // namespace adc128s102
} // namespace esphome

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