mirror of
https://github.com/esphome/esphome.git
synced 2026-09-04 12:06:01 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a4f67def8 | ||
|
|
5b728f19c3 |
@@ -0,0 +1 @@
|
|||||||
|
27aaab4e0ebfc10491720345aa746fc2dffa6a3985f73ec111b12dd99078d46f
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
ARG BUILD_BASE_VERSION=2026.06.1
|
ARG BUILD_BASE_VERSION=2025.04.0
|
||||||
|
|
||||||
|
|
||||||
FROM ghcr.io/esphome/docker-base:debian-${BUILD_BASE_VERSION} AS base
|
FROM ghcr.io/esphome/docker-base:debian-${BUILD_BASE_VERSION} AS base
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
// uncomment and edit the path in order to pass through local USB serial to the container
|
// uncomment and edit the path in order to pass through local USB serial to the container
|
||||||
// , "--device=/dev/ttyACM0"
|
// , "--device=/dev/ttyACM0"
|
||||||
],
|
],
|
||||||
|
"appPort": 6052,
|
||||||
// if you are using avahi in the host device, uncomment these to allow the
|
// if you are using avahi in the host device, uncomment these to allow the
|
||||||
// devcontainer to find devices via mdns
|
// devcontainer to find devices via mdns
|
||||||
//"mounts": [
|
//"mounts": [
|
||||||
@@ -40,11 +41,7 @@
|
|||||||
],
|
],
|
||||||
"settings": {
|
"settings": {
|
||||||
"python.languageServer": "Pylance",
|
"python.languageServer": "Pylance",
|
||||||
// Use the container's pre-provisioned venv (built by the Dockerfile, outside the
|
"python.pythonPath": "/usr/bin/python3",
|
||||||
// bind-mounted workspace) rather than a ./venv that may leak in from the host and
|
|
||||||
// mismatch the container's Python. See .devcontainer/Dockerfile (esphome-venv).
|
|
||||||
"python.defaultInterpreterPath": "/home/esphome/.local/esphome-venv/bin/python",
|
|
||||||
"python.terminal.activateEnvironment": true,
|
|
||||||
"pylint.args": [
|
"pylint.args": [
|
||||||
"--rcfile=${workspaceFolder}/pyproject.toml"
|
"--rcfile=${workspaceFolder}/pyproject.toml"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
# Normalize line endings to LF in the repository
|
# Normalize line endings to LF in the repository
|
||||||
* text eol=lf
|
* text eol=lf
|
||||||
*.png binary
|
*.png binary
|
||||||
*.gif binary
|
|
||||||
*.apng binary
|
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ inputs:
|
|||||||
description: "Version to build"
|
description: "Version to build"
|
||||||
required: true
|
required: true
|
||||||
example: "2023.12.0"
|
example: "2023.12.0"
|
||||||
|
base_os:
|
||||||
|
description: "Base OS to use"
|
||||||
|
required: false
|
||||||
|
default: "debian"
|
||||||
|
example: "debian"
|
||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
steps:
|
steps:
|
||||||
@@ -42,7 +47,7 @@ runs:
|
|||||||
|
|
||||||
- name: Build and push to ghcr by digest
|
- name: Build and push to ghcr by digest
|
||||||
id: build-ghcr
|
id: build-ghcr
|
||||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
||||||
env:
|
env:
|
||||||
DOCKER_BUILD_SUMMARY: false
|
DOCKER_BUILD_SUMMARY: false
|
||||||
DOCKER_BUILD_RECORD_UPLOAD: false
|
DOCKER_BUILD_RECORD_UPLOAD: false
|
||||||
@@ -55,6 +60,7 @@ runs:
|
|||||||
build-args: |
|
build-args: |
|
||||||
BUILD_TYPE=${{ inputs.build_type }}
|
BUILD_TYPE=${{ inputs.build_type }}
|
||||||
BUILD_VERSION=${{ inputs.version }}
|
BUILD_VERSION=${{ inputs.version }}
|
||||||
|
BUILD_OS=${{ inputs.base_os }}
|
||||||
outputs: |
|
outputs: |
|
||||||
type=image,name=ghcr.io/${{ steps.tags.outputs.image_name }},push-by-digest=true,name-canonical=true,push=true
|
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
|
- name: Build and push to dockerhub by digest
|
||||||
id: build-dockerhub
|
id: build-dockerhub
|
||||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
||||||
env:
|
env:
|
||||||
DOCKER_BUILD_SUMMARY: false
|
DOCKER_BUILD_SUMMARY: false
|
||||||
DOCKER_BUILD_RECORD_UPLOAD: false
|
DOCKER_BUILD_RECORD_UPLOAD: false
|
||||||
@@ -80,6 +86,7 @@ runs:
|
|||||||
build-args: |
|
build-args: |
|
||||||
BUILD_TYPE=${{ inputs.build_type }}
|
BUILD_TYPE=${{ inputs.build_type }}
|
||||||
BUILD_VERSION=${{ inputs.version }}
|
BUILD_VERSION=${{ inputs.version }}
|
||||||
|
BUILD_OS=${{ inputs.base_os }}
|
||||||
outputs: |
|
outputs: |
|
||||||
type=image,name=docker.io/${{ steps.tags.outputs.image_name }},push-by-digest=true,name-canonical=true,push=true
|
type=image,name=docker.io/${{ steps.tags.outputs.image_name }},push-by-digest=true,name-canonical=true,push=true
|
||||||
|
|
||||||
|
|||||||
@@ -1,52 +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 component test batches) shares
|
|
||||||
one cache, since the install is identical (ESPHOME_IDF_DEFAULT_TARGETS
|
|
||||||
defaults to "all", so all toolchains are present regardless of the chip).
|
|
||||||
Callers must set env ESPHOME_ESP_IDF_PREFIX: ~/.esphome-idf and have the
|
|
||||||
Python venv already restored.
|
|
||||||
inputs:
|
|
||||||
framework:
|
|
||||||
description: 'Which pinned IDF version to key on: "espidf" (recommended) or "arduino".'
|
|
||||||
default: espidf
|
|
||||||
restore-only:
|
|
||||||
description: >
|
|
||||||
When "true", only restore -- never save the cache, even on dev. Use from
|
|
||||||
jobs that may not produce an ESP-IDF install (e.g. a component batch with
|
|
||||||
no esp32 target), so a partial/empty install is never written to the key.
|
|
||||||
default: "false"
|
|
||||||
runs:
|
|
||||||
using: composite
|
|
||||||
steps:
|
|
||||||
- name: Resolve ESP-IDF version for cache key
|
|
||||||
# The native-IDF version is pinned in code, not in any file that feeds the
|
|
||||||
# other cache keys, so resolve it explicitly. Keying on it means the cache
|
|
||||||
# invalidates on a version bump (actions/cache never overwrites a key).
|
|
||||||
id: version
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
. venv/bin/activate
|
|
||||||
if [ "${{ inputs.framework }}" = "arduino" ]; then
|
|
||||||
version=$(python -c 'from esphome.components.esp32 import ARDUINO_FRAMEWORK_VERSION_LOOKUP as A, ARDUINO_IDF_VERSION_LOOKUP as L; print(L[A["recommended"]])')
|
|
||||||
else
|
|
||||||
version=$(python -c 'from esphome.components.esp32 import ESP_IDF_FRAMEWORK_VERSION_LOOKUP as L; print(L["recommended"])')
|
|
||||||
fi
|
|
||||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
|
||||||
# Mirror the adjacent PlatformIO cache: only dev-branch runs write the
|
|
||||||
# shared cache (so it lives in the default-branch scope readable by all
|
|
||||||
# PRs), and PRs are restore-only -- they never push multi-GB artifacts into
|
|
||||||
# their own scope / the repo quota (e.g. on a version-bump PR).
|
|
||||||
- name: Cache ESP-IDF install (write on dev)
|
|
||||||
if: github.ref == 'refs/heads/dev' && inputs.restore-only != 'true'
|
|
||||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
|
||||||
with:
|
|
||||||
path: ~/.esphome-idf
|
|
||||||
key: ${{ runner.os }}-esphome-idf-${{ steps.version.outputs.version }}
|
|
||||||
- name: Cache ESP-IDF install (restore-only off dev)
|
|
||||||
if: github.ref != 'refs/heads/dev' || inputs.restore-only == 'true'
|
|
||||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
|
||||||
with:
|
|
||||||
path: ~/.esphome-idf
|
|
||||||
key: ${{ runner.os }}-esphome-idf-${{ steps.version.outputs.version }}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
name: Cache nRF Connect SDK
|
|
||||||
description: >
|
|
||||||
Resolve the pinned sdk-nrf version and cache the native sdk-nrf install
|
|
||||||
(west workspace, Zephyr SDK toolchain, python env) at ~/.esphome-sdk-nrf.
|
|
||||||
Every job that installs sdk-nrf natively (the nrf52 clang-tidy job and,
|
|
||||||
once the component tests build natively, their batches) shares one cache.
|
|
||||||
Callers must set env ESPHOME_SDK_NRF_PREFIX: ~/.esphome-sdk-nrf and have
|
|
||||||
the Python venv already restored.
|
|
||||||
inputs:
|
|
||||||
restore-only:
|
|
||||||
description: >
|
|
||||||
When "true", only restore -- never save the cache, even on dev. Use from
|
|
||||||
jobs that may not produce a complete install (e.g. a component batch
|
|
||||||
that fails mid-install), so a partial install is never written.
|
|
||||||
default: "false"
|
|
||||||
runs:
|
|
||||||
using: composite
|
|
||||||
steps:
|
|
||||||
- name: Resolve sdk-nrf and toolchain versions for cache key
|
|
||||||
# Both versions are pinned in code, not in any file that feeds the
|
|
||||||
# other cache keys, so resolve them explicitly. Keying on them means
|
|
||||||
# the cache invalidates when either is bumped (actions/cache never
|
|
||||||
# overwrites a key).
|
|
||||||
id: version
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
. venv/bin/activate
|
|
||||||
version=$(python -c '
|
|
||||||
from esphome.components.nrf52 import RECOMMENDED_SDK_NRF_VERSION
|
|
||||||
from esphome.components.nrf52.framework import TOOLCHAIN_VERSION
|
|
||||||
print(f"{RECOMMENDED_SDK_NRF_VERSION}-{TOOLCHAIN_VERSION}")')
|
|
||||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
|
||||||
# Mirror cache-esp-idf: only dev-branch runs write the shared cache (so it
|
|
||||||
# lives in the default-branch scope readable by all PRs); PRs are
|
|
||||||
# restore-only and never push multi-GB artifacts into their own scope.
|
|
||||||
- name: Cache nRF Connect SDK install (write on dev)
|
|
||||||
if: github.ref == 'refs/heads/dev' && inputs.restore-only != 'true'
|
|
||||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
||||||
with:
|
|
||||||
path: ~/.esphome-sdk-nrf
|
|
||||||
# yamllint disable-line rule:line-length
|
|
||||||
key: ${{ runner.os }}-esphome-sdk-nrf-${{ steps.version.outputs.version }}-${{ hashFiles('esphome/components/nrf52/requirements.txt') }}
|
|
||||||
- name: Cache nRF Connect SDK install (restore-only off dev)
|
|
||||||
if: github.ref != 'refs/heads/dev' || inputs.restore-only == 'true'
|
|
||||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
||||||
with:
|
|
||||||
path: ~/.esphome-sdk-nrf
|
|
||||||
# yamllint disable-line rule:line-length
|
|
||||||
key: ${{ runner.os }}-esphome-sdk-nrf-${{ steps.version.outputs.version }}-${{ hashFiles('esphome/components/nrf52/requirements.txt') }}
|
|
||||||
@@ -17,12 +17,12 @@ runs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Set up Python ${{ inputs.python-version }}
|
- name: Set up Python ${{ inputs.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ inputs.python-version }}
|
python-version: ${{ inputs.python-version }}
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
# yamllint disable-line rule:line-length
|
# yamllint disable-line rule:line-length
|
||||||
@@ -32,12 +32,9 @@ runs:
|
|||||||
# detects the activated venv via ``VIRTUAL_ENV`` so the venv layout
|
# detects the activated venv via ``VIRTUAL_ENV`` so the venv layout
|
||||||
# downstream jobs rely on is preserved.
|
# downstream jobs rely on is preserved.
|
||||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
# Pull request saves land in per-PR scopes nothing else can
|
|
||||||
# reuse; dev pushes seed the shared copy instead.
|
|
||||||
save-cache: ${{ github.event_name != 'pull_request' }}
|
|
||||||
# Pin uv version so the action does not have to fetch the
|
# Pin uv version so the action does not have to fetch the
|
||||||
# manifest from raw.githubusercontent.com on every cache
|
# manifest from raw.githubusercontent.com on every cache
|
||||||
# miss; that fetch flakes on Windows runners.
|
# miss; that fetch flakes on Windows runners.
|
||||||
|
|||||||
@@ -147,9 +147,19 @@ async function detectCoreChanges(changedFiles) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Strategy: PR size detection
|
// Strategy: PR size detection
|
||||||
async function detectPRSize(prFiles, totalAdditions, totalDeletions, isMegaPR, SMALL_PR_THRESHOLD, MEDIUM_PR_THRESHOLD, TOO_BIG_THRESHOLD) {
|
async function detectPRSize(prFiles, totalAdditions, totalDeletions, totalChanges, isMegaPR, SMALL_PR_THRESHOLD, MEDIUM_PR_THRESHOLD, TOO_BIG_THRESHOLD) {
|
||||||
const labels = new Set();
|
const labels = new Set();
|
||||||
|
|
||||||
|
if (totalChanges <= SMALL_PR_THRESHOLD) {
|
||||||
|
labels.add('small-pr');
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalChanges <= MEDIUM_PR_THRESHOLD) {
|
||||||
|
labels.add('medium-pr');
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
|
||||||
const testAdditions = prFiles
|
const testAdditions = prFiles
|
||||||
.filter(file => file.filename.startsWith('tests/'))
|
.filter(file => file.filename.startsWith('tests/'))
|
||||||
.reduce((sum, file) => sum + (file.additions || 0), 0);
|
.reduce((sum, file) => sum + (file.additions || 0), 0);
|
||||||
@@ -157,24 +167,7 @@ async function detectPRSize(prFiles, totalAdditions, totalDeletions, isMegaPR, S
|
|||||||
.filter(file => file.filename.startsWith('tests/'))
|
.filter(file => file.filename.startsWith('tests/'))
|
||||||
.reduce((sum, file) => sum + (file.deletions || 0), 0);
|
.reduce((sum, file) => sum + (file.deletions || 0), 0);
|
||||||
|
|
||||||
const nonTestAdditions = totalAdditions - testAdditions;
|
const nonTestChanges = (totalAdditions - testAdditions) - (totalDeletions - testDeletions);
|
||||||
const nonTestDeletions = totalDeletions - testDeletions;
|
|
||||||
|
|
||||||
// small/medium count churn (additions + deletions) so a balanced refactor isn't undersized.
|
|
||||||
const nonTestChurn = nonTestAdditions + nonTestDeletions;
|
|
||||||
|
|
||||||
if (nonTestChurn <= SMALL_PR_THRESHOLD) {
|
|
||||||
labels.add('small-pr');
|
|
||||||
return labels;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nonTestChurn <= MEDIUM_PR_THRESHOLD) {
|
|
||||||
labels.add('medium-pr');
|
|
||||||
return labels;
|
|
||||||
}
|
|
||||||
|
|
||||||
// too-big uses net line delta (additions - deletions), matching the review message in reviews.js.
|
|
||||||
const nonTestChanges = nonTestAdditions - nonTestDeletions;
|
|
||||||
|
|
||||||
// Don't add too-big if mega-pr label is already present
|
// Don't add too-big if mega-pr label is already present
|
||||||
if (nonTestChanges > TOO_BIG_THRESHOLD && !isMegaPR) {
|
if (nonTestChanges > TOO_BIG_THRESHOLD && !isMegaPR) {
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ module.exports = async ({ github, context }) => {
|
|||||||
detectNewComponents(github, context, prFiles),
|
detectNewComponents(github, context, prFiles),
|
||||||
detectNewPlatforms(github, context, prFiles, apiData),
|
detectNewPlatforms(github, context, prFiles, apiData),
|
||||||
detectCoreChanges(changedFiles),
|
detectCoreChanges(changedFiles),
|
||||||
detectPRSize(prFiles, totalAdditions, totalDeletions, isMegaPR, SMALL_PR_THRESHOLD, MEDIUM_PR_THRESHOLD, TOO_BIG_THRESHOLD),
|
detectPRSize(prFiles, totalAdditions, totalDeletions, totalChanges, isMegaPR, SMALL_PR_THRESHOLD, MEDIUM_PR_THRESHOLD, TOO_BIG_THRESHOLD),
|
||||||
detectDashboardChanges(changedFiles),
|
detectDashboardChanges(changedFiles),
|
||||||
detectGitHubActionsChanges(changedFiles),
|
detectGitHubActionsChanges(changedFiles),
|
||||||
detectCodeOwner(github, context, changedFiles),
|
detectCodeOwner(github, context, changedFiles),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const { describe, it } = require('node:test');
|
const { describe, it } = require('node:test');
|
||||||
const assert = require('node:assert/strict');
|
const assert = require('node:assert/strict');
|
||||||
const { detectNewPlatforms, detectNewComponents, detectPRSize } = require('../detectors');
|
const { detectNewPlatforms, detectNewComponents } = require('../detectors');
|
||||||
|
|
||||||
// Minimal GitHub API mock — only repos.getContent is called by detectNewPlatforms/detectNewComponents
|
// Minimal GitHub API mock — only repos.getContent is called by detectNewPlatforms/detectNewComponents
|
||||||
// to check for CONFIG_SCHEMA in newly added files.
|
// to check for CONFIG_SCHEMA in newly added files.
|
||||||
@@ -145,79 +145,3 @@ describe('detectNewComponents', () => {
|
|||||||
assert.equal(result.labels.size, 0);
|
assert.equal(result.labels.size, 0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// detectPRSize
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
describe('detectPRSize', () => {
|
|
||||||
const SMALL = 30;
|
|
||||||
const MEDIUM = 100;
|
|
||||||
const TOO_BIG = 1000;
|
|
||||||
|
|
||||||
function size(prFiles, isMegaPR = false) {
|
|
||||||
const totalAdditions = prFiles.reduce((sum, file) => sum + (file.additions || 0), 0);
|
|
||||||
const totalDeletions = prFiles.reduce((sum, file) => sum + (file.deletions || 0), 0);
|
|
||||||
return detectPRSize(prFiles, totalAdditions, totalDeletions, isMegaPR, SMALL, MEDIUM, TOO_BIG);
|
|
||||||
}
|
|
||||||
|
|
||||||
it('counts only non-test changes toward small-pr', async () => {
|
|
||||||
// 10 source + 5000 test lines -> non-test churn of 10 is still small.
|
|
||||||
const labels = await size([
|
|
||||||
{ filename: 'esphome/components/foo/foo.cpp', additions: 10, deletions: 0 },
|
|
||||||
{ filename: 'tests/components/foo/test.esp32-idf.yaml', additions: 5000, deletions: 0 },
|
|
||||||
]);
|
|
||||||
assert.ok(labels.has('small-pr'));
|
|
||||||
assert.equal(labels.size, 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('counts additions and deletions as churn (not net delta)', async () => {
|
|
||||||
// A balanced refactor (40 added, 40 removed) is 80 lines of churn -> medium, not small.
|
|
||||||
const labels = await size([
|
|
||||||
{ filename: 'esphome/components/foo/foo.cpp', additions: 40, deletions: 40 },
|
|
||||||
]);
|
|
||||||
assert.ok(labels.has('medium-pr'));
|
|
||||||
assert.equal(labels.size, 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('labels medium-pr when non-test changes exceed small threshold', async () => {
|
|
||||||
const labels = await size([
|
|
||||||
{ filename: 'esphome/components/foo/foo.cpp', additions: 60, deletions: 0 },
|
|
||||||
{ filename: 'tests/components/foo/test.esp32-idf.yaml', additions: 5000, deletions: 0 },
|
|
||||||
]);
|
|
||||||
assert.ok(labels.has('medium-pr'));
|
|
||||||
assert.equal(labels.size, 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('uses net delta (not churn) for too-big', async () => {
|
|
||||||
// 600 added + 600 removed: 1200 churn (above too-big) but 0 net delta -> not too-big.
|
|
||||||
const labels = await size([
|
|
||||||
{ filename: 'esphome/components/foo/foo.cpp', additions: 600, deletions: 600 },
|
|
||||||
]);
|
|
||||||
assert.equal(labels.size, 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('labels too-big when non-test changes exceed the big threshold', async () => {
|
|
||||||
const labels = await size([
|
|
||||||
{ filename: 'esphome/components/foo/foo.cpp', additions: 2000, deletions: 0 },
|
|
||||||
{ filename: 'tests/components/foo/test.esp32-idf.yaml', additions: 5000, deletions: 0 },
|
|
||||||
]);
|
|
||||||
assert.ok(labels.has('too-big'));
|
|
||||||
assert.equal(labels.size, 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not label too-big when mega-pr is set', async () => {
|
|
||||||
const labels = await size([
|
|
||||||
{ filename: 'esphome/components/foo/foo.cpp', additions: 2000, deletions: 0 },
|
|
||||||
], true);
|
|
||||||
assert.equal(labels.size, 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('produces no size label for a large mega-pr in the gap above medium', async () => {
|
|
||||||
// Non-test changes land between MEDIUM and TOO_BIG: not small/medium, and mega-pr suppresses too-big.
|
|
||||||
const labels = await size([
|
|
||||||
{ filename: 'esphome/components/foo/foo.cpp', additions: 500, deletions: 0 },
|
|
||||||
], true);
|
|
||||||
assert.equal(labels.size, 0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ function hasCoreChanges(changedFiles) {
|
|||||||
*/
|
*/
|
||||||
function hasDashboardChanges(changedFiles) {
|
function hasDashboardChanges(changedFiles) {
|
||||||
return changedFiles.some(file =>
|
return changedFiles.some(file =>
|
||||||
|
file.startsWith('esphome/dashboard/') ||
|
||||||
file.startsWith('esphome/components/dashboard_import/')
|
file.startsWith('esphome/components/dashboard_import/')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
if: github.event.pull_request.state == 'open' && (github.event.action != 'labeled' || github.event.sender.type != 'Bot')
|
if: github.event.pull_request.state == 'open' && (github.event.action != 'labeled' || github.event.sender.type != 'Bot')
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Generate a token
|
- name: Generate a token
|
||||||
id: generate-token
|
id: generate-token
|
||||||
|
|||||||
@@ -21,20 +21,17 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.11"
|
||||||
- name: Set up uv
|
- name: Set up uv
|
||||||
# ``--system`` (below) installs into the setup-python interpreter;
|
# ``--system`` (below) installs into the setup-python interpreter;
|
||||||
# no venv is created or restored by this workflow.
|
# no venv is created or restored by this workflow.
|
||||||
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
# Pull-request-only workflow: a save could never be shared and
|
|
||||||
# would only consume quota.
|
|
||||||
save-cache: "false"
|
|
||||||
# Pin uv version so the action does not have to fetch the
|
# Pin uv version so the action does not have to fetch the
|
||||||
# manifest from raw.githubusercontent.com on every cache
|
# manifest from raw.githubusercontent.com on every cache
|
||||||
# miss; that fetch flakes on Windows runners.
|
# miss; that fetch flakes on Windows runners.
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
name: Clang-tidy Hash CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- ".clang-tidy"
|
||||||
|
- "platformio.ini"
|
||||||
|
- "requirements_dev.txt"
|
||||||
|
- "sdkconfig.defaults"
|
||||||
|
- ".clang-tidy.hash"
|
||||||
|
- "script/clang_tidy_hash.py"
|
||||||
|
- ".github/workflows/ci-clang-tidy-hash.yml"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read # actions/checkout for the PR head
|
||||||
|
pull-requests: write # pulls.createReview / listReviews / dismissReview when the clang-tidy hash is out of date
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
verify-hash:
|
||||||
|
name: Verify clang-tidy hash
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
|
with:
|
||||||
|
python-version: "3.11"
|
||||||
|
|
||||||
|
- name: Verify hash
|
||||||
|
run: |
|
||||||
|
python script/clang_tidy_hash.py --verify
|
||||||
|
|
||||||
|
- if: failure()
|
||||||
|
name: Show hash details
|
||||||
|
run: |
|
||||||
|
python script/clang_tidy_hash.py
|
||||||
|
echo "## Job Failed" | tee -a $GITHUB_STEP_SUMMARY
|
||||||
|
echo "You have modified clang-tidy configuration but have not updated the hash." | tee -a $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Please run 'script/clang_tidy_hash.py --update' and commit the changes." | tee -a $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
- if: failure() && github.event.pull_request.head.repo.full_name == github.repository
|
||||||
|
name: Request changes
|
||||||
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
await github.rest.pulls.createReview({
|
||||||
|
pull_number: context.issue.number,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
event: 'REQUEST_CHANGES',
|
||||||
|
body: 'You have modified clang-tidy configuration but have not updated the hash.\nPlease run `script/clang_tidy_hash.py --update` and commit the changes.'
|
||||||
|
})
|
||||||
|
|
||||||
|
- if: success() && github.event.pull_request.head.repo.full_name == github.repository
|
||||||
|
name: Dismiss review
|
||||||
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
let reviews = await github.rest.pulls.listReviews({
|
||||||
|
pull_number: context.issue.number,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo
|
||||||
|
});
|
||||||
|
for (let review of reviews.data) {
|
||||||
|
if (review.user.login === 'github-actions[bot]' && review.state === 'CHANGES_REQUESTED') {
|
||||||
|
await github.rest.pulls.dismissReview({
|
||||||
|
pull_number: context.issue.number,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
review_id: review.id,
|
||||||
|
message: 'Clang-tidy hash now matches configuration.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
-173
@@ -1,41 +1,28 @@
|
|||||||
---
|
---
|
||||||
name: CI for docker images
|
name: CI for docker images
|
||||||
|
|
||||||
# Only run on PRs that touch the docker image, its build inputs, or any code
|
# Only run when docker paths change
|
||||||
# whose toolchain the compile smoke test exercises (core + target platforms).
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
push:
|
||||||
|
branches: [dev, beta, release]
|
||||||
|
paths:
|
||||||
|
- "docker/**"
|
||||||
|
- ".github/workflows/ci-docker.yml"
|
||||||
|
- "requirements*.txt"
|
||||||
|
- "platformio.ini"
|
||||||
|
- "script/platformio_install_deps.py"
|
||||||
|
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
# Docker image and its build inputs.
|
|
||||||
- "docker/**"
|
- "docker/**"
|
||||||
- ".github/workflows/ci-docker.yml"
|
- ".github/workflows/ci-docker.yml"
|
||||||
- "requirements*.txt"
|
- "requirements*.txt"
|
||||||
- "pyproject.toml"
|
|
||||||
- "platformio.ini"
|
- "platformio.ini"
|
||||||
- "esphome/idf_component.yml"
|
|
||||||
- "script/platformio_install_deps.py"
|
- "script/platformio_install_deps.py"
|
||||||
# Core, build pipeline, toolchain, and target-platform changes can change
|
|
||||||
# how a toolchain is set up or built, so re-run the per-toolchain compile
|
|
||||||
# smoke test when they change.
|
|
||||||
- "esphome/core/**"
|
|
||||||
- "esphome/writer.py"
|
|
||||||
- "esphome/build_gen/**"
|
|
||||||
- "esphome/espidf/**"
|
|
||||||
- "esphome/platformio/**"
|
|
||||||
- "esphome/components/bk72xx/**"
|
|
||||||
- "esphome/components/esp32/**"
|
|
||||||
- "esphome/components/esp8266/**"
|
|
||||||
- "esphome/components/host/**"
|
|
||||||
- "esphome/components/libretiny/**"
|
|
||||||
- "esphome/components/ln882x/**"
|
|
||||||
- "esphome/components/nrf52/**"
|
|
||||||
- "esphome/components/rp2040/**"
|
|
||||||
- "esphome/components/rtl87xx/**"
|
|
||||||
- "esphome/components/zephyr/**"
|
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read # actions/checkout only
|
contents: read # actions/checkout only; the build does not push images
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
# yamllint disable-line rule:line-length
|
# yamllint disable-line rule:line-length
|
||||||
@@ -46,9 +33,6 @@ jobs:
|
|||||||
check-docker:
|
check-docker:
|
||||||
name: Build docker containers
|
name: Build docker containers
|
||||||
runs-on: ${{ matrix.os }}
|
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:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -57,161 +41,23 @@ jobs:
|
|||||||
- "ha-addon"
|
- "ha-addon"
|
||||||
- "docker"
|
- "docker"
|
||||||
# - "lint"
|
# - "lint"
|
||||||
outputs:
|
|
||||||
tag: ${{ steps.tag.outputs.tag }}
|
|
||||||
push: ${{ steps.tag.outputs.push }}
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.11"
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||||
|
|
||||||
- name: Determine tag and whether to push
|
- name: Set TAG
|
||||||
id: tag
|
|
||||||
run: |
|
run: |
|
||||||
# Sanitize the branch name into a valid docker tag: replace invalid
|
echo "TAG=check" >> $GITHUB_ENV
|
||||||
# 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@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Run build
|
- name: Run build
|
||||||
run: |
|
run: |
|
||||||
docker/build.py \
|
docker/build.py \
|
||||||
--tag "${{ steps.tag.outputs.tag }}" \
|
--tag "${TAG}" \
|
||||||
--arch "${{ matrix.os == 'ubuntu-24.04-arm' && 'aarch64' || 'amd64' }}" \
|
--arch "${{ matrix.os == 'ubuntu-24.04-arm' && 'aarch64' || 'amd64' }}" \
|
||||||
--build-type "${{ matrix.build_type }}" \
|
--build-type "${{ matrix.build_type }}" \
|
||||||
--registry ghcr \
|
build
|
||||||
build ${{ steps.tag.outputs.push == 'true' && '--push --no-cache-to' || '' }} ${{ (matrix.os == 'ubuntu-24.04' && matrix.build_type == 'docker') && '--load' || '' }}
|
|
||||||
|
|
||||||
# The amd64 "docker" image is also loaded locally (above) and handed to
|
|
||||||
# compile-test as an artifact, so the smoke test reuses this build instead
|
|
||||||
# of building the image a second time. Using an artifact (rather than the
|
|
||||||
# pushed image) keeps it working for fork PRs, which never push to ghcr.io.
|
|
||||||
- name: Export image for compile-test
|
|
||||||
if: matrix.os == 'ubuntu-24.04' && matrix.build_type == 'docker'
|
|
||||||
run: docker save "ghcr.io/esphome/esphome-amd64:${{ steps.tag.outputs.tag }}" | gzip > compile-test-image.tar.gz
|
|
||||||
|
|
||||||
- name: Upload compile-test image artifact
|
|
||||||
if: matrix.os == 'ubuntu-24.04' && matrix.build_type == 'docker'
|
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
||||||
with:
|
|
||||||
# The tar is already gzipped, so upload it as-is. archive: false skips
|
|
||||||
# the redundant zip and makes the file name the artifact name (the
|
|
||||||
# `name` input is ignored in that mode).
|
|
||||||
path: compile-test-image.tar.gz
|
|
||||||
retention-days: 1
|
|
||||||
archive: false
|
|
||||||
|
|
||||||
manifest:
|
|
||||||
name: Push ${{ matrix.build_type }} manifest to ghcr.io
|
|
||||||
needs: [check-docker]
|
|
||||||
if: needs.check-docker.outputs.push == 'true'
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
permissions:
|
|
||||||
contents: read # actions/checkout to run docker/build.py
|
|
||||||
packages: write # buildx imagetools writes the multi-arch tag to ghcr.io
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
build_type:
|
|
||||||
- "ha-addon"
|
|
||||||
- "docker"
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
|
||||||
with:
|
|
||||||
python-version: "3.12"
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
|
||||||
|
|
||||||
- name: Log in to the GitHub container registry
|
|
||||||
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Create and push manifest
|
|
||||||
run: |
|
|
||||||
docker/build.py \
|
|
||||||
--tag "${{ needs.check-docker.outputs.tag }}" \
|
|
||||||
--build-type "${{ matrix.build_type }}" \
|
|
||||||
--registry ghcr \
|
|
||||||
manifest
|
|
||||||
|
|
||||||
# Smoke-test the built image by compiling one minimal config per target
|
|
||||||
# platform / toolchain. This catches missing system dependencies in the image
|
|
||||||
# that only surface when a given toolchain is downloaded and run. The image is
|
|
||||||
# the amd64 "docker" build produced by check-docker (shared as an artifact).
|
|
||||||
compile-test:
|
|
||||||
name: Compile ${{ matrix.id }}
|
|
||||||
needs: check-docker
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
permissions:
|
|
||||||
contents: read # actions/checkout to load the test configs
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
# Cap concurrency so this smoke test doesn't hog all the shared runners.
|
|
||||||
max-parallel: 2
|
|
||||||
matrix:
|
|
||||||
# One entry per distinct toolchain. ESP32 variants (c3/c6/s2/s3/p4)
|
|
||||||
# share a toolchain bundle, so esp32 is exercised on the base variant
|
|
||||||
# across the full framework x toolchain cross-product (arduino/esp-idf
|
|
||||||
# framework, each built with the platformio and native esp-idf
|
|
||||||
# toolchains) so both toolchains stay covered regardless of which one is
|
|
||||||
# the default.
|
|
||||||
id:
|
|
||||||
- esp8266-arduino
|
|
||||||
- esp32-arduino-platformio
|
|
||||||
- esp32-arduino-esp-idf
|
|
||||||
- esp32-idf-platformio
|
|
||||||
- esp32-idf-esp-idf
|
|
||||||
- rp2040-arduino
|
|
||||||
- bk72xx-arduino
|
|
||||||
- rtl87xx-arduino
|
|
||||||
- ln882x-arduino
|
|
||||||
- nrf52
|
|
||||||
- host
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
||||||
- name: Download image artifact
|
|
||||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
||||||
with:
|
|
||||||
name: compile-test-image.tar.gz
|
|
||||||
- name: Load image
|
|
||||||
run: docker load --input compile-test-image.tar.gz
|
|
||||||
- name: Compile ${{ matrix.id }}
|
|
||||||
run: |
|
|
||||||
docker run --rm \
|
|
||||||
-v "${{ github.workspace }}/docker/test_configs:/config" \
|
|
||||||
"ghcr.io/esphome/esphome-amd64:${{ needs.check-docker.outputs.tag }}" \
|
|
||||||
compile "${{ matrix.id }}.yaml"
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
working-directory: .github/scripts/auto-label-pr
|
working-directory: .github/scripts/auto-label-pr
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Check out code from base repository
|
- name: Check out code from base repository
|
||||||
if: steps.pr.outputs.skip != 'true'
|
if: steps.pr.outputs.skip != 'true'
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
# Always check out from the base repository (esphome/esphome), never from forks
|
# 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
|
# Use the PR's target branch to ensure we run trusted code from the main repo
|
||||||
@@ -60,7 +60,7 @@ jobs:
|
|||||||
if: steps.pr.outputs.skip != 'true'
|
if: steps.pr.outputs.skip != 'true'
|
||||||
uses: ./.github/actions/restore-python
|
uses: ./.github/actions/restore-python
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.11"
|
||||||
cache-key: ${{ hashFiles('.cache-key') }}
|
cache-key: ${{ hashFiles('.cache-key') }}
|
||||||
|
|
||||||
- name: Download memory analysis artifacts
|
- name: Download memory analysis artifacts
|
||||||
|
|||||||
+197
-319
@@ -12,8 +12,8 @@ permissions:
|
|||||||
contents: read # actions/checkout for all jobs; individual jobs add their own scopes when they need to write
|
contents: read # actions/checkout for all jobs; individual jobs add their own scopes when they need to write
|
||||||
|
|
||||||
env:
|
env:
|
||||||
DEFAULT_PYTHON: "3.12"
|
DEFAULT_PYTHON: "3.11"
|
||||||
PYUPGRADE_TARGET: "--py312-plus"
|
PYUPGRADE_TARGET: "--py311-plus"
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
# yamllint disable-line rule:line-length
|
# yamllint disable-line rule:line-length
|
||||||
@@ -28,18 +28,18 @@ jobs:
|
|||||||
cache-key: ${{ steps.cache-key.outputs.key }}
|
cache-key: ${{ steps.cache-key.outputs.key }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Generate cache-key
|
- name: Generate cache-key
|
||||||
id: cache-key
|
id: cache-key
|
||||||
run: echo key="${{ hashFiles('requirements.txt', 'requirements_dev.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
|
run: echo key="${{ hashFiles('requirements.txt', 'requirements_dev.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
# yamllint disable-line rule:line-length
|
# yamllint disable-line rule:line-length
|
||||||
@@ -49,12 +49,9 @@ jobs:
|
|||||||
# detects the activated venv via ``VIRTUAL_ENV`` so downstream jobs
|
# detects the activated venv via ``VIRTUAL_ENV`` so downstream jobs
|
||||||
# that ``. venv/bin/activate`` see an identical layout.
|
# that ``. venv/bin/activate`` see an identical layout.
|
||||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
# Pull request saves land in per-PR scopes nothing else can
|
|
||||||
# reuse; dev pushes seed the shared copy instead.
|
|
||||||
save-cache: ${{ github.event_name != 'pull_request' }}
|
|
||||||
# Pin uv version so the action does not have to fetch the
|
# Pin uv version so the action does not have to fetch the
|
||||||
# manifest from raw.githubusercontent.com on every cache
|
# manifest from raw.githubusercontent.com on every cache
|
||||||
# miss; that fetch flakes on Windows runners.
|
# miss; that fetch flakes on Windows runners.
|
||||||
@@ -77,7 +74,7 @@ jobs:
|
|||||||
if: needs.determine-jobs.outputs.python-linters == 'true'
|
if: needs.determine-jobs.outputs.python-linters == 'true'
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Restore Python
|
- name: Restore Python
|
||||||
uses: ./.github/actions/restore-python
|
uses: ./.github/actions/restore-python
|
||||||
with:
|
with:
|
||||||
@@ -100,7 +97,7 @@ jobs:
|
|||||||
if: needs.determine-jobs.outputs.core-ci == 'true'
|
if: needs.determine-jobs.outputs.core-ci == 'true'
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Restore Python
|
- name: Restore Python
|
||||||
uses: ./.github/actions/restore-python
|
uses: ./.github/actions/restore-python
|
||||||
with:
|
with:
|
||||||
@@ -115,8 +112,7 @@ jobs:
|
|||||||
script/build_codeowners.py --check
|
script/build_codeowners.py --check
|
||||||
script/build_language_schema.py --check
|
script/build_language_schema.py --check
|
||||||
script/generate-esp32-boards.py --check
|
script/generate-esp32-boards.py --check
|
||||||
script/generate-rp2-boards.py --check
|
script/generate-rp2040-boards.py --check
|
||||||
script/ci_check_duplicate_test_ids.py
|
|
||||||
|
|
||||||
import-time:
|
import-time:
|
||||||
name: Check import esphome.__main__ time
|
name: Check import esphome.__main__ time
|
||||||
@@ -127,7 +123,7 @@ jobs:
|
|||||||
if: needs.determine-jobs.outputs.import-time == 'true'
|
if: needs.determine-jobs.outputs.import-time == 'true'
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Restore Python
|
- name: Restore Python
|
||||||
uses: ./.github/actions/restore-python
|
uses: ./.github/actions/restore-python
|
||||||
with:
|
with:
|
||||||
@@ -155,17 +151,17 @@ jobs:
|
|||||||
if: needs.determine-jobs.outputs.device-builder == 'true'
|
if: needs.determine-jobs.outputs.device-builder == 'true'
|
||||||
steps:
|
steps:
|
||||||
- name: Check out esphome (this PR)
|
- name: Check out esphome (this PR)
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
path: esphome
|
path: esphome
|
||||||
- name: Check out esphome/device-builder
|
- name: Check out esphome/device-builder
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
repository: esphome/device-builder
|
repository: esphome/device-builder
|
||||||
ref: main
|
ref: main
|
||||||
path: device-builder
|
path: device-builder
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.13"
|
python-version: "3.13"
|
||||||
- name: Set up uv
|
- name: Set up uv
|
||||||
@@ -174,12 +170,9 @@ jobs:
|
|||||||
# install step (order-of-magnitude faster on cold boots,
|
# install step (order-of-magnitude faster on cold boots,
|
||||||
# with its own wheel cache). actions/setup-python still
|
# with its own wheel cache). actions/setup-python still
|
||||||
# provides the interpreter.
|
# provides the interpreter.
|
||||||
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
# Pull request saves land in per-PR scopes nothing else can
|
|
||||||
# reuse; dev pushes seed the shared copy instead.
|
|
||||||
save-cache: ${{ github.event_name != 'pull_request' }}
|
|
||||||
# Pin uv version so the action does not have to fetch the
|
# Pin uv version so the action does not have to fetch the
|
||||||
# manifest from raw.githubusercontent.com on every cache
|
# manifest from raw.githubusercontent.com on every cache
|
||||||
# miss; that fetch flakes on Windows runners.
|
# miss; that fetch flakes on Windows runners.
|
||||||
@@ -196,12 +189,9 @@ jobs:
|
|||||||
- name: Run device-builder pytest
|
- name: Run device-builder pytest
|
||||||
# ``-n auto`` runs under pytest-xdist (matches device-builder's
|
# ``-n auto`` runs under pytest-xdist (matches device-builder's
|
||||||
# own CI). No ``--cov`` here -- this is purely a downstream
|
# own CI). No ``--cov`` here -- this is purely a downstream
|
||||||
# smoke check against this PR's esphome code. ``tests/e2e/slow``
|
# smoke check against this PR's esphome code.
|
||||||
# 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
|
working-directory: device-builder
|
||||||
run: pytest -q -n auto --maxfail=5 --durations=30 --no-cov --ignore=tests/benchmarks --ignore=tests/e2e/slow
|
run: pytest -q -n auto --maxfail=5 --durations=30 --no-cov --ignore=tests/benchmarks
|
||||||
|
|
||||||
pytest:
|
pytest:
|
||||||
name: Run pytest
|
name: Run pytest
|
||||||
@@ -209,7 +199,7 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
python-version:
|
python-version:
|
||||||
- "3.12"
|
- "3.11"
|
||||||
- "3.13"
|
- "3.13"
|
||||||
- "3.14"
|
- "3.14"
|
||||||
os:
|
os:
|
||||||
@@ -231,7 +221,7 @@ jobs:
|
|||||||
if: needs.determine-jobs.outputs.core-ci == 'true'
|
if: needs.determine-jobs.outputs.core-ci == 'true'
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Restore Python
|
- name: Restore Python
|
||||||
id: restore-python
|
id: restore-python
|
||||||
uses: ./.github/actions/restore-python
|
uses: ./.github/actions/restore-python
|
||||||
@@ -251,12 +241,12 @@ jobs:
|
|||||||
. venv/bin/activate
|
. 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 --durations=30 -n auto tests --ignore=tests/integration/
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
- name: Save Python virtual environment cache
|
- name: Save Python virtual environment cache
|
||||||
if: github.ref == 'refs/heads/dev'
|
if: github.ref == 'refs/heads/dev'
|
||||||
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
|
key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
|
||||||
@@ -276,8 +266,8 @@ jobs:
|
|||||||
python-linters: ${{ steps.determine.outputs.python-linters }}
|
python-linters: ${{ steps.determine.outputs.python-linters }}
|
||||||
import-time: ${{ steps.determine.outputs.import-time }}
|
import-time: ${{ steps.determine.outputs.import-time }}
|
||||||
device-builder: ${{ steps.determine.outputs.device-builder }}
|
device-builder: ${{ steps.determine.outputs.device-builder }}
|
||||||
esp32-platformio: ${{ steps.determine.outputs.esp32-platformio }}
|
native-idf: ${{ steps.determine.outputs.native-idf }}
|
||||||
esp32-platformio-components: ${{ steps.determine.outputs.esp32-platformio-components }}
|
native-idf-components: ${{ steps.determine.outputs.native-idf-components }}
|
||||||
changed-components: ${{ steps.determine.outputs.changed-components }}
|
changed-components: ${{ steps.determine.outputs.changed-components }}
|
||||||
changed-components-with-tests: ${{ steps.determine.outputs.changed-components-with-tests }}
|
changed-components-with-tests: ${{ steps.determine.outputs.changed-components-with-tests }}
|
||||||
directly-changed-components-with-tests: ${{ steps.determine.outputs.directly-changed-components-with-tests }}
|
directly-changed-components-with-tests: ${{ steps.determine.outputs.directly-changed-components-with-tests }}
|
||||||
@@ -291,7 +281,7 @@ jobs:
|
|||||||
benchmarks: ${{ steps.determine.outputs.benchmarks }}
|
benchmarks: ${{ steps.determine.outputs.benchmarks }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
# Fetch enough history to find the merge base
|
# Fetch enough history to find the merge base
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
@@ -301,7 +291,7 @@ jobs:
|
|||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||||
- name: Restore components graph cache
|
- name: Restore components graph cache
|
||||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: .temp/components_graph.json
|
path: .temp/components_graph.json
|
||||||
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
|
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
|
||||||
@@ -330,8 +320,8 @@ jobs:
|
|||||||
echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $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 "import-time=$(echo "$output" | jq -r '.import_time')" >> $GITHUB_OUTPUT
|
||||||
echo "device-builder=$(echo "$output" | jq -r '.device_builder')" >> $GITHUB_OUTPUT
|
echo "device-builder=$(echo "$output" | jq -r '.device_builder')" >> $GITHUB_OUTPUT
|
||||||
echo "esp32-platformio=$(echo "$output" | jq -r '.esp32_platformio')" >> $GITHUB_OUTPUT
|
echo "native-idf=$(echo "$output" | jq -r '.native_idf')" >> $GITHUB_OUTPUT
|
||||||
echo "esp32-platformio-components=$(echo "$output" | jq -r '.esp32_platformio_components')" >> $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=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT
|
||||||
echo "changed-components-with-tests=$(echo "$output" | jq -c '.changed_components_with_tests')" >> $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
|
echo "directly-changed-components-with-tests=$(echo "$output" | jq -c '.directly_changed_components_with_tests')" >> $GITHUB_OUTPUT
|
||||||
@@ -345,7 +335,7 @@ jobs:
|
|||||||
echo "benchmarks=$(echo "$output" | jq -r '.benchmarks')" >> $GITHUB_OUTPUT
|
echo "benchmarks=$(echo "$output" | jq -r '.benchmarks')" >> $GITHUB_OUTPUT
|
||||||
- name: Save components graph cache
|
- name: Save components graph cache
|
||||||
if: github.ref == 'refs/heads/dev'
|
if: github.ref == 'refs/heads/dev'
|
||||||
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: .temp/components_graph.json
|
path: .temp/components_graph.json
|
||||||
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
|
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
|
||||||
@@ -363,27 +353,24 @@ jobs:
|
|||||||
bucket: ${{ fromJson(needs.determine-jobs.outputs.integration-test-buckets) }}
|
bucket: ${{ fromJson(needs.determine-jobs.outputs.integration-test-buckets) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Set up Python 3.13
|
- name: Set up Python 3.13
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.13"
|
python-version: "3.13"
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
|
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
|
||||||
- name: Set up uv
|
- name: Set up uv
|
||||||
# Only needed on cache miss to populate the venv.
|
# Only needed on cache miss to populate the venv.
|
||||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
# Pull request saves land in per-PR scopes nothing else can
|
|
||||||
# reuse; dev pushes seed the shared copy instead.
|
|
||||||
save-cache: ${{ github.event_name != 'pull_request' }}
|
|
||||||
# Pin uv version so the action does not have to fetch the
|
# Pin uv version so the action does not have to fetch the
|
||||||
# manifest from raw.githubusercontent.com on every cache
|
# manifest from raw.githubusercontent.com on every cache
|
||||||
# miss; that fetch flakes on Windows runners.
|
# miss; that fetch flakes on Windows runners.
|
||||||
@@ -418,7 +405,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 != '[]')
|
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:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Restore Python
|
- name: Restore Python
|
||||||
uses: ./.github/actions/restore-python
|
uses: ./.github/actions/restore-python
|
||||||
@@ -447,7 +434,7 @@ jobs:
|
|||||||
(github.event_name == 'pull_request' && needs.determine-jobs.outputs.benchmarks == 'true')
|
(github.event_name == 'pull_request' && needs.determine-jobs.outputs.benchmarks == 'true')
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Restore Python
|
- name: Restore Python
|
||||||
uses: ./.github/actions/restore-python
|
uses: ./.github/actions/restore-python
|
||||||
@@ -465,7 +452,7 @@ jobs:
|
|||||||
echo "binary=$BINARY" >> $GITHUB_OUTPUT
|
echo "binary=$BINARY" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Run CodSpeed benchmarks
|
- name: Run CodSpeed benchmarks
|
||||||
uses: CodSpeedHQ/action@f99becdce5e5d51fd556489ebef684f4ecfd6286 # v4.18.5
|
uses: CodSpeedHQ/action@9d332c4d90b43981c3e55ae8e38e68709996240f # v4.17.0
|
||||||
with:
|
with:
|
||||||
run: |
|
run: |
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
@@ -482,10 +469,6 @@ jobs:
|
|||||||
if: needs.determine-jobs.outputs.clang-tidy == 'true'
|
if: needs.determine-jobs.outputs.clang-tidy == 'true'
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
# esp32-arduino-tidy installs ESP-IDF natively; share the native IDF cache.
|
|
||||||
ESPHOME_ESP_IDF_PREFIX: ~/.esphome-idf
|
|
||||||
# nrf52-tidy installs sdk-nrf natively; pin it to a cacheable path.
|
|
||||||
ESPHOME_SDK_NRF_PREFIX: ~/.esphome-sdk-nrf
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
max-parallel: 2
|
max-parallel: 2
|
||||||
@@ -496,27 +479,18 @@ jobs:
|
|||||||
options: --environment esp8266-arduino-tidy --grep USE_ESP8266
|
options: --environment esp8266-arduino-tidy --grep USE_ESP8266
|
||||||
pio_cache_key: tidyesp8266
|
pio_cache_key: tidyesp8266
|
||||||
- id: clang-tidy
|
- id: clang-tidy
|
||||||
name: Run script/clang-tidy for ESP32 Arduino
|
name: Run script/clang-tidy for ESP32 IDF
|
||||||
options: --environment esp32-arduino-tidy --grep USE_ARDUINO
|
options: --environment esp32-idf-tidy --grep USE_ESP_IDF
|
||||||
cache_idf: true
|
pio_cache_key: tidyesp32-idf
|
||||||
- id: clang-tidy
|
- id: clang-tidy
|
||||||
name: Run script/clang-tidy for ZEPHYR
|
name: Run script/clang-tidy for ZEPHYR
|
||||||
options: --environment nrf52-tidy --grep USE_ZEPHYR --grep USE_NRF52
|
options: --environment nrf52-tidy --grep USE_ZEPHYR --grep USE_NRF52
|
||||||
cache_sdk_nrf: true
|
pio_cache_key: tidy-zephyr
|
||||||
ignore_errors: false
|
ignore_errors: false
|
||||||
- id: clang-tidy
|
|
||||||
name: Run script/clang-tidy for RP2
|
|
||||||
options: --environment rp2-tidy --grep USE_RP2
|
|
||||||
pio_cache_key: tidyrp2
|
|
||||||
- id: clang-tidy
|
|
||||||
name: Run script/clang-tidy for LibreTiny
|
|
||||||
environments: bk72xx-tidy ln882h-tidy rtl87xxb-tidy rtl87xxc-tidy
|
|
||||||
options: --grep USE_LIBRETINY --grep USE_BK72XX --grep USE_RTL87XX --grep USE_LN882X
|
|
||||||
pio_cache_key: tidylibretiny
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
# Need history for HEAD~1 to work for checking changed files
|
# Need history for HEAD~1 to work for checking changed files
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
@@ -528,44 +502,44 @@ jobs:
|
|||||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||||
|
|
||||||
- name: Cache platformio
|
- name: Cache platformio
|
||||||
if: github.ref == 'refs/heads/dev' && matrix.pio_cache_key
|
if: github.ref == 'refs/heads/dev'
|
||||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: ~/.platformio
|
path: ~/.platformio
|
||||||
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
||||||
|
|
||||||
- name: Cache platformio
|
- name: Cache platformio
|
||||||
if: github.ref != 'refs/heads/dev' && matrix.pio_cache_key
|
if: github.ref != 'refs/heads/dev'
|
||||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: ~/.platformio
|
path: ~/.platformio
|
||||||
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
||||||
|
|
||||||
- name: Cache ESP-IDF install
|
|
||||||
if: matrix.cache_idf
|
|
||||||
uses: ./.github/actions/cache-esp-idf
|
|
||||||
with:
|
|
||||||
framework: arduino
|
|
||||||
|
|
||||||
- name: Cache nRF Connect SDK install
|
|
||||||
if: matrix.cache_sdk_nrf
|
|
||||||
uses: ./.github/actions/cache-sdk-nrf
|
|
||||||
|
|
||||||
- name: Register problem matchers
|
- name: Register problem matchers
|
||||||
run: |
|
run: |
|
||||||
echo "::add-matcher::.github/workflows/matchers/gcc.json"
|
echo "::add-matcher::.github/workflows/matchers/gcc.json"
|
||||||
echo "::add-matcher::.github/workflows/matchers/clang-tidy.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
|
- name: Check if full clang-tidy scan needed
|
||||||
id: check_full_scan
|
id: check_full_scan
|
||||||
run: |
|
run: |
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
# determine-jobs.clang-tidy-full-scan is true when core C++ or a
|
# determine-jobs.clang-tidy-full-scan is true when core C++ changed
|
||||||
# clang-tidy-relevant config file changed, or the ci-run-all label
|
# OR the ci-run-all label forced --force-all. Independent of the
|
||||||
# forced --force-all.
|
# hash check, both must produce a full scan in the job itself.
|
||||||
if [ "${{ needs.determine-jobs.outputs.clang-tidy-full-scan }}" = "true" ]; then
|
if [ "${{ needs.determine-jobs.outputs.clang-tidy-full-scan }}" = "true" ]; then
|
||||||
echo "full_scan=true" >> $GITHUB_OUTPUT
|
echo "full_scan=true" >> $GITHUB_OUTPUT
|
||||||
echo "reason=determine_jobs" >> $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
|
else
|
||||||
echo "full_scan=false" >> $GITHUB_OUTPUT
|
echo "full_scan=false" >> $GITHUB_OUTPUT
|
||||||
echo "reason=normal" >> $GITHUB_OUTPUT
|
echo "reason=normal" >> $GITHUB_OUTPUT
|
||||||
@@ -576,21 +550,10 @@ jobs:
|
|||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
if [ "${{ steps.check_full_scan.outputs.full_scan }}" = "true" ]; then
|
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 (reason: ${{ steps.check_full_scan.outputs.reason }})"
|
||||||
changed=""
|
script/clang-tidy --all-headers --fix ${{ matrix.options }} ${{ matrix.ignore_errors && '|| true' || '' }}
|
||||||
else
|
else
|
||||||
echo "Running clang-tidy on changed files only"
|
echo "Running clang-tidy on changed files only"
|
||||||
changed="--changed"
|
script/clang-tidy --all-headers --fix --changed ${{ matrix.options }} ${{ matrix.ignore_errors && '|| true' || '' }}
|
||||||
fi
|
|
||||||
if [ -n "${{ matrix.environments }}" ]; then
|
|
||||||
rc=0
|
|
||||||
for env in ${{ matrix.environments }}; do
|
|
||||||
echo "::group::clang-tidy $env"
|
|
||||||
script/clang-tidy --all-headers --fix $changed --environment "$env" ${{ matrix.options }} ${{ matrix.ignore_errors && '|| true' || '' }} || rc=1
|
|
||||||
echo "::endgroup::"
|
|
||||||
done
|
|
||||||
exit $rc
|
|
||||||
else
|
|
||||||
script/clang-tidy --all-headers --fix $changed ${{ matrix.options }} ${{ matrix.ignore_errors && '|| true' || '' }}
|
|
||||||
fi
|
fi
|
||||||
env:
|
env:
|
||||||
# Also cache libdeps, store them in a ~/.platformio subfolder
|
# Also cache libdeps, store them in a ~/.platformio subfolder
|
||||||
@@ -602,7 +565,7 @@ jobs:
|
|||||||
if: always()
|
if: always()
|
||||||
|
|
||||||
clang-tidy-nosplit:
|
clang-tidy-nosplit:
|
||||||
name: Run script/clang-tidy for ESP32 IDF
|
name: Run script/clang-tidy for ESP32 Arduino
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
needs:
|
needs:
|
||||||
- common
|
- common
|
||||||
@@ -610,11 +573,9 @@ jobs:
|
|||||||
if: needs.determine-jobs.outputs.clang-tidy-mode == 'nosplit'
|
if: needs.determine-jobs.outputs.clang-tidy-mode == 'nosplit'
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
# esp32-idf-tidy installs ESP-IDF natively; share the native IDF cache.
|
|
||||||
ESPHOME_ESP_IDF_PREFIX: ~/.esphome-idf
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
# Need history for HEAD~1 to work for checking changed files
|
# Need history for HEAD~1 to work for checking changed files
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
@@ -625,8 +586,19 @@ jobs:
|
|||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||||
|
|
||||||
- name: Cache ESP-IDF install
|
- name: Cache platformio
|
||||||
uses: ./.github/actions/cache-esp-idf
|
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
|
- name: Register problem matchers
|
||||||
run: |
|
run: |
|
||||||
@@ -637,12 +609,15 @@ jobs:
|
|||||||
id: check_full_scan
|
id: check_full_scan
|
||||||
run: |
|
run: |
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
# determine-jobs.clang-tidy-full-scan is true when core C++ or a
|
# determine-jobs.clang-tidy-full-scan is true when core C++ changed
|
||||||
# clang-tidy-relevant config file changed, or the ci-run-all label
|
# OR the ci-run-all label forced --force-all. Independent of the
|
||||||
# forced --force-all.
|
# hash check, both must produce a full scan in the job itself.
|
||||||
if [ "${{ needs.determine-jobs.outputs.clang-tidy-full-scan }}" = "true" ]; then
|
if [ "${{ needs.determine-jobs.outputs.clang-tidy-full-scan }}" = "true" ]; then
|
||||||
echo "full_scan=true" >> $GITHUB_OUTPUT
|
echo "full_scan=true" >> $GITHUB_OUTPUT
|
||||||
echo "reason=determine_jobs" >> $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
|
else
|
||||||
echo "full_scan=false" >> $GITHUB_OUTPUT
|
echo "full_scan=false" >> $GITHUB_OUTPUT
|
||||||
echo "reason=normal" >> $GITHUB_OUTPUT
|
echo "reason=normal" >> $GITHUB_OUTPUT
|
||||||
@@ -653,10 +628,10 @@ jobs:
|
|||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
if [ "${{ steps.check_full_scan.outputs.full_scan }}" = "true" ]; then
|
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 (reason: ${{ steps.check_full_scan.outputs.reason }})"
|
||||||
script/clang-tidy --all-headers --fix --environment esp32-idf-tidy
|
script/clang-tidy --all-headers --fix --environment esp32-arduino-tidy
|
||||||
else
|
else
|
||||||
echo "Running clang-tidy on changed files only"
|
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
|
fi
|
||||||
env:
|
env:
|
||||||
# Also cache libdeps, store them in a ~/.platformio subfolder
|
# Also cache libdeps, store them in a ~/.platformio subfolder
|
||||||
@@ -675,26 +650,27 @@ jobs:
|
|||||||
if: needs.determine-jobs.outputs.clang-tidy-mode == 'split'
|
if: needs.determine-jobs.outputs.clang-tidy-mode == 'split'
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
# esp32-idf-tidy installs ESP-IDF natively; share the native IDF cache.
|
|
||||||
ESPHOME_ESP_IDF_PREFIX: ~/.esphome-idf
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
max-parallel: 3
|
max-parallel: 2
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- id: clang-tidy
|
- id: clang-tidy
|
||||||
name: Run script/clang-tidy for ESP32 IDF 1/3
|
name: Run script/clang-tidy for ESP32 Arduino 1/4
|
||||||
options: --environment esp32-idf-tidy --split-num 3 --split-at 1
|
options: --environment esp32-arduino-tidy --split-num 4 --split-at 1
|
||||||
- id: clang-tidy
|
- id: clang-tidy
|
||||||
name: Run script/clang-tidy for ESP32 IDF 2/3
|
name: Run script/clang-tidy for ESP32 Arduino 2/4
|
||||||
options: --environment esp32-idf-tidy --split-num 3 --split-at 2
|
options: --environment esp32-arduino-tidy --split-num 4 --split-at 2
|
||||||
- id: clang-tidy
|
- id: clang-tidy
|
||||||
name: Run script/clang-tidy for ESP32 IDF 3/3
|
name: Run script/clang-tidy for ESP32 Arduino 3/4
|
||||||
options: --environment esp32-idf-tidy --split-num 3 --split-at 3
|
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:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
# Need history for HEAD~1 to work for checking changed files
|
# Need history for HEAD~1 to work for checking changed files
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
@@ -705,8 +681,19 @@ jobs:
|
|||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||||
|
|
||||||
- name: Cache ESP-IDF install
|
- name: Cache platformio
|
||||||
uses: ./.github/actions/cache-esp-idf
|
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
|
- name: Register problem matchers
|
||||||
run: |
|
run: |
|
||||||
@@ -717,12 +704,15 @@ jobs:
|
|||||||
id: check_full_scan
|
id: check_full_scan
|
||||||
run: |
|
run: |
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
# determine-jobs.clang-tidy-full-scan is true when core C++ or a
|
# determine-jobs.clang-tidy-full-scan is true when core C++ changed
|
||||||
# clang-tidy-relevant config file changed, or the ci-run-all label
|
# OR the ci-run-all label forced --force-all. Independent of the
|
||||||
# forced --force-all.
|
# hash check, both must produce a full scan in the job itself.
|
||||||
if [ "${{ needs.determine-jobs.outputs.clang-tidy-full-scan }}" = "true" ]; then
|
if [ "${{ needs.determine-jobs.outputs.clang-tidy-full-scan }}" = "true" ]; then
|
||||||
echo "full_scan=true" >> $GITHUB_OUTPUT
|
echo "full_scan=true" >> $GITHUB_OUTPUT
|
||||||
echo "reason=determine_jobs" >> $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
|
else
|
||||||
echo "full_scan=false" >> $GITHUB_OUTPUT
|
echo "full_scan=false" >> $GITHUB_OUTPUT
|
||||||
echo "reason=normal" >> $GITHUB_OUTPUT
|
echo "reason=normal" >> $GITHUB_OUTPUT
|
||||||
@@ -746,109 +736,18 @@ jobs:
|
|||||||
run: script/ci-suggest-changes
|
run: script/ci-suggest-changes
|
||||||
if: always()
|
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
|
|
||||||
# yamllint disable-line rule:line-length
|
|
||||||
options: --environment esp32s3-idf-tidy --grep SOC_TEMP_SENSOR_SUPPORTED --grep USE_ESP32_VARIANT_ESP32S3 --grep USE_LOGGER_USB_CDC
|
|
||||||
- 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 SOC_LP_I2C_SUPPORTED --grep USE_ESP32_VARIANT_ESP32C6 --grep USE_OPENTHREAD --grep USE_ZIGBEE
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Check out code from GitHub
|
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
||||||
with:
|
|
||||||
# Need history for HEAD~1 to work for checking changed files
|
|
||||||
fetch-depth: 2
|
|
||||||
|
|
||||||
- name: Restore Python
|
|
||||||
uses: ./.github/actions/restore-python
|
|
||||||
with:
|
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
|
||||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
|
||||||
|
|
||||||
- name: Cache ESP-IDF install
|
|
||||||
uses: ./.github/actions/cache-esp-idf
|
|
||||||
|
|
||||||
- name: Register problem matchers
|
|
||||||
run: |
|
|
||||||
echo "::add-matcher::.github/workflows/matchers/gcc.json"
|
|
||||||
echo "::add-matcher::.github/workflows/matchers/clang-tidy.json"
|
|
||||||
|
|
||||||
- name: Check if full clang-tidy scan needed
|
|
||||||
id: check_full_scan
|
|
||||||
run: |
|
|
||||||
. venv/bin/activate
|
|
||||||
# determine-jobs.clang-tidy-full-scan is true when core C++ or a
|
|
||||||
# clang-tidy-relevant config file changed, or the ci-run-all label
|
|
||||||
# forced --force-all.
|
|
||||||
if [ "${{ needs.determine-jobs.outputs.clang-tidy-full-scan }}" = "true" ]; then
|
|
||||||
echo "full_scan=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "reason=determine_jobs" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "full_scan=false" >> $GITHUB_OUTPUT
|
|
||||||
echo "reason=normal" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Run clang-tidy
|
|
||||||
# Limited variant scan: only the files carrying that variant's code paths
|
|
||||||
# (no --all-headers; the comprehensive esp32-idf pass covers the shared tree).
|
|
||||||
run: |
|
|
||||||
. venv/bin/activate
|
|
||||||
if [ "${{ steps.check_full_scan.outputs.full_scan }}" = "true" ]; then
|
|
||||||
echo "Running FULL clang-tidy scan (reason: ${{ steps.check_full_scan.outputs.reason }})"
|
|
||||||
script/clang-tidy --fix ${{ matrix.options }}
|
|
||||||
else
|
|
||||||
echo "Running clang-tidy on changed files only"
|
|
||||||
script/clang-tidy --fix --changed ${{ matrix.options }}
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Suggested changes
|
|
||||||
run: script/ci-suggest-changes
|
|
||||||
if: always()
|
|
||||||
|
|
||||||
test-build-components-split:
|
test-build-components-split:
|
||||||
name: Test components batch (${{ matrix.batch.components }})
|
name: Test components batch (${{ matrix.components }})
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
needs:
|
needs:
|
||||||
- common
|
- common
|
||||||
- determine-jobs
|
- determine-jobs
|
||||||
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0
|
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0
|
||||||
env:
|
|
||||||
# esp32 component builds use the native ESP-IDF toolchain (default), so
|
|
||||||
# share the tidy jobs' install location -- the restore below lands here.
|
|
||||||
ESPHOME_ESP_IDF_PREFIX: ~/.esphome-idf
|
|
||||||
# nrf52 component builds install sdk-nrf natively; pin it to the shared
|
|
||||||
# cacheable path so the restore below lands where the build looks.
|
|
||||||
ESPHOME_SDK_NRF_PREFIX: ~/.esphome-sdk-nrf
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
max-parallel: ${{ (startsWith(github.base_ref, 'beta') || startsWith(github.base_ref, 'release')) && 8 || 4 }}
|
max-parallel: ${{ (startsWith(github.base_ref, 'beta') || startsWith(github.base_ref, 'release')) && 8 || 4 }}
|
||||||
matrix:
|
matrix:
|
||||||
batch: ${{ fromJson(needs.determine-jobs.outputs.component-test-batches) }}
|
components: ${{ fromJson(needs.determine-jobs.outputs.component-test-batches) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Show disk space
|
- name: Show disk space
|
||||||
run: |
|
run: |
|
||||||
@@ -856,38 +755,21 @@ jobs:
|
|||||||
df -h
|
df -h
|
||||||
|
|
||||||
- name: List components
|
- name: List components
|
||||||
run: echo ${{ matrix.batch.components }}
|
run: echo ${{ matrix.components }}
|
||||||
|
|
||||||
- name: Install apt packages
|
- name: Cache apt packages
|
||||||
# Not cached: this job is pull-request-only, so a cache save could
|
uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.5.3
|
||||||
# never be shared and would only consume quota.
|
with:
|
||||||
run: |
|
packages: libsdl2-dev
|
||||||
sudo apt-get update -qq
|
version: 1.0
|
||||||
sudo apt-get install -y --no-install-recommends libsdl2-dev ccache
|
|
||||||
|
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Restore Python
|
- name: Restore Python
|
||||||
uses: ./.github/actions/restore-python
|
uses: ./.github/actions/restore-python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||||
- name: Cache ESP-IDF install (restore-only)
|
|
||||||
# Only batches whose test platforms include esp32 need the native
|
|
||||||
# ESP-IDF install; never save -- just reuse the shared install the
|
|
||||||
# dev tidy jobs already cached when present.
|
|
||||||
if: matrix.batch.needs_idf
|
|
||||||
uses: ./.github/actions/cache-esp-idf
|
|
||||||
with:
|
|
||||||
restore-only: true
|
|
||||||
- name: Cache nRF Connect SDK install (restore-only)
|
|
||||||
# Only batches whose test platforms include nrf52 need the native
|
|
||||||
# sdk-nrf install; never save -- just reuse the shared install the
|
|
||||||
# dev nrf52 tidy job cached when present.
|
|
||||||
if: matrix.batch.needs_nrf
|
|
||||||
uses: ./.github/actions/cache-sdk-nrf
|
|
||||||
with:
|
|
||||||
restore-only: true
|
|
||||||
- name: Validate and compile components with intelligent grouping
|
- name: Validate and compile components with intelligent grouping
|
||||||
run: |
|
run: |
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
@@ -918,7 +800,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Convert space-separated components to comma-separated for Python script
|
# Convert space-separated components to comma-separated for Python script
|
||||||
components_csv=$(echo "${{ matrix.batch.components }}" | tr ' ' ',')
|
components_csv=$(echo "${{ matrix.components }}" | tr ' ' ',')
|
||||||
|
|
||||||
# Only isolate directly changed components when targeting dev branch
|
# Only isolate directly changed components when targeting dev branch
|
||||||
# For beta/release branches, group everything for faster CI
|
# For beta/release branches, group everything for faster CI
|
||||||
@@ -939,7 +821,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Show disk space before validation
|
# Show disk space before validation (after bind mounts setup)
|
||||||
echo "Disk space before config validation:"
|
echo "Disk space before config validation:"
|
||||||
df -h
|
df -h
|
||||||
echo ""
|
echo ""
|
||||||
@@ -991,27 +873,23 @@ jobs:
|
|||||||
echo "All components in this batch are validate-only -- skipping compile stage."
|
echo "All components in this batch are validate-only -- skipping compile stage."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Print ccache statistics
|
test-native-idf:
|
||||||
# esphome stores the cache under the IDF tools path; expand the leading
|
name: Test components with native ESP-IDF
|
||||||
# ~ in ESPHOME_ESP_IDF_PREFIX so ccache reads the dir the build used.
|
|
||||||
run: CCACHE_DIR="${ESPHOME_ESP_IDF_PREFIX/#\~/$HOME}/ccache" ccache -s
|
|
||||||
|
|
||||||
test-esp32-platformio:
|
|
||||||
name: Test esp32 components with PlatformIO
|
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
needs:
|
needs:
|
||||||
- common
|
- common
|
||||||
- determine-jobs
|
- determine-jobs
|
||||||
if: github.event_name == 'pull_request' && needs.determine-jobs.outputs.esp32-platformio == 'true'
|
if: github.event_name == 'pull_request' && needs.determine-jobs.outputs.native-idf == 'true'
|
||||||
env:
|
env:
|
||||||
# Comma-joined subset of the esp32 PlatformIO representative component list,
|
ESPHOME_ESP_IDF_PREFIX: ~/.esphome-idf
|
||||||
# computed by script/determine-jobs.py (esp32_platformio_components_to_test).
|
# 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
|
# Single source of truth -- the full list lives in
|
||||||
# script/determine-jobs.py::ESP32_PLATFORMIO_TEST_COMPONENTS.
|
# script/determine-jobs.py::NATIVE_IDF_TEST_COMPONENTS.
|
||||||
TEST_COMPONENTS: ${{ needs.determine-jobs.outputs.esp32-platformio-components }}
|
TEST_COMPONENTS: ${{ needs.determine-jobs.outputs.native-idf-components }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Restore Python
|
- name: Restore Python
|
||||||
uses: ./.github/actions/restore-python
|
uses: ./.github/actions/restore-python
|
||||||
@@ -1019,53 +897,70 @@ jobs:
|
|||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||||
|
|
||||||
- name: Run PlatformIO compile test
|
- name: Cache ESPHome
|
||||||
|
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
|
with:
|
||||||
|
path: ~/.esphome-idf
|
||||||
|
key: ${{ runner.os }}-esphome-${{ needs.common.outputs.cache-key }}
|
||||||
|
|
||||||
|
- name: Run native ESP-IDF compile test
|
||||||
run: |
|
run: |
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
|
|
||||||
|
# Check if /mnt has more free space than / before bind mounting
|
||||||
|
# Extract available space in KB for comparison
|
||||||
|
root_avail=$(df -k / | awk 'NR==2 {print $4}')
|
||||||
|
mnt_avail=$(df -k /mnt 2>/dev/null | awk 'NR==2 {print $4}')
|
||||||
|
|
||||||
|
echo "Available space: / has ${root_avail}KB, /mnt has ${mnt_avail}KB"
|
||||||
|
|
||||||
|
# Only use /mnt if it has more space than /
|
||||||
|
if [ -n "$mnt_avail" ] && [ "$mnt_avail" -gt "$root_avail" ]; then
|
||||||
|
echo "Using /mnt for build files (more space available)"
|
||||||
|
# Bind mount PlatformIO directory to /mnt (tools, packages, build cache all go there)
|
||||||
|
sudo mkdir -p /mnt/esphome-idf
|
||||||
|
sudo chown $USER:$USER /mnt/esphome-idf
|
||||||
|
mkdir -p ~/.esphome-idf
|
||||||
|
sudo mount --bind /mnt/esphome-idf ~/.esphome-idf
|
||||||
|
|
||||||
|
# Bind mount test build directory to /mnt
|
||||||
|
sudo mkdir -p /mnt/test_build_components_build
|
||||||
|
sudo chown $USER:$USER /mnt/test_build_components_build
|
||||||
|
mkdir -p tests/test_build_components/build
|
||||||
|
sudo mount --bind /mnt/test_build_components_build tests/test_build_components/build
|
||||||
|
else
|
||||||
|
echo "Using / for build files (more space available than /mnt or /mnt unavailable)"
|
||||||
|
fi
|
||||||
|
|
||||||
echo "Testing components: $TEST_COMPONENTS"
|
echo "Testing components: $TEST_COMPONENTS"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# compile validates config first, so a separate config pass is
|
# Show disk space before validation (after bind mounts setup)
|
||||||
# redundant for this smoke test. ESP-IDF framework via PlatformIO:
|
echo "Disk space before config validation:"
|
||||||
python3 script/test_build_components.py -e compile -t esp32-idf -c "$TEST_COMPONENTS" -f --toolchain platformio
|
df -h
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "ESP-IDF-via-PlatformIO build passed! Starting Arduino smoke test..."
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Arduino framework via PlatformIO (only components with an esp32-ard test are built):
|
# Run config validation (auto-grouped by test_build_components.py)
|
||||||
python3 script/test_build_components.py -e compile -t esp32-ard -c "$TEST_COMPONENTS" -f --toolchain platformio
|
python3 script/test_build_components.py -e config -t esp32-idf -c "$TEST_COMPONENTS" -f --toolchain esp-idf
|
||||||
|
|
||||||
pre-commit-seed-cache:
|
echo ""
|
||||||
name: Seed pre-commit cache
|
echo "Config validation passed! Starting compilation..."
|
||||||
runs-on: ubuntu-latest
|
echo ""
|
||||||
needs:
|
|
||||||
- common
|
# Show disk space before compilation
|
||||||
# Saves a dev-scoped pre-commit cache that pull request runs can
|
echo "Disk space before compilation:"
|
||||||
# restore, since pre-commit.ci lite itself never runs on dev pushes.
|
df -h
|
||||||
if: github.event_name == 'push' && github.ref == 'refs/heads/dev'
|
echo ""
|
||||||
steps:
|
|
||||||
- name: Check out code from GitHub
|
# Run compilation (auto-grouped by test_build_components.py)
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
python3 script/test_build_components.py -e compile -t esp32-idf -c "$TEST_COMPONENTS" -f --toolchain esp-idf
|
||||||
- name: Restore Python
|
|
||||||
uses: ./.github/actions/restore-python
|
- name: Save ESPHome cache
|
||||||
|
if: github.ref == 'refs/heads/dev'
|
||||||
|
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
path: ~/.esphome-idf
|
||||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
key: ${{ runner.os }}-esphome-${{ needs.common.outputs.cache-key }}
|
||||||
- name: Cache pre-commit environments
|
|
||||||
id: cache-pre-commit
|
|
||||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
||||||
with:
|
|
||||||
path: ~/.cache/pre-commit
|
|
||||||
# Must match the restore key in pre-commit-ci-lite
|
|
||||||
# yamllint disable-line rule:line-length
|
|
||||||
key: pre-commit-3|${{ env.pythonLocation }}|${{ hashFiles('.pre-commit-config.yaml') }}
|
|
||||||
- name: Install pre-commit hook environments
|
|
||||||
if: steps.cache-pre-commit.outputs.cache-hit != 'true'
|
|
||||||
run: |
|
|
||||||
python -m pip install pre-commit
|
|
||||||
pre-commit install-hooks
|
|
||||||
|
|
||||||
pre-commit-ci-lite:
|
pre-commit-ci-lite:
|
||||||
name: pre-commit.ci lite
|
name: pre-commit.ci lite
|
||||||
@@ -1076,28 +971,15 @@ 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') && needs.determine-jobs.outputs.core-ci == 'true'
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Restore Python
|
- name: Restore Python
|
||||||
uses: ./.github/actions/restore-python
|
uses: ./.github/actions/restore-python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||||
# Inlined from esphome/pre-commit-action with a restore-only cache
|
- uses: esphome/pre-commit-action@43cd1109c09c544d97196f7730ee5b2e0cc6d81e # v3.0.1 fork with pinned actions/cache
|
||||||
# step: the pre-commit-seed-cache job owns saving this cache, so
|
|
||||||
# pull request runs never write per-PR copies.
|
|
||||||
- name: Restore pre-commit cache
|
|
||||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
||||||
with:
|
|
||||||
path: ~/.cache/pre-commit
|
|
||||||
# Must match the key pre-commit-seed-cache saves
|
|
||||||
# yamllint disable-line rule:line-length
|
|
||||||
key: pre-commit-3|${{ env.pythonLocation }}|${{ hashFiles('.pre-commit-config.yaml') }}
|
|
||||||
- name: Run pre-commit
|
|
||||||
env:
|
env:
|
||||||
SKIP: pylint,ci-custom
|
SKIP: pylint,clang-tidy-hash,ci-custom
|
||||||
run: |
|
|
||||||
python -m pip install pre-commit
|
|
||||||
pre-commit run --show-diff-on-failure --color=always --all-files
|
|
||||||
- uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0
|
- uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0
|
||||||
if: always()
|
if: always()
|
||||||
|
|
||||||
@@ -1115,7 +997,7 @@ jobs:
|
|||||||
skip: ${{ steps.check-script.outputs.skip || steps.check-tests.outputs.skip }}
|
skip: ${{ steps.check-script.outputs.skip || steps.check-tests.outputs.skip }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out target branch
|
- name: Check out target branch
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.base_ref }}
|
ref: ${{ github.base_ref }}
|
||||||
|
|
||||||
@@ -1191,7 +1073,7 @@ jobs:
|
|||||||
- name: Restore cached memory analysis
|
- name: Restore cached memory analysis
|
||||||
id: cache-memory-analysis
|
id: cache-memory-analysis
|
||||||
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true'
|
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true'
|
||||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: memory-analysis-target.json
|
path: memory-analysis-target.json
|
||||||
key: ${{ steps.cache-key.outputs.cache-key }}
|
key: ${{ steps.cache-key.outputs.cache-key }}
|
||||||
@@ -1215,7 +1097,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Cache platformio
|
- name: Cache platformio
|
||||||
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
|
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
|
||||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: ~/.platformio
|
path: ~/.platformio
|
||||||
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
|
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
|
||||||
@@ -1257,7 +1139,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Save memory analysis to cache
|
- name: Save memory analysis to cache
|
||||||
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success'
|
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success'
|
||||||
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: memory-analysis-target.json
|
path: memory-analysis-target.json
|
||||||
key: ${{ steps.cache-key.outputs.cache-key }}
|
key: ${{ steps.cache-key.outputs.cache-key }}
|
||||||
@@ -1297,14 +1179,14 @@ jobs:
|
|||||||
flash_usage: ${{ steps.extract.outputs.flash_usage }}
|
flash_usage: ${{ steps.extract.outputs.flash_usage }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out PR branch
|
- name: Check out PR branch
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Restore Python
|
- name: Restore Python
|
||||||
uses: ./.github/actions/restore-python
|
uses: ./.github/actions/restore-python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||||
- name: Cache platformio
|
- name: Cache platformio
|
||||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: ~/.platformio
|
path: ~/.platformio
|
||||||
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
|
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
|
||||||
@@ -1366,7 +1248,7 @@ jobs:
|
|||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Restore Python
|
- name: Restore Python
|
||||||
uses: ./.github/actions/restore-python
|
uses: ./.github/actions/restore-python
|
||||||
with:
|
with:
|
||||||
@@ -1409,11 +1291,10 @@ jobs:
|
|||||||
- clang-tidy-single
|
- clang-tidy-single
|
||||||
- clang-tidy-nosplit
|
- clang-tidy-nosplit
|
||||||
- clang-tidy-split
|
- clang-tidy-split
|
||||||
- clang-tidy-esp32-variants
|
|
||||||
- determine-jobs
|
- determine-jobs
|
||||||
- device-builder
|
- device-builder
|
||||||
- test-build-components-split
|
- test-build-components-split
|
||||||
- test-esp32-platformio
|
- test-native-idf
|
||||||
- pre-commit-ci-lite
|
- pre-commit-ci-lite
|
||||||
- memory-impact-target-branch
|
- memory-impact-target-branch
|
||||||
- memory-impact-pr-branch
|
- memory-impact-pr-branch
|
||||||
@@ -1429,7 +1310,4 @@ jobs:
|
|||||||
# 1. The target branch has a build issue independent of this PR
|
# 1. The target branch has a build issue independent of this PR
|
||||||
# 2. This PR fixes a build issue on the target branch
|
# 2. This PR fixes a build issue on the target branch
|
||||||
# In either case, we only care that the PR branch builds successfully.
|
# In either case, we only care that the PR branch builds successfully.
|
||||||
# Every other job must have succeeded or been skipped; a "cancelled" or
|
echo "$NEEDS_JSON" | jq -e 'del(.["memory-impact-target-branch"]) | all(.result != "failure")'
|
||||||
# "failure" result fails this check so CI is not reported green when the
|
|
||||||
# workflow was cancelled.
|
|
||||||
echo "$NEEDS_JSON" | jq -e 'del(.["memory-impact-target-branch"]) | all(.result == "success" or .result == "skipped")'
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout base branch
|
- name: Checkout base branch
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.base.sha }}
|
ref: ${{ github.event.pull_request.base.sha }}
|
||||||
sparse-checkout: |
|
sparse-checkout: |
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout base branch
|
- name: Checkout base branch
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.base.sha }}
|
ref: ${{ github.event.pull_request.base.sha }}
|
||||||
|
|
||||||
|
|||||||
@@ -52,11 +52,11 @@ jobs:
|
|||||||
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
|
# 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:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1
|
uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
build-mode: ${{ matrix.build-mode }}
|
build-mode: ${{ matrix.build-mode }}
|
||||||
@@ -84,6 +84,6 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1
|
uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
|
||||||
with:
|
with:
|
||||||
category: "/language:${{matrix.language}}"
|
category: "/language:${{matrix.language}}"
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -14,4 +14,4 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
issues: write # issues.lock on closed issues
|
issues: write # issues.lock on closed issues
|
||||||
pull-requests: write # issues.lock on closed pull requests
|
pull-requests: write # issues.lock on closed pull requests
|
||||||
uses: esphome/workflows/.github/workflows/lock.yml@9f6577fd37b5cf773ab1b9be929714a0dcd15661 # 2026.7.0
|
uses: esphome/workflows/.github/workflows/lock.yml@025a1e6255610c498ed590403b7e510b69e474df # 2026.4.1
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ jobs:
|
|||||||
name: Validate PR title
|
name: Validate PR title
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ jobs:
|
|||||||
branch_build: ${{ steps.tag.outputs.branch_build }}
|
branch_build: ${{ steps.tag.outputs.branch_build }}
|
||||||
deploy_env: ${{ steps.tag.outputs.deploy_env }}
|
deploy_env: ${{ steps.tag.outputs.deploy_env }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Get tag
|
- name: Get tag
|
||||||
id: tag
|
id: tag
|
||||||
# yamllint disable rule:line-length
|
# yamllint disable rule:line-length
|
||||||
@@ -60,9 +60,9 @@ jobs:
|
|||||||
contents: read # actions/checkout to build the sdist/wheel
|
contents: read # actions/checkout to build the sdist/wheel
|
||||||
id-token: write # OIDC token for PyPI Trusted Publishing (pypa/gh-action-pypi-publish)
|
id-token: write # OIDC token for PyPI Trusted Publishing (pypa/gh-action-pypi-publish)
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.x"
|
python-version: "3.x"
|
||||||
- name: Build
|
- name: Build
|
||||||
@@ -92,22 +92,22 @@ jobs:
|
|||||||
os: "ubuntu-24.04-arm"
|
os: "ubuntu-24.04-arm"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.11"
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||||
|
|
||||||
- name: Log in to docker hub
|
- name: Log in to docker hub
|
||||||
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USER }}
|
username: ${{ secrets.DOCKER_USER }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
- name: Log in to the GitHub container registry
|
- name: Log in to the GitHub container registry
|
||||||
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -168,7 +168,7 @@ jobs:
|
|||||||
- ghcr
|
- ghcr
|
||||||
- dockerhub
|
- dockerhub
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Download digests
|
- name: Download digests
|
||||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
@@ -178,17 +178,17 @@ jobs:
|
|||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||||
|
|
||||||
- name: Log in to docker hub
|
- name: Log in to docker hub
|
||||||
if: matrix.registry == 'dockerhub'
|
if: matrix.registry == 'dockerhub'
|
||||||
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USER }}
|
username: ${{ secrets.DOCKER_USER }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
- name: Log in to the GitHub container registry
|
- name: Log in to the GitHub container registry
|
||||||
if: matrix.registry == 'ghcr'
|
if: matrix.registry == 'ghcr'
|
||||||
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Stale
|
- name: Stale
|
||||||
uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10.4.0
|
uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
|
||||||
with:
|
with:
|
||||||
debug-only: ${{ github.ref != 'refs/heads/dev' }} # Dry-run when not run on dev branch
|
debug-only: ${{ github.ref != 'refs/heads/dev' }} # Dry-run when not run on dev branch
|
||||||
remove-stale-when-updated: true
|
remove-stale-when-updated: true
|
||||||
|
|||||||
@@ -28,16 +28,16 @@ jobs:
|
|||||||
permission-pull-requests: write # pulls.create / pulls.update to open or refresh the sync PR
|
permission-pull-requests: write # pulls.create / pulls.update to open or refresh the sync PR
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Checkout Home Assistant
|
- name: Checkout Home Assistant
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
repository: home-assistant/core
|
repository: home-assistant/core
|
||||||
path: lib/home-assistant
|
path: lib/home-assistant
|
||||||
|
|
||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.14"
|
python-version: "3.14"
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ jobs:
|
|||||||
# setup-python interpreter so subsequent ``pre-commit`` /
|
# setup-python interpreter so subsequent ``pre-commit`` /
|
||||||
# ``script/run-in-env.py`` steps find the deps without a
|
# ``script/run-in-env.py`` steps find the deps without a
|
||||||
# ``uv run`` prefix.
|
# ``uv run`` prefix.
|
||||||
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
# Pin uv version so the action does not have to fetch the
|
# Pin uv version so the action does not have to fetch the
|
||||||
|
|||||||
@@ -141,7 +141,6 @@ tests/.esphome/
|
|||||||
|
|
||||||
sdkconfig.*
|
sdkconfig.*
|
||||||
!sdkconfig.defaults
|
!sdkconfig.defaults
|
||||||
!sdkconfig.defaults.*
|
|
||||||
|
|
||||||
.tests/
|
.tests/
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ ci:
|
|||||||
autoupdate_commit_msg: 'pre-commit: autoupdate'
|
autoupdate_commit_msg: 'pre-commit: autoupdate'
|
||||||
autoupdate_schedule: off # Disabled until ruff versions are synced between deps and pre-commit
|
autoupdate_schedule: off # Disabled until ruff versions are synced between deps and pre-commit
|
||||||
# Skip hooks that have issues in pre-commit CI environment
|
# Skip hooks that have issues in pre-commit CI environment
|
||||||
skip: [pylint]
|
skip: [pylint, clang-tidy-hash]
|
||||||
|
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
@@ -40,7 +40,7 @@ repos:
|
|||||||
rev: v3.21.2
|
rev: v3.21.2
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyupgrade
|
- id: pyupgrade
|
||||||
args: [--py312-plus]
|
args: [--py311-plus]
|
||||||
- repo: https://github.com/adrienverge/yamllint.git
|
- repo: https://github.com/adrienverge/yamllint.git
|
||||||
rev: v1.37.1
|
rev: v1.37.1
|
||||||
hooks:
|
hooks:
|
||||||
@@ -59,6 +59,13 @@ repos:
|
|||||||
language: system
|
language: system
|
||||||
types: [python]
|
types: [python]
|
||||||
files: ^esphome/.+\.py$
|
files: ^esphome/.+\.py$
|
||||||
|
- id: clang-tidy-hash
|
||||||
|
name: Update clang-tidy hash
|
||||||
|
entry: python script/clang_tidy_hash.py --update-if-changed
|
||||||
|
language: python
|
||||||
|
files: ^(\.clang-tidy|platformio\.ini|requirements_dev\.txt)$
|
||||||
|
pass_filenames: false
|
||||||
|
additional_dependencies: []
|
||||||
- id: ci-custom
|
- id: ci-custom
|
||||||
name: ci-custom
|
name: ci-custom
|
||||||
entry: python script/run-in-env.py script/ci-custom.py
|
entry: python script/run-in-env.py script/ci-custom.py
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ This document provides essential context for AI models interacting with this pro
|
|||||||
|
|
||||||
## 2. Core Technologies & Stack
|
## 2. Core Technologies & Stack
|
||||||
|
|
||||||
* **Languages:** Python (>=3.12), C++ (gnu++20)
|
* **Languages:** Python (>=3.11), C++ (gnu++20)
|
||||||
* **Frameworks & Runtimes:** PlatformIO, Arduino, ESP-IDF.
|
* **Frameworks & Runtimes:** PlatformIO, Arduino, ESP-IDF.
|
||||||
* **Build Systems:** PlatformIO is the primary build system. CMake is used as an alternative.
|
* **Build Systems:** PlatformIO is the primary build system. CMake is used as an alternative.
|
||||||
* **Configuration:** YAML.
|
* **Configuration:** YAML.
|
||||||
* **Key Libraries/Dependencies:**
|
* **Key Libraries/Dependencies:**
|
||||||
* **Python:** `voluptuous` (for configuration validation), `PyYAML` (for parsing configuration files), `paho-mqtt` (for MQTT communication), `aioesphomeapi` (for the native API).
|
* **Python:** `voluptuous` (for configuration validation), `PyYAML` (for parsing configuration files), `paho-mqtt` (for MQTT communication), `tornado` (for the web server), `aioesphomeapi` (for the native API).
|
||||||
* **C++:** `ArduinoJson` (for JSON serialization/deserialization), `AsyncMqttClient-esphome` (for MQTT), `ESPAsyncWebServer` (for the web server).
|
* **C++:** `ArduinoJson` (for JSON serialization/deserialization), `AsyncMqttClient-esphome` (for MQTT), `ESPAsyncWebServer` (for the web server).
|
||||||
* **Package Manager(s):** `pip` (for Python dependencies), `platformio` (for C++/PlatformIO dependencies).
|
* **Package Manager(s):** `pip` (for Python dependencies), `platformio` (for C++/PlatformIO dependencies).
|
||||||
* **Communication Protocols:** Protobuf (for native API), MQTT, HTTP.
|
* **Communication Protocols:** Protobuf (for native API), MQTT, HTTP.
|
||||||
@@ -35,6 +35,7 @@ This document provides essential context for AI models interacting with this pro
|
|||||||
2. **Code Generation** (`esphome/codegen.py`, `esphome/cpp_generator.py`): Manages Python to C++ code generation, template processing, and build flag management.
|
2. **Code Generation** (`esphome/codegen.py`, `esphome/cpp_generator.py`): Manages Python to C++ code generation, template processing, and build flag management.
|
||||||
3. **Component System** (`esphome/components/`): Contains modular hardware and software components with platform-specific implementations and dependency management.
|
3. **Component System** (`esphome/components/`): Contains modular hardware and software components with platform-specific implementations and dependency management.
|
||||||
4. **Core Framework** (`esphome/core/`): Manages the application lifecycle, hardware abstraction, and component registration.
|
4. **Core Framework** (`esphome/core/`): Manages the application lifecycle, hardware abstraction, and component registration.
|
||||||
|
5. **Dashboard** (`esphome/dashboard/`): A web-based interface for device configuration, management, and OTA updates.
|
||||||
|
|
||||||
* **Platform Support:**
|
* **Platform Support:**
|
||||||
1. **ESP32** (`components/esp32/`): Espressif ESP32 family. Supports multiple variants (Original, C2, C3, C5, C6, H2, P4, S2, S3) with ESP-IDF framework. Arduino framework supports only a subset of the variants (Original, C3, S2, S3).
|
1. **ESP32** (`components/esp32/`): Espressif ESP32 family. Supports multiple variants (Original, C2, C3, C5, C6, H2, P4, S2, S3) with ESP-IDF framework. Arduino framework supports only a subset of the variants (Original, C3, S2, S3).
|
||||||
@@ -58,19 +59,6 @@ This document provides essential context for AI models interacting with this pro
|
|||||||
- Protected/private fields: `lower_snake_case_with_trailing_underscore_`
|
- Protected/private fields: `lower_snake_case_with_trailing_underscore_`
|
||||||
- Favor descriptive names over abbreviations
|
- Favor descriptive names over abbreviations
|
||||||
|
|
||||||
* **Python Idioms:**
|
|
||||||
* **Assignment expressions (PEP 572):** Prefer the walrus operator (`:=`) wherever it removes a redundant lookup or a throwaway temporary. The most common case in component code is presence-checking a config key and then indexing it separately — fetch once with `.get()` and bind in the condition instead:
|
|
||||||
```python
|
|
||||||
# Bad - looks up CONF_BLAH twice
|
|
||||||
if CONF_BLAH in config:
|
|
||||||
cg.add(var.set_blah(config[CONF_BLAH]))
|
|
||||||
|
|
||||||
# Good - single lookup, value bound inline
|
|
||||||
if (blah := config.get(CONF_BLAH)) is not None:
|
|
||||||
cg.add(var.set_blah(blah))
|
|
||||||
```
|
|
||||||
The same applies to `while` loops and comprehensions where it avoids recomputing a value. Don't contort code to use it — reach for `:=` only when it genuinely cuts repetition or an extra assignment line.
|
|
||||||
|
|
||||||
* **C++ Field Visibility:**
|
* **C++ Field Visibility:**
|
||||||
* **Prefer `protected`:** Use `protected` for most class fields to enable extensibility and testing. Fields should be `lower_snake_case_with_trailing_underscore_`.
|
* **Prefer `protected`:** Use `protected` for most class fields to enable extensibility and testing. Fields should be `lower_snake_case_with_trailing_underscore_`.
|
||||||
* **Use `private` for safety-critical cases:** Use `private` visibility when direct field access could introduce bugs or violate invariants:
|
* **Use `private` for safety-critical cases:** Use `private` visibility when direct field access could introduce bugs or violate invariants:
|
||||||
@@ -427,14 +415,13 @@ This document provides essential context for AI models interacting with this pro
|
|||||||
|
|
||||||
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.
|
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/`.
|
* **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/`:
|
||||||
|
|
||||||
All includes in test files must go through dict-style `packages:` so that batch grouping works correctly — the grouping scripts only understand dict-style packages. Never use list-style packages (`packages: [- !include ...]`) or top-level merge keys (`<<: !include common.yaml`). Bus packages are keyed by the bus name; the component's `common.yaml` is keyed by the component name (e.g. `cst328: !include common.yaml`):
|
|
||||||
```yaml
|
```yaml
|
||||||
# test.esp32-idf.yaml — everything included via named packages
|
# test.esp32-idf.yaml — use packages for buses
|
||||||
packages:
|
packages:
|
||||||
uart: !include ../../test_build_components/common/uart_115200/esp32-idf.yaml
|
uart: !include ../../test_build_components/common/uart_115200/esp32-idf.yaml
|
||||||
my_component: !include common.yaml
|
|
||||||
|
<<: !include common.yaml
|
||||||
```
|
```
|
||||||
```yaml
|
```yaml
|
||||||
# common.yaml — component config only, NO bus definitions
|
# common.yaml — component config only, NO bus definitions
|
||||||
@@ -456,6 +443,7 @@ This document provides essential context for AI models interacting with this pro
|
|||||||
* **Debug Tools:**
|
* **Debug Tools:**
|
||||||
- `esphome config <file>.yaml` to validate configuration.
|
- `esphome config <file>.yaml` to validate configuration.
|
||||||
- `esphome compile <file>.yaml` to compile without uploading.
|
- `esphome compile <file>.yaml` to compile without uploading.
|
||||||
|
- Check the Dashboard for real-time logs.
|
||||||
- Use component-specific debug logging.
|
- Use component-specific debug logging.
|
||||||
* **Common Issues:**
|
* **Common Issues:**
|
||||||
- **Import Errors**: Check component dependencies and `PYTHONPATH`.
|
- **Import Errors**: Check component dependencies and `PYTHONPATH`.
|
||||||
@@ -657,7 +645,7 @@ This document provides essential context for AI models interacting with this pro
|
|||||||
If you need a real-world example, search for components that use `@dataclass` with `CORE.data` in the codebase. Note: Some components may use `TypedDict` for dictionary-based storage; both patterns are acceptable depending on your needs.
|
If you need a real-world example, search for components that use `@dataclass` with `CORE.data` in the codebase. Note: Some components may use `TypedDict` for dictionary-based storage; both patterns are acceptable depending on your needs.
|
||||||
|
|
||||||
**Why this matters:**
|
**Why this matters:**
|
||||||
- Module-level globals persist between compilation runs if the host process (e.g. device-builder) doesn't fork/exec
|
- Module-level globals persist between compilation runs if the dashboard doesn't fork/exec
|
||||||
- `CORE.data` automatically clears between runs
|
- `CORE.data` automatically clears between runs
|
||||||
- Namespacing under `DOMAIN` prevents key collisions between components
|
- Namespacing under `DOMAIN` prevents key collisions between components
|
||||||
- `@dataclass` provides type safety and cleaner attribute access
|
- `@dataclass` provides type safety and cleaner attribute access
|
||||||
@@ -710,9 +698,3 @@ This document provides essential context for AI models interacting with this pro
|
|||||||
_LOGGER.warning(f"'{CONF_OLD_KEY}' deprecated, use '{CONF_NEW_KEY}'. Removed in 2026.6.0")
|
_LOGGER.warning(f"'{CONF_OLD_KEY}' deprecated, use '{CONF_NEW_KEY}'. Removed in 2026.6.0")
|
||||||
config[CONF_NEW_KEY] = config.pop(CONF_OLD_KEY) # Auto-migrate
|
config[CONF_NEW_KEY] = config.pop(CONF_OLD_KEY) # Auto-migrate
|
||||||
```
|
```
|
||||||
## 9. English Language
|
|
||||||
|
|
||||||
The project uses English for non-code content. When drafting documentation, code comments, commit messages,
|
|
||||||
PR descriptions, and similar text, avoid technical jargon. Instead, express concepts in plain English,
|
|
||||||
using standard technical terms only when required. Ensure the text is readily comprehensible to a wide
|
|
||||||
audience, including non-native English speakers.
|
|
||||||
|
|||||||
+5
-21
@@ -19,6 +19,7 @@ esphome/components/ac_dimmer/* @glmnet
|
|||||||
esphome/components/adc/* @esphome/core
|
esphome/components/adc/* @esphome/core
|
||||||
esphome/components/adc128s102/* @DeerMaximum
|
esphome/components/adc128s102/* @DeerMaximum
|
||||||
esphome/components/addressable_light/* @justfalter
|
esphome/components/addressable_light/* @justfalter
|
||||||
|
esphome/components/ade7880/* @kpfleming
|
||||||
esphome/components/ade7953/* @angelnu
|
esphome/components/ade7953/* @angelnu
|
||||||
esphome/components/ade7953_base/* @angelnu
|
esphome/components/ade7953_base/* @angelnu
|
||||||
esphome/components/ade7953_i2c/* @angelnu
|
esphome/components/ade7953_i2c/* @angelnu
|
||||||
@@ -27,7 +28,7 @@ esphome/components/ads1118/* @solomondg1
|
|||||||
esphome/components/ags10/* @mak-42
|
esphome/components/ags10/* @mak-42
|
||||||
esphome/components/aic3204/* @kbx81
|
esphome/components/aic3204/* @kbx81
|
||||||
esphome/components/airthings_ble/* @jeromelaban
|
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_mini/* @ncareau
|
||||||
esphome/components/airthings_wave_plus/* @jeromelaban @precurse
|
esphome/components/airthings_wave_plus/* @jeromelaban @precurse
|
||||||
esphome/components/alarm_control_panel/* @grahambrown11 @hwstar
|
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/* @kbx81 @neffs
|
||||||
esphome/components/bme68x_bsec2_i2c/* @kbx81 @neffs
|
esphome/components/bme68x_bsec2_i2c/* @kbx81 @neffs
|
||||||
esphome/components/bmi160/* @flaviut
|
esphome/components/bmi160/* @flaviut
|
||||||
esphome/components/bmi270/* @clydebarrow
|
|
||||||
esphome/components/bmp280_base/* @ademuri
|
esphome/components/bmp280_base/* @ademuri
|
||||||
esphome/components/bmp280_i2c/* @ademuri
|
esphome/components/bmp280_i2c/* @ademuri
|
||||||
esphome/components/bmp280_spi/* @ademuri
|
esphome/components/bmp280_spi/* @ademuri
|
||||||
@@ -122,9 +122,7 @@ esphome/components/cover/* @esphome/core
|
|||||||
esphome/components/cs5460a/* @balrog-kun
|
esphome/components/cs5460a/* @balrog-kun
|
||||||
esphome/components/cse7761/* @berfenger
|
esphome/components/cse7761/* @berfenger
|
||||||
esphome/components/cst226/* @clydebarrow
|
esphome/components/cst226/* @clydebarrow
|
||||||
esphome/components/cst328/* @latonita
|
|
||||||
esphome/components/cst816/* @clydebarrow
|
esphome/components/cst816/* @clydebarrow
|
||||||
esphome/components/cst9220/* @clydebarrow
|
|
||||||
esphome/components/ct_clamp/* @jesserockz
|
esphome/components/ct_clamp/* @jesserockz
|
||||||
esphome/components/current_based/* @djwmarcx
|
esphome/components/current_based/* @djwmarcx
|
||||||
esphome/components/dac7678/* @NickB1
|
esphome/components/dac7678/* @NickB1
|
||||||
@@ -141,11 +139,10 @@ esphome/components/dfplayer/* @glmnet
|
|||||||
esphome/components/dfrobot_sen0395/* @niklasweber
|
esphome/components/dfrobot_sen0395/* @niklasweber
|
||||||
esphome/components/dht/* @OttoWinter
|
esphome/components/dht/* @OttoWinter
|
||||||
esphome/components/display_menu_base/* @numo68
|
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/dps310/* @kbx81
|
||||||
esphome/components/ds1307/* @badbadc0ffee
|
esphome/components/ds1307/* @badbadc0ffee
|
||||||
esphome/components/ds2484/* @mrk-its
|
esphome/components/ds2484/* @mrk-its
|
||||||
esphome/components/ds248x/* @tomwellnitz
|
|
||||||
esphome/components/dsmr/* @glmnet @PolarGoose
|
esphome/components/dsmr/* @glmnet @PolarGoose
|
||||||
esphome/components/duty_time/* @dudanov
|
esphome/components/duty_time/* @dudanov
|
||||||
esphome/components/ee895/* @Stock-M
|
esphome/components/ee895/* @Stock-M
|
||||||
@@ -188,7 +185,6 @@ esphome/components/ezo_pmp/* @carlos-sarmiento
|
|||||||
esphome/components/factory_reset/* @anatoly-savchenkov
|
esphome/components/factory_reset/* @anatoly-savchenkov
|
||||||
esphome/components/fastled_base/* @OttoWinter
|
esphome/components/fastled_base/* @OttoWinter
|
||||||
esphome/components/feedback/* @ianchi
|
esphome/components/feedback/* @ianchi
|
||||||
esphome/components/file/* @esphome/core
|
|
||||||
esphome/components/fingerprint_grow/* @alexborro @loongyh @OnFreund
|
esphome/components/fingerprint_grow/* @alexborro @loongyh @OnFreund
|
||||||
esphome/components/font/* @clydebarrow @esphome/core
|
esphome/components/font/* @clydebarrow @esphome/core
|
||||||
esphome/components/fs3000/* @kahrendt
|
esphome/components/fs3000/* @kahrendt
|
||||||
@@ -210,7 +206,6 @@ esphome/components/gree/switch/* @nagyrobi
|
|||||||
esphome/components/grove_gas_mc_v2/* @YorkshireIoT
|
esphome/components/grove_gas_mc_v2/* @YorkshireIoT
|
||||||
esphome/components/grove_tb6612fng/* @max246
|
esphome/components/grove_tb6612fng/* @max246
|
||||||
esphome/components/growatt_solar/* @leeuwte
|
esphome/components/growatt_solar/* @leeuwte
|
||||||
esphome/components/gsl3670/* @clydebarrow
|
|
||||||
esphome/components/gt911/* @clydebarrow @jesserockz
|
esphome/components/gt911/* @clydebarrow @jesserockz
|
||||||
esphome/components/haier/* @paveldn
|
esphome/components/haier/* @paveldn
|
||||||
esphome/components/haier/binary_sensor/* @paveldn
|
esphome/components/haier/binary_sensor/* @paveldn
|
||||||
@@ -271,7 +266,6 @@ esphome/components/integration/* @OttoWinter
|
|||||||
esphome/components/internal_temperature/* @Mat931
|
esphome/components/internal_temperature/* @Mat931
|
||||||
esphome/components/interval/* @esphome/core
|
esphome/components/interval/* @esphome/core
|
||||||
esphome/components/ir_rf_proxy/* @kbx81
|
esphome/components/ir_rf_proxy/* @kbx81
|
||||||
esphome/components/it8951/* @koosoli @limengdu @Passific
|
|
||||||
esphome/components/jsn_sr04t/* @Mafus1
|
esphome/components/jsn_sr04t/* @Mafus1
|
||||||
esphome/components/json/* @esphome/core
|
esphome/components/json/* @esphome/core
|
||||||
esphome/components/kamstrup_kmp/* @cfeenstra1024
|
esphome/components/kamstrup_kmp/* @cfeenstra1024
|
||||||
@@ -297,7 +291,6 @@ esphome/components/lock/* @esphome/core
|
|||||||
esphome/components/logger/* @esphome/core
|
esphome/components/logger/* @esphome/core
|
||||||
esphome/components/logger/select/* @clydebarrow
|
esphome/components/logger/select/* @clydebarrow
|
||||||
esphome/components/lps22/* @nagisa
|
esphome/components/lps22/* @nagisa
|
||||||
esphome/components/lsm6ds/* @clydebarrow
|
|
||||||
esphome/components/ltr390/* @latonita @sjtrny
|
esphome/components/ltr390/* @latonita @sjtrny
|
||||||
esphome/components/ltr501/* @latonita
|
esphome/components/ltr501/* @latonita
|
||||||
esphome/components/ltr_als_ps/* @latonita
|
esphome/components/ltr_als_ps/* @latonita
|
||||||
@@ -358,7 +351,6 @@ esphome/components/modbus_server/* @exciton
|
|||||||
esphome/components/mopeka_ble/* @Fabian-Schmidt @spbrogan
|
esphome/components/mopeka_ble/* @Fabian-Schmidt @spbrogan
|
||||||
esphome/components/mopeka_pro_check/* @spbrogan
|
esphome/components/mopeka_pro_check/* @spbrogan
|
||||||
esphome/components/mopeka_std_check/* @Fabian-Schmidt
|
esphome/components/mopeka_std_check/* @Fabian-Schmidt
|
||||||
esphome/components/motion/* @esphome/core
|
|
||||||
esphome/components/mpl3115a2/* @kbickar
|
esphome/components/mpl3115a2/* @kbickar
|
||||||
esphome/components/mpu6886/* @fabaff
|
esphome/components/mpu6886/* @fabaff
|
||||||
esphome/components/ms8607/* @e28eta
|
esphome/components/ms8607/* @e28eta
|
||||||
@@ -387,11 +379,9 @@ esphome/components/pca6416a/* @Mat931
|
|||||||
esphome/components/pca9554/* @bdraco @clydebarrow @hwstar
|
esphome/components/pca9554/* @bdraco @clydebarrow @hwstar
|
||||||
esphome/components/pcf85063/* @brogon
|
esphome/components/pcf85063/* @brogon
|
||||||
esphome/components/pcf8563/* @KoenBreeman
|
esphome/components/pcf8563/* @KoenBreeman
|
||||||
esphome/components/pcm5122/* @remcom
|
|
||||||
esphome/components/pi4ioe5v6408/* @jesserockz
|
esphome/components/pi4ioe5v6408/* @jesserockz
|
||||||
esphome/components/pid/* @OttoWinter
|
esphome/components/pid/* @OttoWinter
|
||||||
esphome/components/pipsolar/* @andreashergert1984
|
esphome/components/pipsolar/* @andreashergert1984
|
||||||
esphome/components/pixoo/* @jesserockz
|
|
||||||
esphome/components/pm1006/* @habbie
|
esphome/components/pm1006/* @habbie
|
||||||
esphome/components/pm2005/* @andrewjswan
|
esphome/components/pm2005/* @andrewjswan
|
||||||
esphome/components/pmsa003i/* @sjtrny
|
esphome/components/pmsa003i/* @sjtrny
|
||||||
@@ -407,12 +397,10 @@ esphome/components/pn7160_i2c/* @jesserockz @kbx81
|
|||||||
esphome/components/pn7160_spi/* @jesserockz @kbx81
|
esphome/components/pn7160_spi/* @jesserockz @kbx81
|
||||||
esphome/components/power_supply/* @esphome/core
|
esphome/components/power_supply/* @esphome/core
|
||||||
esphome/components/preferences/* @esphome/core
|
esphome/components/preferences/* @esphome/core
|
||||||
esphome/components/provisioning/* @esphome/core
|
|
||||||
esphome/components/psram/* @esphome/core
|
esphome/components/psram/* @esphome/core
|
||||||
esphome/components/pulse_meter/* @cstaahl @stevebaxter @TrentHouliston
|
esphome/components/pulse_meter/* @cstaahl @stevebaxter @TrentHouliston
|
||||||
esphome/components/pvvx_mithermometer/* @pasiz
|
esphome/components/pvvx_mithermometer/* @pasiz
|
||||||
esphome/components/pylontech/* @functionpointer
|
esphome/components/pylontech/* @functionpointer
|
||||||
esphome/components/qmi8658/* @clydebarrow
|
|
||||||
esphome/components/qmp6988/* @andrewpc
|
esphome/components/qmp6988/* @andrewpc
|
||||||
esphome/components/qr_code/* @wjtje
|
esphome/components/qr_code/* @wjtje
|
||||||
esphome/components/qspi_dbi/* @clydebarrow
|
esphome/components/qspi_dbi/* @clydebarrow
|
||||||
@@ -430,7 +418,7 @@ esphome/components/rf_bridge/* @jesserockz
|
|||||||
esphome/components/rgbct/* @jesserockz
|
esphome/components/rgbct/* @jesserockz
|
||||||
esphome/components/ring_buffer/* @kahrendt
|
esphome/components/ring_buffer/* @kahrendt
|
||||||
esphome/components/router/speaker/* @kahrendt
|
esphome/components/router/speaker/* @kahrendt
|
||||||
esphome/components/rp2/* @jesserockz
|
esphome/components/rp2040/* @jesserockz
|
||||||
esphome/components/rp2040_ble/* @bdraco
|
esphome/components/rp2040_ble/* @bdraco
|
||||||
esphome/components/rp2040_pio_led_strip/* @Papa-DMan
|
esphome/components/rp2040_pio_led_strip/* @Papa-DMan
|
||||||
esphome/components/rp2040_pwm/* @jesserockz
|
esphome/components/rp2040_pwm/* @jesserockz
|
||||||
@@ -454,7 +442,7 @@ esphome/components/select/* @esphome/core
|
|||||||
esphome/components/sen0321/* @notjj
|
esphome/components/sen0321/* @notjj
|
||||||
esphome/components/sen21231/* @shreyaskarnik
|
esphome/components/sen21231/* @shreyaskarnik
|
||||||
esphome/components/sen5x/* @martgras
|
esphome/components/sen5x/* @martgras
|
||||||
esphome/components/sen6x/* @martgras @mebner86 @tuct
|
esphome/components/sen6x/* @martgras @mebner86 @mikelawrence @tuct
|
||||||
esphome/components/sendspin/* @kahrendt
|
esphome/components/sendspin/* @kahrendt
|
||||||
esphome/components/sendspin/media_player/* @kahrendt
|
esphome/components/sendspin/media_player/* @kahrendt
|
||||||
esphome/components/sendspin/media_source/* @kahrendt
|
esphome/components/sendspin/media_source/* @kahrendt
|
||||||
@@ -506,7 +494,6 @@ esphome/components/ssd1331_base/* @kbx81
|
|||||||
esphome/components/ssd1331_spi/* @kbx81
|
esphome/components/ssd1331_spi/* @kbx81
|
||||||
esphome/components/ssd1351_base/* @kbx81
|
esphome/components/ssd1351_base/* @kbx81
|
||||||
esphome/components/ssd1351_spi/* @kbx81
|
esphome/components/ssd1351_spi/* @kbx81
|
||||||
esphome/components/st7123/* @miniskipper
|
|
||||||
esphome/components/st7567_base/* @latonita
|
esphome/components/st7567_base/* @latonita
|
||||||
esphome/components/st7567_i2c/* @latonita
|
esphome/components/st7567_i2c/* @latonita
|
||||||
esphome/components/st7567_spi/* @latonita
|
esphome/components/st7567_spi/* @latonita
|
||||||
@@ -571,7 +558,6 @@ esphome/components/uart/packet_transport/* @clydebarrow
|
|||||||
esphome/components/udp/* @clydebarrow
|
esphome/components/udp/* @clydebarrow
|
||||||
esphome/components/ufire_ec/* @pvizeli
|
esphome/components/ufire_ec/* @pvizeli
|
||||||
esphome/components/ufire_ise/* @pvizeli
|
esphome/components/ufire_ise/* @pvizeli
|
||||||
esphome/components/ufm01/* @ljungqvist
|
|
||||||
esphome/components/ultrasonic/* @ssieb @swoboda1337
|
esphome/components/ultrasonic/* @ssieb @swoboda1337
|
||||||
esphome/components/update/* @jesserockz
|
esphome/components/update/* @jesserockz
|
||||||
esphome/components/uponor_smatrix/* @kroimon
|
esphome/components/uponor_smatrix/* @kroimon
|
||||||
@@ -588,7 +574,6 @@ esphome/components/wake_on_lan/* @clydebarrow @willwill2will54
|
|||||||
esphome/components/watchdog/* @oarcher
|
esphome/components/watchdog/* @oarcher
|
||||||
esphome/components/water_heater/* @dhoeben
|
esphome/components/water_heater/* @dhoeben
|
||||||
esphome/components/waveshare_epaper/* @clydebarrow
|
esphome/components/waveshare_epaper/* @clydebarrow
|
||||||
esphome/components/waveshare_io_ch32v003/* @latonita
|
|
||||||
esphome/components/web_server/ota/* @esphome/core
|
esphome/components/web_server/ota/* @esphome/core
|
||||||
esphome/components/web_server_base/* @esphome/core
|
esphome/components/web_server_base/* @esphome/core
|
||||||
esphome/components/web_server_idf/* @dentra
|
esphome/components/web_server_idf/* @dentra
|
||||||
@@ -610,7 +595,6 @@ esphome/components/wk2212_spi/* @DrCoolZic
|
|||||||
esphome/components/wl_134/* @hobbypunk90
|
esphome/components/wl_134/* @hobbypunk90
|
||||||
esphome/components/wts01/* @alepee
|
esphome/components/wts01/* @alepee
|
||||||
esphome/components/x9c/* @EtienneMD
|
esphome/components/x9c/* @EtienneMD
|
||||||
esphome/components/xdb401/* @RT530
|
|
||||||
esphome/components/xgzp68xx/* @gcormier
|
esphome/components/xgzp68xx/* @gcormier
|
||||||
esphome/components/xiaomi_hhccjcy10/* @fariouche
|
esphome/components/xiaomi_hhccjcy10/* @fariouche
|
||||||
esphome/components/xiaomi_lywsd02mmc/* @juanluss31
|
esphome/components/xiaomi_lywsd02mmc/* @juanluss31
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
|||||||
# could be handy for archiving the generated documentation or if some version
|
# could be handy for archiving the generated documentation or if some version
|
||||||
# control system is used.
|
# control system is used.
|
||||||
|
|
||||||
PROJECT_NUMBER = 2026.8.0-dev
|
PROJECT_NUMBER = 2026.6.0-dev
|
||||||
|
|
||||||
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
# 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
|
# for a project that appears at the top of each page and should give viewer a
|
||||||
|
|||||||
@@ -6,4 +6,3 @@ recursive-include esphome *.cpp *.h *.tcc *.c
|
|||||||
recursive-include esphome *.py.script
|
recursive-include esphome *.py.script
|
||||||
recursive-include esphome *.jinja
|
recursive-include esphome *.jinja
|
||||||
recursive-include esphome LICENSE.txt
|
recursive-include esphome LICENSE.txt
|
||||||
recursive-include esphome requirements.txt
|
|
||||||
|
|||||||
-148
@@ -1,148 +0,0 @@
|
|||||||
# ESPHome Threat Model
|
|
||||||
|
|
||||||
This document defines the trust boundary for the **ESPHome** repository — the
|
|
||||||
Python compiler/CLI and the device firmware it generates — so that real security
|
|
||||||
bugs can be told apart from defense-in-depth improvements. It gives contributors,
|
|
||||||
reviewers, and security researchers a clear answer to one question:
|
|
||||||
**does this issue let an _unauthenticated_ attacker do something they shouldn't?**
|
|
||||||
|
|
||||||
Related documents:
|
|
||||||
|
|
||||||
- Deployment guidance for operators:
|
|
||||||
https://esphome.io/guides/security_best_practices/
|
|
||||||
- The **Device Builder dashboard** (the web UI, its authentication, ingress,
|
|
||||||
Origin/Host gates, and peer-link pairing) lives in a separate repository and
|
|
||||||
has its own threat model. If your report concerns any of that, please read and
|
|
||||||
report there instead:
|
|
||||||
https://github.com/esphome/device-builder/blob/main/docs/THREAT_MODEL.md
|
|
||||||
|
|
||||||
## The trust boundary
|
|
||||||
|
|
||||||
For this repository there are two trusted inputs by design:
|
|
||||||
|
|
||||||
1. **The configuration.** Anyone who can supply or edit a YAML config is trusted
|
|
||||||
(see below).
|
|
||||||
2. **Authenticated peers of a running device** — clients holding the device's
|
|
||||||
API encryption key / password, OTA password, or web server credentials.
|
|
||||||
|
|
||||||
The security boundary is therefore **unauthenticated network traffic vs. those
|
|
||||||
trusted inputs.** A bug that lets an unauthenticated attacker cross it is a
|
|
||||||
security bug.
|
|
||||||
|
|
||||||
## Config authors are host-equivalent by design
|
|
||||||
|
|
||||||
Anyone who can supply or edit a configuration is **trusted with full code
|
|
||||||
execution on the host that runs `esphome`**, on purpose. This is what the product
|
|
||||||
does, not a flaw. A config author can already, through fully supported features:
|
|
||||||
|
|
||||||
- Run arbitrary **Python** at validation/compile time via `external_components:`
|
|
||||||
(and other component-import mechanisms) — ESPHome imports those packages as
|
|
||||||
ordinary Python.
|
|
||||||
- Run arbitrary **shell** commands through the compile/validate/flash toolchain
|
|
||||||
that ESPHome invokes as subprocesses.
|
|
||||||
- Read and write arbitrary files reachable by the process (e.g. via `!include`,
|
|
||||||
`packages:`, `dashboard_import:`, and generated build output).
|
|
||||||
|
|
||||||
Because of this, a malicious config author is equivalent to shell access on the
|
|
||||||
host running the build.
|
|
||||||
|
|
||||||
## What is *not* a security vulnerability
|
|
||||||
|
|
||||||
If exploiting an issue requires the ability to supply or edit configuration, it
|
|
||||||
is **not** a vulnerability in ESPHome, because that ability already grants host
|
|
||||||
code execution. This explicitly includes, among others:
|
|
||||||
|
|
||||||
- Template / expression injection in substitutions or any YAML string value
|
|
||||||
(e.g. Jinja `${...}` evaluation reaching Python internals). This grants no
|
|
||||||
capability a config author lacks.
|
|
||||||
- `!include` / `packages:` / `dashboard_import:` reading or fetching content
|
|
||||||
from surprising or remote locations.
|
|
||||||
- The validator or compiler crashing or behaving unexpectedly on adversarial
|
|
||||||
YAML.
|
|
||||||
- ESPHome running as root in the official container — that is the documented
|
|
||||||
deployment posture, reachable by the same caller through the features above.
|
|
||||||
|
|
||||||
These do not warrant a CVE or coordinated disclosure. Hardening in these areas
|
|
||||||
(for example, sandboxing template evaluation as least-surprise defense-in-depth)
|
|
||||||
is welcome as a normal enhancement PR, framed as cleanliness rather than a
|
|
||||||
security fix — not as a vulnerability remediation.
|
|
||||||
|
|
||||||
## What we do defend
|
|
||||||
|
|
||||||
These *are* security bugs in this repo, and we want to hear about them privately:
|
|
||||||
|
|
||||||
- Memory-safety or protocol bugs in the generated **device firmware** that are
|
|
||||||
remotely triggerable over the network (native API, web server, OTA, BLE,
|
|
||||||
captive portal, etc.) **without** valid credentials.
|
|
||||||
- Authentication or encryption bypass on the device — reaching API calls, OTA
|
|
||||||
updates, or the web server without the configured key/password.
|
|
||||||
- Flaws that weaken the device's API encryption (Noise), OTA, or web server auth
|
|
||||||
below their documented guarantees.
|
|
||||||
|
|
||||||
## The web server is an open HTTP API by design
|
|
||||||
|
|
||||||
The `web_server` component exposes a plain HTTP interface for viewing and
|
|
||||||
controlling entities, and, when the `web_server` OTA platform is enabled, for
|
|
||||||
uploading firmware at `/update`. Its only access controls are the optional
|
|
||||||
`web_server` `auth:` credentials and the network the device sits on.
|
|
||||||
|
|
||||||
When `auth:` is not configured, every endpoint is reachable by any client that
|
|
||||||
can reach the device. This is intentional; enabling `web_server` without `auth:`
|
|
||||||
is choosing an open control surface, in the same way that running native OTA
|
|
||||||
without a password leaves OTA open. The API is documented and is meant to be
|
|
||||||
called by other devices, scripts, and pages.
|
|
||||||
|
|
||||||
As defense-in-depth, the web server checks the `Origin` header on browser requests
|
|
||||||
to its entity control and state endpoints: a request whose `Origin` does not match
|
|
||||||
the address the device is served on is rejected, and the `allowed_origins` option
|
|
||||||
widens that list. This blocks the common "confused deputy" (CSRF) case where a page
|
|
||||||
the operator visits drives the device through their browser. It is **not** an
|
|
||||||
authentication boundary: it only constrains browsers. Any client that omits the
|
|
||||||
`Origin` header — `curl`, scripts, or other non-browser callers on the same
|
|
||||||
network — reaches every endpoint exactly as before. The check also does not cover
|
|
||||||
the web OTA `/update` endpoint. The device performs no CSRF-token or `Referer`
|
|
||||||
validation. The following are therefore **not** vulnerabilities in this repository:
|
|
||||||
|
|
||||||
- Requests without an `Origin` header (for example `curl`) reaching the control
|
|
||||||
endpoints, whether or not `web_server` `auth:` is set.
|
|
||||||
- Requests from an origin the operator added to `allowed_origins`.
|
|
||||||
- Cross-origin or CSRF firmware upload through the web OTA endpoint (`/update`) when
|
|
||||||
web OTA is enabled without `web_server` `auth:`. The `/update` endpoint is not
|
|
||||||
covered by the `Origin` check; this is the same exposure as running OTA without a
|
|
||||||
password.
|
|
||||||
|
|
||||||
The supported defenses are `web_server` `auth:`, protecting OTA (a web password or
|
|
||||||
a native OTA password), and keeping devices on a trusted, segmented network. See
|
|
||||||
the security best practices guide linked above.
|
|
||||||
|
|
||||||
What remains in scope is bypassing `web_server` `auth:` when it *is* configured,
|
|
||||||
and any memory-safety or protocol bug in the server reachable without credentials.
|
|
||||||
|
|
||||||
This section documents the current design and scope; it is not a judgment that the
|
|
||||||
design is optimal or that it will not change.
|
|
||||||
|
|
||||||
## Explicitly out of scope
|
|
||||||
|
|
||||||
- Local attackers who already have shell access on the host that runs `esphome`.
|
|
||||||
- Supply-chain attacks against ESPHome or its dependencies.
|
|
||||||
- Operator-supplied hostile YAML (covered above — config authoring is trusted).
|
|
||||||
- Attacks that require an already-authenticated device peer (someone who already
|
|
||||||
holds the API key / OTA / web credentials).
|
|
||||||
- Access to the device web server or its web OTA endpoint by non-browser clients
|
|
||||||
(those that send no `Origin` header). The web server is an open HTTP API by
|
|
||||||
design (see above); browser cross-origin requests are blocked by default, but the
|
|
||||||
real controls are `web_server` `auth:` and network isolation.
|
|
||||||
- Anything in the dashboard / device-builder — report that in its own repository
|
|
||||||
(linked at the top).
|
|
||||||
- Deployments where the operator removed protections or exposed credentials. See
|
|
||||||
the security best practices guide:
|
|
||||||
https://esphome.io/guides/security_best_practices/
|
|
||||||
|
|
||||||
## Reporting a vulnerability
|
|
||||||
|
|
||||||
If you believe you've found an issue that crosses the unauthenticated boundary
|
|
||||||
above, please report it privately via GitHub Security Advisories rather than a
|
|
||||||
public issue. For issues that require config-write access, please review this
|
|
||||||
document first — they are very likely out of scope by design. For dashboard /
|
|
||||||
device-builder issues, report against that repository and consult its threat
|
|
||||||
model (linked at the top).
|
|
||||||
-18
@@ -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
|
|
||||||
+18
-6
@@ -1,9 +1,10 @@
|
|||||||
ARG BUILD_VERSION=dev
|
ARG BUILD_VERSION=dev
|
||||||
ARG BUILD_BASE_VERSION=2026.06.1
|
ARG BUILD_OS=alpine
|
||||||
|
ARG BUILD_BASE_VERSION=2025.04.0
|
||||||
ARG BUILD_TYPE=docker
|
ARG BUILD_TYPE=docker
|
||||||
|
|
||||||
FROM ghcr.io/esphome/docker-base:debian-${BUILD_BASE_VERSION} AS base-source-docker
|
FROM ghcr.io/esphome/docker-base:${BUILD_OS}-${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}-ha-addon-${BUILD_BASE_VERSION} AS base-source-ha-addon
|
||||||
|
|
||||||
ARG BUILD_TYPE
|
ARG BUILD_TYPE
|
||||||
FROM base-source-${BUILD_TYPE} AS base
|
FROM base-source-${BUILD_TYPE} AS base
|
||||||
@@ -11,6 +12,20 @@ FROM base-source-${BUILD_TYPE} AS base
|
|||||||
RUN git config --system --add safe.directory "*" \
|
RUN git config --system --add safe.directory "*" \
|
||||||
&& git config --system advice.detachedHead false
|
&& 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 if command -v apk > /dev/null; then \
|
||||||
|
apk add --no-cache build-base libusb; \
|
||||||
|
else \
|
||||||
|
apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends build-essential libusb-1.0-0 \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*; \
|
||||||
|
fi
|
||||||
|
|
||||||
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
|
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||||
|
|
||||||
RUN pip install --no-cache-dir -U pip uv==0.10.1
|
RUN pip install --no-cache-dir -U pip uv==0.10.1
|
||||||
@@ -21,9 +36,6 @@ RUN \
|
|||||||
uv pip install --no-cache-dir \
|
uv pip install --no-cache-dir \
|
||||||
-r /requirements.txt
|
-r /requirements.txt
|
||||||
|
|
||||||
# Install the ESPHome Device Builder dashboard.
|
|
||||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.6.4
|
|
||||||
|
|
||||||
RUN \
|
RUN \
|
||||||
platformio settings set enable_telemetry No \
|
platformio settings set enable_telemetry No \
|
||||||
&& platformio settings set check_platformio_interval 1000000 \
|
&& platformio settings set check_platformio_interval 1000000 \
|
||||||
|
|||||||
+13
-42
@@ -20,10 +20,6 @@ TYPE_HA_ADDON = "ha-addon"
|
|||||||
TYPE_LINT = "lint"
|
TYPE_LINT = "lint"
|
||||||
TYPES = [TYPE_DOCKER, TYPE_HA_ADDON, TYPE_LINT]
|
TYPES = [TYPE_DOCKER, TYPE_HA_ADDON, TYPE_LINT]
|
||||||
|
|
||||||
REGISTRY_GHCR = "ghcr"
|
|
||||||
REGISTRY_DOCKERHUB = "dockerhub"
|
|
||||||
REGISTRIES = [REGISTRY_GHCR, REGISTRY_DOCKERHUB]
|
|
||||||
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
@@ -38,12 +34,6 @@ parser.add_argument(
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--build-type", choices=TYPES, required=True, help="The type of build to run"
|
"--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(
|
parser.add_argument(
|
||||||
"--dry-run", action="store_true", help="Don't run any commands, just print them"
|
"--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(
|
build_parser.add_argument(
|
||||||
"--load", help="Load the docker image locally", action="store_true"
|
"--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_parser = subparsers.add_parser(
|
||||||
"manifest", help="Create a manifest from already pushed images"
|
"manifest", help="Create a manifest from already pushed images"
|
||||||
)
|
)
|
||||||
@@ -110,14 +95,11 @@ def main():
|
|||||||
print("Command failed")
|
print("Command failed")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
registries = args.registry or REGISTRIES
|
|
||||||
|
|
||||||
# detect channel from tag
|
# detect channel from tag
|
||||||
match = re.match(r"^(\d+\.\d+)(?:\.\d+)?(b\d+)?$", args.tag)
|
match = re.match(r"^(\d+\.\d+)(?:\.\d+)?(b\d+)?$", args.tag)
|
||||||
major_minor_version = None
|
major_minor_version = None
|
||||||
if match is None:
|
if match is None:
|
||||||
# Custom tag (e.g. a branch name) -- push only the tag itself
|
channel = CHANNEL_DEV
|
||||||
channel = None
|
|
||||||
elif match.group(2) is None:
|
elif match.group(2) is None:
|
||||||
major_minor_version = match.group(1)
|
major_minor_version = match.group(1)
|
||||||
channel = CHANNEL_RELEASE
|
channel = CHANNEL_RELEASE
|
||||||
@@ -146,18 +128,11 @@ def main():
|
|||||||
CHANNEL_DEV: "cache-dev",
|
CHANNEL_DEV: "cache-dev",
|
||||||
CHANNEL_BETA: "cache-beta",
|
CHANNEL_BETA: "cache-beta",
|
||||||
CHANNEL_RELEASE: "cache-latest",
|
CHANNEL_RELEASE: "cache-latest",
|
||||||
}.get(channel, "cache-dev")
|
}[channel]
|
||||||
# Cache images live alongside the pushed images; prefer GHCR when it is
|
cache_img = f"ghcr.io/{params.build_to}:{cache_tag}"
|
||||||
# one of the selected registries, otherwise fall back to Docker Hub so a
|
|
||||||
# registry-restricted build doesn't need GHCR auth.
|
|
||||||
cache_prefix = "ghcr.io/" if REGISTRY_GHCR in registries else ""
|
|
||||||
cache_img = f"{cache_prefix}{params.build_to}:{cache_tag}"
|
|
||||||
|
|
||||||
imgs = []
|
imgs = [f"{params.build_to}:{tag}" for tag in tags_to_push]
|
||||||
if REGISTRY_DOCKERHUB 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]
|
|
||||||
if REGISTRY_GHCR in registries:
|
|
||||||
imgs += [f"ghcr.io/{params.build_to}:{tag}" for tag in tags_to_push]
|
|
||||||
|
|
||||||
# 3. build
|
# 3. build
|
||||||
cmd = [
|
cmd = [
|
||||||
@@ -180,9 +155,7 @@ def main():
|
|||||||
for img in imgs:
|
for img in imgs:
|
||||||
cmd += ["--tag", img]
|
cmd += ["--tag", img]
|
||||||
if args.push:
|
if args.push:
|
||||||
cmd += ["--push"]
|
cmd += ["--push", "--cache-to", f"type=registry,ref={cache_img},mode=max"]
|
||||||
if not args.no_cache_to:
|
|
||||||
cmd += ["--cache-to", f"type=registry,ref={cache_img},mode=max"]
|
|
||||||
if args.load:
|
if args.load:
|
||||||
cmd += ["--load"]
|
cmd += ["--load"]
|
||||||
|
|
||||||
@@ -190,22 +163,20 @@ def main():
|
|||||||
elif args.command == "manifest":
|
elif args.command == "manifest":
|
||||||
manifest = DockerParams.for_type_arch(args.build_type, ARCH_AMD64).manifest_to
|
manifest = DockerParams.for_type_arch(args.build_type, ARCH_AMD64).manifest_to
|
||||||
|
|
||||||
targets = []
|
targets = [f"{manifest}:{tag}" for tag in tags_to_push]
|
||||||
if REGISTRY_DOCKERHUB in registries:
|
targets += [f"ghcr.io/{manifest}:{tag}" for tag in tags_to_push]
|
||||||
targets += [f"{manifest}:{tag}" for tag in tags_to_push]
|
# 1. Create manifests
|
||||||
if REGISTRY_GHCR in registries:
|
|
||||||
targets += [f"ghcr.io/{manifest}:{tag}" for tag in tags_to_push]
|
|
||||||
# Use buildx imagetools (not `docker manifest`) so the per-arch sources,
|
|
||||||
# which buildx pushes as single-platform manifest lists, are combined
|
|
||||||
# and pushed correctly in one step.
|
|
||||||
for target in targets:
|
for target in targets:
|
||||||
cmd = ["docker", "buildx", "imagetools", "create", "--tag", target]
|
cmd = ["docker", "manifest", "create", target]
|
||||||
for arch in ARCHS:
|
for arch in ARCHS:
|
||||||
src = f"{DockerParams.for_type_arch(args.build_type, arch).build_to}:{args.tag}"
|
src = f"{DockerParams.for_type_arch(args.build_type, arch).build_to}:{args.tag}"
|
||||||
if target.startswith("ghcr.io"):
|
if target.startswith("ghcr.io"):
|
||||||
src = f"ghcr.io/{src}"
|
src = f"ghcr.io/{src}"
|
||||||
cmd.append(src)
|
cmd.append(src)
|
||||||
run_command(*cmd)
|
run_command(*cmd)
|
||||||
|
# 2. Push manifests
|
||||||
|
for target in targets:
|
||||||
|
run_command("docker", "manifest", "push", target)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -21,23 +21,10 @@ export PLATFORMIO_PLATFORMS_DIR="${pio_cache_base}/platforms"
|
|||||||
export PLATFORMIO_PACKAGES_DIR="${pio_cache_base}/packages"
|
export PLATFORMIO_PACKAGES_DIR="${pio_cache_base}/packages"
|
||||||
export PLATFORMIO_CACHE_DIR="${pio_cache_base}/cache"
|
export PLATFORMIO_CACHE_DIR="${pio_cache_base}/cache"
|
||||||
|
|
||||||
# Keep the native toolchain installs on the persistent cache root, not the
|
|
||||||
# container's ephemeral user cache dir (re-downloaded on every restart).
|
|
||||||
export ESPHOME_ESP_IDF_PREFIX="$(dirname "${pio_cache_base}")/idf"
|
|
||||||
export ESPHOME_SDK_NRF_PREFIX="$(dirname "${pio_cache_base}")/sdk-nrf"
|
|
||||||
|
|
||||||
# If /build is mounted, use that as the build path
|
# If /build is mounted, use that as the build path
|
||||||
# otherwise use path in /config (so that builds aren't lost on container restart)
|
# otherwise use path in /config (so that builds aren't lost on container restart)
|
||||||
if [[ -d /build ]]; then
|
if [[ -d /build ]]; then
|
||||||
export ESPHOME_BUILD_PATH=/build
|
export ESPHOME_BUILD_PATH=/build
|
||||||
fi
|
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 "$@"
|
exec esphome "$@"
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/with-contenv bashio
|
||||||
|
# ==============================================================================
|
||||||
|
# Installs the latest prerelease of esphome-device-builder when the
|
||||||
|
# `use_new_device_builder` config option is enabled.
|
||||||
|
# This is a temporary install-on-boot step until esphome-device-builder
|
||||||
|
# becomes a direct dependency of esphome.
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
if ! bashio::config.true 'use_new_device_builder'; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
bashio::log.info "Installing latest prerelease of esphome-device-builder..."
|
||||||
|
if command -v uv > /dev/null; then
|
||||||
|
uv pip install --system --no-cache-dir --prerelease=allow --upgrade \
|
||||||
|
esphome-device-builder ||
|
||||||
|
bashio::exit.nok "Failed installing esphome-device-builder."
|
||||||
|
else
|
||||||
|
pip install --no-cache-dir --pre --upgrade esphome-device-builder ||
|
||||||
|
bashio::exit.nok "Failed installing esphome-device-builder."
|
||||||
|
fi
|
||||||
|
bashio::log.info "Installed esphome-device-builder."
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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 "";
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
upstream esphome {
|
||||||
|
server unix:/var/run/esphome.sock;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Without requirements or design, programming is the art of adding bugs to an empty text file. (Louis Srygley)
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ fi
|
|||||||
|
|
||||||
port=$(bashio::addon.ingress_port)
|
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
|
bashio::net.wait_for "${port}" "127.0.0.1" 300
|
||||||
|
|
||||||
config=$(\
|
config=$(\
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# shellcheck shell=bash
|
# shellcheck shell=bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# Home Assistant Community Add-on: ESPHome
|
# 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
|
declare exit_code
|
||||||
readonly exit_code_container=$(</run/s6-linux-init-container-results/exitcode)
|
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}"
|
readonly exit_code_signal="${2}"
|
||||||
|
|
||||||
bashio::log.info \
|
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})"
|
"(by signal ${exit_code_signal})"
|
||||||
|
|
||||||
if [[ "${exit_code_service}" -eq 256 ]]; then
|
if [[ "${exit_code_service}" -eq 256 ]]; then
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# shellcheck shell=bash
|
# shellcheck shell=bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# Community Hass.io Add-ons: ESPHome
|
# Community Hass.io Add-ons: ESPHome
|
||||||
# Runs the ESPHome Device Builder
|
# Runs the ESPHome dashboard
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
readonly pio_cache_base=/data/cache/platformio
|
readonly pio_cache_base=/data/cache/platformio
|
||||||
|
|
||||||
@@ -15,15 +15,18 @@ export PLATFORMIO_PLATFORMS_DIR="${pio_cache_base}/platforms"
|
|||||||
export PLATFORMIO_PACKAGES_DIR="${pio_cache_base}/packages"
|
export PLATFORMIO_PACKAGES_DIR="${pio_cache_base}/packages"
|
||||||
export PLATFORMIO_CACHE_DIR="${pio_cache_base}/cache"
|
export PLATFORMIO_CACHE_DIR="${pio_cache_base}/cache"
|
||||||
|
|
||||||
# Keep the native toolchain installs on the persistent /data volume, not the
|
|
||||||
# container's ephemeral user cache dir (wiped on every add-on update/restart).
|
|
||||||
export ESPHOME_ESP_IDF_PREFIX=/data/cache/idf
|
|
||||||
export ESPHOME_SDK_NRF_PREFIX=/data/cache/sdk-nrf
|
|
||||||
|
|
||||||
if bashio::config.true 'leave_front_door_open'; then
|
if bashio::config.true 'leave_front_door_open'; then
|
||||||
export DISABLE_HA_AUTHENTICATION=true
|
export DISABLE_HA_AUTHENTICATION=true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if bashio::config.true 'streamer_mode'; then
|
||||||
|
export ESPHOME_STREAMER_MODE=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if bashio::config.has_value 'relative_url'; then
|
||||||
|
export ESPHOME_DASHBOARD_RELATIVE_URL=$(bashio::config 'relative_url')
|
||||||
|
fi
|
||||||
|
|
||||||
if bashio::config.has_value 'default_compile_process_limit'; then
|
if bashio::config.has_value 'default_compile_process_limit'; then
|
||||||
export ESPHOME_DEFAULT_COMPILE_PROCESS_LIMIT=$(bashio::config 'default_compile_process_limit')
|
export ESPHOME_DEFAULT_COMPILE_PROCESS_LIMIT=$(bashio::config 'default_compile_process_limit')
|
||||||
else
|
else
|
||||||
@@ -46,21 +49,12 @@ if bashio::fs.directory_exists '/config/esphome/.esphome'; then
|
|||||||
rm -rf /config/esphome/.esphome
|
rm -rf /config/esphome/.esphome
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Only signal device-builder to expose the public LAN port when the operator
|
if bashio::config.true 'use_new_device_builder'; then
|
||||||
# mapped port 6052, matching the legacy dashboard where nginx listened on the
|
bashio::log.info "Starting ESPHome Device Builder..."
|
||||||
# fixed port 6052 only when it was configured. We use the mapping purely as a
|
exec esphome-device-builder /config/esphome \
|
||||||
# presence check and don't forward the published value; device-builder binds
|
--ha-addon \
|
||||||
# its default port 6052 (the fixed container port, as the legacy
|
--ingress-port "$(bashio::addon.ingress_port)"
|
||||||
# "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
|
fi
|
||||||
|
|
||||||
bashio::log.info "Starting ESPHome Device Builder..."
|
bashio::log.info "Starting ESPHome dashboard..."
|
||||||
exec esphome-device-builder /config/esphome \
|
exec esphome dashboard /config/esphome --socket /var/run/esphome.sock --ha-addon
|
||||||
--ha-addon \
|
|
||||||
--ingress-port "$(bashio::addon.ingress_port)" \
|
|
||||||
"$@"
|
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
#!/command/with-contenv bashio
|
||||||
|
# shellcheck shell=bash
|
||||||
|
# ==============================================================================
|
||||||
|
# Community Hass.io Add-ons: ESPHome
|
||||||
|
# Configures NGINX for use with ESPHome
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
# When the new device builder is enabled it serves HA ingress directly,
|
||||||
|
# so nginx is not used at all -- skip configuration.
|
||||||
|
if bashio::config.true 'use_new_device_builder'; then
|
||||||
|
bashio::log.info "Skipping NGINX setup: new device builder serves ingress directly."
|
||||||
|
bashio::exit.ok
|
||||||
|
fi
|
||||||
|
|
||||||
|
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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
oneshot
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/etc/s6-overlay/s6-rc.d/init-nginx/run
|
||||||
@@ -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
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
#!/command/with-contenv bashio
|
||||||
|
# shellcheck shell=bash
|
||||||
|
# ==============================================================================
|
||||||
|
# Community Hass.io Add-ons: ESPHome
|
||||||
|
# Runs the NGINX proxy
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
# The new device builder handles HA ingress itself, so nginx is bypassed.
|
||||||
|
# Block the longrun forever so s6 keeps the dependency satisfied and does
|
||||||
|
# not respawn it.
|
||||||
|
if bashio::config.true 'use_new_device_builder'; then
|
||||||
|
bashio::log.info "NGINX bypassed: new device builder serves ingress directly."
|
||||||
|
exec sleep infinity
|
||||||
|
fi
|
||||||
|
|
||||||
|
bashio::log.info "Waiting for ESPHome dashboard to come up..."
|
||||||
|
|
||||||
|
while [[ ! -S /var/run/esphome.sock ]]; do
|
||||||
|
sleep 0.5
|
||||||
|
done
|
||||||
|
|
||||||
|
bashio::log.info "Starting NGINX..."
|
||||||
|
exec nginx
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
longrun
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
esphome:
|
|
||||||
name: docker-test-bk72xx-arduino
|
|
||||||
|
|
||||||
bk72xx:
|
|
||||||
board: generic-bk7231n-qfn32-tuya
|
|
||||||
|
|
||||||
logger:
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
esphome:
|
|
||||||
name: docker-test-esp32-ard-idf
|
|
||||||
|
|
||||||
esp32:
|
|
||||||
variant: esp32
|
|
||||||
framework:
|
|
||||||
type: arduino
|
|
||||||
toolchain: esp-idf
|
|
||||||
|
|
||||||
logger:
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
esphome:
|
|
||||||
name: docker-test-esp32-ard-pio
|
|
||||||
|
|
||||||
esp32:
|
|
||||||
variant: esp32
|
|
||||||
framework:
|
|
||||||
type: arduino
|
|
||||||
toolchain: platformio
|
|
||||||
|
|
||||||
logger:
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
esphome:
|
|
||||||
name: docker-test-esp32-idf-idf
|
|
||||||
|
|
||||||
esp32:
|
|
||||||
variant: esp32
|
|
||||||
framework:
|
|
||||||
type: esp-idf
|
|
||||||
toolchain: esp-idf
|
|
||||||
|
|
||||||
logger:
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
esphome:
|
|
||||||
name: docker-test-esp32-idf-pio
|
|
||||||
|
|
||||||
esp32:
|
|
||||||
variant: esp32
|
|
||||||
framework:
|
|
||||||
type: esp-idf
|
|
||||||
toolchain: platformio
|
|
||||||
|
|
||||||
logger:
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
esphome:
|
|
||||||
name: docker-test-esp8266-arduino
|
|
||||||
|
|
||||||
esp8266:
|
|
||||||
board: d1_mini
|
|
||||||
|
|
||||||
logger:
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
esphome:
|
|
||||||
name: docker-test-host
|
|
||||||
|
|
||||||
host:
|
|
||||||
|
|
||||||
logger:
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
esphome:
|
|
||||||
name: docker-test-ln882x-arduino
|
|
||||||
|
|
||||||
ln882x:
|
|
||||||
board: generic-ln882h
|
|
||||||
|
|
||||||
logger:
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
esphome:
|
|
||||||
name: docker-test-nrf52
|
|
||||||
|
|
||||||
nrf52:
|
|
||||||
board: adafruit_itsybitsy_nrf52840
|
|
||||||
bootloader: adafruit_nrf52_sd140_v6
|
|
||||||
|
|
||||||
logger:
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
esphome:
|
|
||||||
name: docker-test-rp2040-arduino
|
|
||||||
|
|
||||||
rp2040:
|
|
||||||
variant: rp2040
|
|
||||||
|
|
||||||
logger:
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
esphome:
|
|
||||||
name: docker-test-rtl87xx-arduino
|
|
||||||
|
|
||||||
rtl87xx:
|
|
||||||
board: generic-rtl8710bn-2mb-788k
|
|
||||||
|
|
||||||
logger:
|
|
||||||
+98
-186
@@ -28,13 +28,13 @@ from esphome.const import (
|
|||||||
ALLOWED_NAME_CHARS,
|
ALLOWED_NAME_CHARS,
|
||||||
ARGUMENT_HELP_DEVICE,
|
ARGUMENT_HELP_DEVICE,
|
||||||
CONF_API,
|
CONF_API,
|
||||||
|
CONF_AUTH,
|
||||||
CONF_BAUD_RATE,
|
CONF_BAUD_RATE,
|
||||||
CONF_BROKER,
|
CONF_BROKER,
|
||||||
CONF_DEASSERT_RTS_DTR,
|
CONF_DEASSERT_RTS_DTR,
|
||||||
CONF_DISABLED,
|
CONF_DISABLED,
|
||||||
CONF_ESPHOME,
|
CONF_ESPHOME,
|
||||||
CONF_LEVEL,
|
CONF_LEVEL,
|
||||||
CONF_LOG,
|
|
||||||
CONF_LOG_TOPIC,
|
CONF_LOG_TOPIC,
|
||||||
CONF_LOGGER,
|
CONF_LOGGER,
|
||||||
CONF_MDNS,
|
CONF_MDNS,
|
||||||
@@ -48,10 +48,15 @@ from esphome.const import (
|
|||||||
CONF_PORT,
|
CONF_PORT,
|
||||||
CONF_SUBSTITUTIONS,
|
CONF_SUBSTITUTIONS,
|
||||||
CONF_TOPIC,
|
CONF_TOPIC,
|
||||||
CONF_VERSION,
|
CONF_USERNAME,
|
||||||
CONF_WEB_SERVER,
|
CONF_WEB_SERVER,
|
||||||
CONF_WIFI,
|
CONF_WIFI,
|
||||||
ENV_NOGITIGNORE,
|
ENV_NOGITIGNORE,
|
||||||
|
KEY_CORE,
|
||||||
|
KEY_TARGET_PLATFORM,
|
||||||
|
PLATFORM_ESP32,
|
||||||
|
PLATFORM_ESP8266,
|
||||||
|
PLATFORM_RP2040,
|
||||||
SECRETS_FILES,
|
SECRETS_FILES,
|
||||||
Toolchain,
|
Toolchain,
|
||||||
)
|
)
|
||||||
@@ -225,9 +230,8 @@ def _discover_mac_suffix_devices() -> list[str] | None:
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
- ``None`` when discovery isn't applicable (``name_add_mac_suffix`` off,
|
- ``None`` when discovery isn't applicable (``name_add_mac_suffix`` off,
|
||||||
mDNS disabled, or ``CORE.address`` isn't a ``.local`` mDNS address).
|
mDNS disabled, or ``CORE.address`` is already an IP). Callers should
|
||||||
Callers should then fall back to whatever default OTA address they
|
then fall back to whatever default OTA address they normally use.
|
||||||
normally use.
|
|
||||||
- ``[]`` when discovery ran but found nothing. Callers should NOT fall
|
- ``[]`` when discovery ran but found nothing. Callers should NOT fall
|
||||||
back to the base name: with ``name_add_mac_suffix`` enabled, the base
|
back to the base name: with ``name_add_mac_suffix`` enabled, the base
|
||||||
name by definition doesn't exist on the network.
|
name by definition doesn't exist on the network.
|
||||||
@@ -237,7 +241,7 @@ def _discover_mac_suffix_devices() -> list[str] | None:
|
|||||||
``aioesphomeapi`` via :func:`_resolve_network_devices`) reuses the IPs we
|
``aioesphomeapi`` via :func:`_resolve_network_devices`) reuses the IPs we
|
||||||
already have without opening a second Zeroconf client.
|
already have without opening a second Zeroconf client.
|
||||||
"""
|
"""
|
||||||
if not (has_name_add_mac_suffix() and has_mdns() and has_mdns_address()):
|
if not (has_name_add_mac_suffix() and has_mdns() and has_non_ip_address()):
|
||||||
return None
|
return None
|
||||||
from esphome.zeroconf import discover_mdns_devices
|
from esphome.zeroconf import discover_mdns_devices
|
||||||
|
|
||||||
@@ -264,36 +268,6 @@ def _ota_hostnames_for_default(purpose: Purpose) -> list[str]:
|
|||||||
return _resolve_with_cache(CORE.address, purpose)
|
return _resolve_with_cache(CORE.address, purpose)
|
||||||
|
|
||||||
|
|
||||||
def _unresolved_default_error(purpose: Purpose, defaults: list[str]) -> str:
|
|
||||||
"""Build the error when a default device target produced no usable host.
|
|
||||||
|
|
||||||
When the OTA default was requested and the address resolves but the config
|
|
||||||
lacks the transport the purpose needs (``api:`` for logs, an ``ota:``
|
|
||||||
platform for uploads), name that gap instead of the misleading
|
|
||||||
"could not be resolved" / set-use_address hint.
|
|
||||||
"""
|
|
||||||
if "OTA" in defaults and has_resolvable_address():
|
|
||||||
if purpose == Purpose.LOGGING and not has_api():
|
|
||||||
return (
|
|
||||||
"Cannot view logs over the network: no 'api:' component is "
|
|
||||||
"configured. Network log streaming requires the native API; add "
|
|
||||||
"an 'api:' component, enable MQTT logging, or view logs over USB."
|
|
||||||
)
|
|
||||||
if purpose == Purpose.UPLOADING and not has_ota():
|
|
||||||
return (
|
|
||||||
"Cannot upload over the network: no 'ota:' platform is "
|
|
||||||
"configured. Add an 'ota:' platform, or upload over USB."
|
|
||||||
)
|
|
||||||
if CORE.dashboard:
|
|
||||||
hint = "If you know the IP, set 'use_address' in your network config."
|
|
||||||
else:
|
|
||||||
hint = "If you know the IP, try --device <IP>"
|
|
||||||
return (
|
|
||||||
f"All specified devices {defaults} could not be resolved. "
|
|
||||||
f"Is the device connected to the network? {hint}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def choose_upload_log_host(
|
def choose_upload_log_host(
|
||||||
default: list[str] | str | None,
|
default: list[str] | str | None,
|
||||||
check_default: str | None,
|
check_default: str | None,
|
||||||
@@ -317,12 +291,9 @@ def choose_upload_log_host(
|
|||||||
]
|
]
|
||||||
resolved.append(choose_prompt(options, purpose=purpose))
|
resolved.append(choose_prompt(options, purpose=purpose))
|
||||||
elif device == "OTA":
|
elif device == "OTA":
|
||||||
# Logs can stream over a network transport via the native API
|
|
||||||
# or the web_server HTTP SSE feed.
|
|
||||||
network_logging = has_api() or has_web_server_logging()
|
|
||||||
# ensure IP adresses are used first
|
# ensure IP adresses are used first
|
||||||
if is_ip_address(CORE.address) and (
|
if is_ip_address(CORE.address) and (
|
||||||
(purpose == Purpose.LOGGING and network_logging)
|
(purpose == Purpose.LOGGING and has_api())
|
||||||
or (purpose == Purpose.UPLOADING and has_ota())
|
or (purpose == Purpose.UPLOADING and has_ota())
|
||||||
):
|
):
|
||||||
resolved.extend(_resolve_with_cache(CORE.address, purpose))
|
resolved.extend(_resolve_with_cache(CORE.address, purpose))
|
||||||
@@ -334,11 +305,7 @@ def choose_upload_log_host(
|
|||||||
if has_mqtt_logging():
|
if has_mqtt_logging():
|
||||||
resolved.append("MQTT")
|
resolved.append("MQTT")
|
||||||
|
|
||||||
if (
|
if has_api() and has_non_ip_address() and has_resolvable_address():
|
||||||
network_logging
|
|
||||||
and has_non_ip_address()
|
|
||||||
and has_resolvable_address()
|
|
||||||
):
|
|
||||||
resolved.extend(_ota_hostnames_for_default(purpose))
|
resolved.extend(_ota_hostnames_for_default(purpose))
|
||||||
|
|
||||||
elif purpose == Purpose.UPLOADING:
|
elif purpose == Purpose.UPLOADING:
|
||||||
@@ -350,7 +317,14 @@ def choose_upload_log_host(
|
|||||||
else:
|
else:
|
||||||
resolved.append(device)
|
resolved.append(device)
|
||||||
if not resolved:
|
if not resolved:
|
||||||
raise EsphomeError(_unresolved_default_error(purpose, defaults))
|
if CORE.dashboard:
|
||||||
|
hint = "If you know the IP, set 'use_address' in your network config."
|
||||||
|
else:
|
||||||
|
hint = "If you know the IP, try --device <IP>"
|
||||||
|
raise EsphomeError(
|
||||||
|
f"All specified devices {defaults} could not be resolved. "
|
||||||
|
f"Is the device connected to the network? {hint}"
|
||||||
|
)
|
||||||
return resolved
|
return resolved
|
||||||
|
|
||||||
# No devices specified, show interactive chooser
|
# No devices specified, show interactive chooser
|
||||||
@@ -362,7 +336,7 @@ def choose_upload_log_host(
|
|||||||
bootsel_permission_error = False
|
bootsel_permission_error = False
|
||||||
if (
|
if (
|
||||||
purpose == Purpose.UPLOADING
|
purpose == Purpose.UPLOADING
|
||||||
and CORE.is_rp2
|
and CORE.data.get(KEY_CORE, {}).get(KEY_TARGET_PLATFORM) == PLATFORM_RP2040
|
||||||
and (picotool := _find_picotool()) is not None
|
and (picotool := _find_picotool()) is not None
|
||||||
):
|
):
|
||||||
bootsel = detect_rp2040_bootsel(picotool)
|
bootsel = detect_rp2040_bootsel(picotool)
|
||||||
@@ -400,7 +374,7 @@ def choose_upload_log_host(
|
|||||||
mqtt_config = CORE.config[CONF_MQTT]
|
mqtt_config = CORE.config[CONF_MQTT]
|
||||||
options.append((f"MQTT ({mqtt_config[CONF_BROKER]})", "MQTT"))
|
options.append((f"MQTT ({mqtt_config[CONF_BROKER]})", "MQTT"))
|
||||||
|
|
||||||
if has_api() or has_web_server_logging():
|
if has_api():
|
||||||
add_ota_options()
|
add_ota_options()
|
||||||
|
|
||||||
elif purpose == Purpose.UPLOADING and has_ota():
|
elif purpose == Purpose.UPLOADING and has_ota():
|
||||||
@@ -409,7 +383,7 @@ def choose_upload_log_host(
|
|||||||
# Show helpful BOOTSEL instructions for RP2040 when no BOOTSEL device is found
|
# Show helpful BOOTSEL instructions for RP2040 when no BOOTSEL device is found
|
||||||
if (
|
if (
|
||||||
purpose == Purpose.UPLOADING
|
purpose == Purpose.UPLOADING
|
||||||
and CORE.is_rp2
|
and CORE.data.get(KEY_CORE, {}).get(KEY_TARGET_PLATFORM) == PLATFORM_RP2040
|
||||||
and not any(get_port_type(opt[1]) == PortType.BOOTSEL for opt in options)
|
and not any(get_port_type(opt[1]) == PortType.BOOTSEL for opt in options)
|
||||||
):
|
):
|
||||||
if bootsel_permission_error:
|
if bootsel_permission_error:
|
||||||
@@ -493,21 +467,6 @@ def has_web_server_ota() -> bool:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def has_web_server_logging() -> bool:
|
|
||||||
"""Check if logs can be streamed over the web_server HTTP SSE endpoint.
|
|
||||||
|
|
||||||
The ``web_server`` component exposes a ``/events`` Server-Sent Events
|
|
||||||
stream that carries ``event: log`` frames. This requires version 2+ (the
|
|
||||||
v1 UI has no ``/events`` endpoint) and the ``log`` option enabled (default).
|
|
||||||
"""
|
|
||||||
web_conf = CORE.config.get(CONF_WEB_SERVER)
|
|
||||||
if web_conf is None:
|
|
||||||
return False
|
|
||||||
if web_conf.get(CONF_VERSION, 2) == 1:
|
|
||||||
return False
|
|
||||||
return web_conf.get(CONF_LOG, True)
|
|
||||||
|
|
||||||
|
|
||||||
def has_mqtt_ip_lookup() -> bool:
|
def has_mqtt_ip_lookup() -> bool:
|
||||||
"""Check if MQTT is available and IP lookup is supported."""
|
"""Check if MQTT is available and IP lookup is supported."""
|
||||||
from esphome.components.mqtt import CONF_DISCOVER_IP
|
from esphome.components.mqtt import CONF_DISCOVER_IP
|
||||||
@@ -526,22 +485,17 @@ def has_mdns() -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def has_non_ip_address() -> bool:
|
def has_non_ip_address() -> bool:
|
||||||
"""Check if ``CORE.address`` is set and is not an IP address."""
|
"""Check if CORE.address is set and is not an IP address."""
|
||||||
return CORE.address is not None and not is_ip_address(CORE.address)
|
return CORE.address is not None and not is_ip_address(CORE.address)
|
||||||
|
|
||||||
|
|
||||||
def has_mdns_address() -> bool:
|
|
||||||
"""Check if ``CORE.address`` is a ``.local`` mDNS hostname."""
|
|
||||||
return CORE.address is not None and CORE.address.endswith(".local")
|
|
||||||
|
|
||||||
|
|
||||||
def has_ip_address() -> bool:
|
def has_ip_address() -> bool:
|
||||||
"""Check if ``CORE.address`` is a valid IP address."""
|
"""Check if CORE.address is a valid IP address."""
|
||||||
return CORE.address is not None and is_ip_address(CORE.address)
|
return CORE.address is not None and is_ip_address(CORE.address)
|
||||||
|
|
||||||
|
|
||||||
def has_resolvable_address() -> bool:
|
def has_resolvable_address() -> bool:
|
||||||
"""Check if ``CORE.address`` is resolvable (via mDNS, DNS, or is an IP address)."""
|
"""Check if CORE.address is resolvable (via mDNS, DNS, or is an IP address)."""
|
||||||
# Any address (IP, mDNS hostname, or regular DNS hostname) is resolvable
|
# Any address (IP, mDNS hostname, or regular DNS hostname) is resolvable
|
||||||
# The resolve_ip_address() function in helpers.py handles all types via AsyncResolver
|
# The resolve_ip_address() function in helpers.py handles all types via AsyncResolver
|
||||||
if CORE.address is None:
|
if CORE.address is None:
|
||||||
@@ -550,17 +504,11 @@ def has_resolvable_address() -> bool:
|
|||||||
if has_ip_address():
|
if has_ip_address():
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# device-builder pre-resolves the device and passes the IPs via
|
|
||||||
# --mdns-address-cache/--dns-address-cache; honor a cached address even when the
|
|
||||||
# device has mDNS disabled (e.g. a .local host found via ping).
|
|
||||||
if CORE.address_cache and CORE.address_cache.get_addresses(CORE.address):
|
|
||||||
return True
|
|
||||||
|
|
||||||
if has_mdns():
|
if has_mdns():
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# .local mDNS hostnames are only resolvable if mDNS is enabled
|
# .local mDNS hostnames are only resolvable if mDNS is enabled
|
||||||
return not has_mdns_address()
|
return not CORE.address.endswith(".local")
|
||||||
|
|
||||||
|
|
||||||
def has_name_add_mac_suffix() -> bool:
|
def has_name_add_mac_suffix() -> bool:
|
||||||
@@ -730,7 +678,6 @@ def _wrap_to_code(name, comp, yaml_util):
|
|||||||
|
|
||||||
@functools.wraps(comp.to_code)
|
@functools.wraps(comp.to_code)
|
||||||
async def wrapped(conf):
|
async def wrapped(conf):
|
||||||
cg.add(cg.ComponentMarker(name))
|
|
||||||
cg.add(cg.LineComment(f"{name}:"))
|
cg.add(cg.LineComment(f"{name}:"))
|
||||||
if comp.config_schema is not None:
|
if comp.config_schema is not None:
|
||||||
conf_str = yaml_util.dump(conf)
|
conf_str = yaml_util.dump(conf)
|
||||||
@@ -748,11 +695,6 @@ def _wrap_to_code(name, comp, yaml_util):
|
|||||||
def write_cpp(config: ConfigType) -> int:
|
def write_cpp(config: ConfigType) -> int:
|
||||||
from esphome import writer
|
from esphome import writer
|
||||||
|
|
||||||
# Refresh the storage sidecar and clean an incompatible previous build
|
|
||||||
# before regenerating any sources. This may full-wipe the build dir, so it
|
|
||||||
# has to run before write_cpp_file writes src/.
|
|
||||||
writer.update_storage_json()
|
|
||||||
|
|
||||||
if not get_bool_env(ENV_NOGITIGNORE):
|
if not get_bool_env(ENV_NOGITIGNORE):
|
||||||
writer.write_gitignore()
|
writer.write_gitignore()
|
||||||
|
|
||||||
@@ -1008,7 +950,7 @@ def upload_using_platformio(config: ConfigType, port: str) -> int:
|
|||||||
# RP2040 platform-raspberrypi build recipe expects firmware.bin.signed for
|
# RP2040 platform-raspberrypi build recipe expects firmware.bin.signed for
|
||||||
# the upload target, but 'nobuild' skips the build phase that creates it.
|
# the upload target, but 'nobuild' skips the build phase that creates it.
|
||||||
# Create it here so the upload doesn't fail.
|
# Create it here so the upload doesn't fail.
|
||||||
if CORE.is_rp2:
|
if CORE.data.get(KEY_CORE, {}).get(KEY_TARGET_PLATFORM) == PLATFORM_RP2040:
|
||||||
idedata = toolchain.get_idedata(config)
|
idedata = toolchain.get_idedata(config)
|
||||||
build_dir = Path(idedata.firmware_elf_path).parent
|
build_dir = Path(idedata.firmware_elf_path).parent
|
||||||
firmware_bin = build_dir / "firmware.bin"
|
firmware_bin = build_dir / "firmware.bin"
|
||||||
@@ -1193,10 +1135,10 @@ def upload_program(
|
|||||||
check_permissions(host)
|
check_permissions(host)
|
||||||
|
|
||||||
exit_code = 1
|
exit_code = 1
|
||||||
if CORE.is_esp32 or CORE.is_esp8266:
|
if CORE.target_platform in (PLATFORM_ESP32, PLATFORM_ESP8266):
|
||||||
file = getattr(args, "file", None)
|
file = getattr(args, "file", None)
|
||||||
exit_code = upload_using_esptool(config, host, file, args.upload_speed)
|
exit_code = upload_using_esptool(config, host, file, args.upload_speed)
|
||||||
elif CORE.is_rp2 or CORE.is_libretiny:
|
elif CORE.target_platform == PLATFORM_RP2040 or CORE.is_libretiny:
|
||||||
exit_code = upload_using_platformio(config, host)
|
exit_code = upload_using_platformio(config, host)
|
||||||
# else: Unknown target platform, exit_code remains 1
|
# else: Unknown target platform, exit_code remains 1
|
||||||
|
|
||||||
@@ -1314,23 +1256,25 @@ def _upload_via_native_api(
|
|||||||
def _upload_via_web_server(
|
def _upload_via_web_server(
|
||||||
config: ConfigType, network_devices: list[str], binary: Path
|
config: ConfigType, network_devices: list[str], binary: Path
|
||||||
) -> tuple[int, str | None]:
|
) -> tuple[int, str | None]:
|
||||||
|
web_conf = config.get(CONF_WEB_SERVER)
|
||||||
|
if not web_conf:
|
||||||
|
raise EsphomeError(
|
||||||
|
f"Cannot upload via web_server OTA: the {CONF_WEB_SERVER} component "
|
||||||
|
f"is not configured."
|
||||||
|
)
|
||||||
|
|
||||||
|
remote_port = int(web_conf[CONF_PORT])
|
||||||
|
auth = web_conf.get(CONF_AUTH) or {}
|
||||||
|
username = auth.get(CONF_USERNAME)
|
||||||
|
password = auth.get(CONF_PASSWORD)
|
||||||
|
|
||||||
from esphome import web_server_ota
|
from esphome import web_server_ota
|
||||||
from esphome.web_server_helpers import get_web_server_connection
|
|
||||||
|
|
||||||
remote_port, username, password = get_web_server_connection(config)
|
|
||||||
return web_server_ota.run_ota(
|
return web_server_ota.run_ota(
|
||||||
network_devices, remote_port, username, password, binary
|
network_devices, remote_port, username, password, binary
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _show_logs_via_web_server(config: ConfigType, network_devices: list[str]) -> int:
|
|
||||||
from esphome import web_server_logs
|
|
||||||
from esphome.web_server_helpers import get_web_server_connection
|
|
||||||
|
|
||||||
port, username, password = get_web_server_connection(config)
|
|
||||||
return web_server_logs.run_logs(network_devices, port, username, password)
|
|
||||||
|
|
||||||
|
|
||||||
# Layout of esp_partition_info_t on flash. Each entry is 32 bytes, leading with a
|
# Layout of esp_partition_info_t on flash. Each entry is 32 bytes, leading with a
|
||||||
# 16-bit little-endian magic. ESP-IDF defines ESP_PARTITION_MAGIC = 0x50AA (stored as
|
# 16-bit little-endian magic. ESP-IDF defines ESP_PARTITION_MAGIC = 0x50AA (stored as
|
||||||
# bytes 0xAA, 0x50) for partition entries and ESP_PARTITION_MAGIC_MD5 = 0xEBEB for the
|
# bytes 0xAA, 0x50) for partition entries and ESP_PARTITION_MAGIC_MD5 = 0xEBEB for the
|
||||||
@@ -1459,13 +1403,6 @@ def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int
|
|||||||
config, args.topic, args.username, args.password, args.client_id
|
config, args.topic, args.username, args.password, args.client_id
|
||||||
)
|
)
|
||||||
|
|
||||||
# Fall back to the web_server HTTP SSE log stream for devices that have
|
|
||||||
# web_server: but no api: (the logging counterpart to web_server OTA).
|
|
||||||
if has_web_server_logging() and (
|
|
||||||
network_devices := _resolve_network_devices(devices, config, args)
|
|
||||||
):
|
|
||||||
return _show_logs_via_web_server(config, network_devices)
|
|
||||||
|
|
||||||
raise EsphomeError("No remote or local logging method configured (api/mqtt/logger)")
|
raise EsphomeError("No remote or local logging method configured (api/mqtt/logger)")
|
||||||
|
|
||||||
|
|
||||||
@@ -1486,16 +1423,7 @@ def command_wizard(args: ArgsProtocol) -> int | None:
|
|||||||
def command_config(args: ArgsProtocol, config: ConfigType) -> int | None:
|
def command_config(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||||
from esphome import yaml_util
|
from esphome import yaml_util
|
||||||
|
|
||||||
if getattr(args, "no_defaults", False):
|
if not CORE.verbose:
|
||||||
user_config = getattr(config, "user_config", None)
|
|
||||||
if user_config is None:
|
|
||||||
_LOGGER.warning(
|
|
||||||
"--no-defaults requested but the user-only config snapshot is "
|
|
||||||
"unavailable; falling back to the validated configuration."
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
config = user_config
|
|
||||||
elif not CORE.verbose:
|
|
||||||
config = strip_default_ids(config)
|
config = strip_default_ids(config)
|
||||||
output = yaml_util.dump(config, args.show_secrets)
|
output = yaml_util.dump(config, args.show_secrets)
|
||||||
if not args.show_secrets:
|
if not args.show_secrets:
|
||||||
@@ -1522,29 +1450,12 @@ _LEGACY_REDACTION_REMOVAL = "2026.12.0"
|
|||||||
|
|
||||||
def _redact_with_legacy_fallback(output: str) -> str:
|
def _redact_with_legacy_fallback(output: str) -> str:
|
||||||
unmarked: set[str] = set()
|
unmarked: set[str] = set()
|
||||||
# Track the top-level ``substitutions:`` block. Its keys are arbitrary
|
|
||||||
# user-chosen names with no schema validator, so the ``cv.sensitive(...)``
|
|
||||||
# migration named in the warning can't be applied to them. Their values are
|
|
||||||
# still redacted, but emitting the (unactionable) deprecation warning would
|
|
||||||
# only confuse users.
|
|
||||||
in_substitutions = False
|
|
||||||
|
|
||||||
lines = output.split("\n")
|
def _replace(m: re.Match[str]) -> str:
|
||||||
for i, line in enumerate(lines):
|
unmarked.add(m.group("key"))
|
||||||
# A non-indented, non-blank line is a top-level key that opens or
|
return f"{m.group('key')}: \\033[8m{m.group('val')}\\033[28m"
|
||||||
# closes the substitutions block.
|
|
||||||
if line and not line[0].isspace():
|
output = _LEGACY_REDACTION_RE.sub(_replace, output)
|
||||||
in_substitutions = line.startswith(f"{CONF_SUBSTITUTIONS}:")
|
|
||||||
m = _LEGACY_REDACTION_RE.search(line)
|
|
||||||
if m is None:
|
|
||||||
continue
|
|
||||||
if not in_substitutions:
|
|
||||||
unmarked.add(m.group("key"))
|
|
||||||
lines[i] = (
|
|
||||||
f"{line[: m.start()]}{m.group('key')}: "
|
|
||||||
f"\\033[8m{m.group('val')}\\033[28m{line[m.end() :]}"
|
|
||||||
)
|
|
||||||
output = "\n".join(lines)
|
|
||||||
for key in sorted(unmarked):
|
for key in sorted(unmarked):
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Field '%s' is being redacted by a legacy substring heuristic. "
|
"Field '%s' is being redacted by a legacy substring heuristic. "
|
||||||
@@ -1675,7 +1586,10 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
|
|||||||
|
|
||||||
# After BOOTSEL upload, wait for a new serial port to appear
|
# After BOOTSEL upload, wait for a new serial port to appear
|
||||||
# so it shows up in the log chooser
|
# so it shows up in the log chooser
|
||||||
if successful_device is None and CORE.is_rp2:
|
if (
|
||||||
|
successful_device is None
|
||||||
|
and CORE.data.get(KEY_CORE, {}).get(KEY_TARGET_PLATFORM) == PLATFORM_RP2040
|
||||||
|
):
|
||||||
_wait_for_serial_port(known_ports=pre_upload_ports)
|
_wait_for_serial_port(known_ports=pre_upload_ports)
|
||||||
# If exactly one new serial port appeared, use it directly
|
# If exactly one new serial port appeared, use it directly
|
||||||
serial_ports = get_serial_ports()
|
serial_ports = get_serial_ports()
|
||||||
@@ -1717,7 +1631,7 @@ def command_clean(args: ArgsProtocol, config: ConfigType) -> int | None:
|
|||||||
from esphome import writer
|
from esphome import writer
|
||||||
|
|
||||||
try:
|
try:
|
||||||
writer.clean_build(full=True)
|
writer.clean_build()
|
||||||
except OSError as err:
|
except OSError as err:
|
||||||
_LOGGER.error("Error deleting build files: %s", err)
|
_LOGGER.error("Error deleting build files: %s", err)
|
||||||
return 1
|
return 1
|
||||||
@@ -1758,13 +1672,9 @@ def command_bundle(args: ArgsProtocol, config: ConfigType) -> int | None:
|
|||||||
|
|
||||||
|
|
||||||
def command_dashboard(args: ArgsProtocol) -> int | None:
|
def command_dashboard(args: ArgsProtocol) -> int | None:
|
||||||
raise EsphomeError(
|
from esphome.dashboard import dashboard
|
||||||
"The built-in dashboard has been removed from ESPHome. "
|
|
||||||
"Install and run ESPHome Device Builder instead:\n"
|
return dashboard.start_dashboard(args)
|
||||||
" pip install esphome-device-builder\n"
|
|
||||||
" esphome-device-builder\n"
|
|
||||||
"See https://github.com/esphome/device-builder for more information."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def run_multiple_configs(
|
def run_multiple_configs(
|
||||||
@@ -1841,21 +1751,6 @@ def command_update_all(args: ArgsProtocol) -> int | None:
|
|||||||
def command_idedata(args: ArgsProtocol, config: ConfigType) -> int:
|
def command_idedata(args: ArgsProtocol, config: ConfigType) -> int:
|
||||||
import json
|
import json
|
||||||
|
|
||||||
if CORE.using_toolchain_esp_idf:
|
|
||||||
# Native ESP-IDF derives idedata from the build's compile_commands.json,
|
|
||||||
# so the configuration must already be compiled.
|
|
||||||
from esphome.espidf import toolchain as espidf_toolchain
|
|
||||||
|
|
||||||
idedata = espidf_toolchain.get_idedata()
|
|
||||||
if idedata is None:
|
|
||||||
_LOGGER.error(
|
|
||||||
"No idedata available; compile the configuration first",
|
|
||||||
)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
print(json.dumps(idedata, indent=2) + "\n")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
if not CORE.using_toolchain_platformio:
|
if not CORE.using_toolchain_platformio:
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
"The idedata command is not compatible with %s toolchain",
|
"The idedata command is not compatible with %s toolchain",
|
||||||
@@ -2252,12 +2147,6 @@ def parse_args(argv):
|
|||||||
parser_config.add_argument(
|
parser_config.add_argument(
|
||||||
"--show-secrets", help="Show secrets in output.", action="store_true"
|
"--show-secrets", help="Show secrets in output.", action="store_true"
|
||||||
)
|
)
|
||||||
parser_config.add_argument(
|
|
||||||
"--no-defaults",
|
|
||||||
help="Only output the user-supplied configuration without "
|
|
||||||
"schema defaults applied.",
|
|
||||||
action="store_true",
|
|
||||||
)
|
|
||||||
|
|
||||||
parser_config_hash = subparsers.add_parser(
|
parser_config_hash = subparsers.add_parser(
|
||||||
"config-hash", help="Calculate the hash of the configuration."
|
"config-hash", help="Calculate the hash of the configuration."
|
||||||
@@ -2420,31 +2309,50 @@ def parse_args(argv):
|
|||||||
)
|
)
|
||||||
|
|
||||||
parser_clean_all = subparsers.add_parser(
|
parser_clean_all = subparsers.add_parser(
|
||||||
"clean-all",
|
"clean-all", help="Clean all build and platform files."
|
||||||
help="Clean all build and platform files, including machine-global "
|
|
||||||
"toolchain caches shared by all configurations, so other projects will "
|
|
||||||
"re-download them on next build.",
|
|
||||||
)
|
)
|
||||||
parser_clean_all.add_argument(
|
parser_clean_all.add_argument(
|
||||||
"configuration", help="Your YAML file or configuration directory.", nargs="*"
|
"configuration", help="Your YAML file or configuration directory.", nargs="*"
|
||||||
)
|
)
|
||||||
|
|
||||||
# The dashboard moved to ESPHome Device Builder; the command is kept only to
|
parser_dashboard = subparsers.add_parser(
|
||||||
# print a redirect (see command_dashboard). Accept and ignore the old flags
|
"dashboard", help="Create a simple web server for a dashboard."
|
||||||
# so legacy invocations reach that message instead of failing on argparse
|
|
||||||
# "unrecognized arguments".
|
|
||||||
parser_dashboard = subparsers.add_parser("dashboard")
|
|
||||||
parser_dashboard.add_argument("configuration", nargs="?", help=argparse.SUPPRESS)
|
|
||||||
parser_dashboard.add_argument("--port", help=argparse.SUPPRESS)
|
|
||||||
parser_dashboard.add_argument("--address", help=argparse.SUPPRESS)
|
|
||||||
parser_dashboard.add_argument("--username", help=argparse.SUPPRESS)
|
|
||||||
parser_dashboard.add_argument("--password", help=argparse.SUPPRESS)
|
|
||||||
parser_dashboard.add_argument("--socket", help=argparse.SUPPRESS)
|
|
||||||
parser_dashboard.add_argument(
|
|
||||||
"--open-ui", action="store_true", help=argparse.SUPPRESS
|
|
||||||
)
|
)
|
||||||
parser_dashboard.add_argument(
|
parser_dashboard.add_argument(
|
||||||
"--ha-addon", action="store_true", help=argparse.SUPPRESS
|
"configuration", help="Your YAML configuration file directory."
|
||||||
|
)
|
||||||
|
parser_dashboard.add_argument(
|
||||||
|
"--port",
|
||||||
|
help="The HTTP port to open connections on. Defaults to 6052.",
|
||||||
|
type=int,
|
||||||
|
default=6052,
|
||||||
|
)
|
||||||
|
parser_dashboard.add_argument(
|
||||||
|
"--address",
|
||||||
|
help="The address to bind to.",
|
||||||
|
type=str,
|
||||||
|
default="0.0.0.0",
|
||||||
|
)
|
||||||
|
parser_dashboard.add_argument(
|
||||||
|
"--username",
|
||||||
|
help="The optional username to require for authentication.",
|
||||||
|
type=str,
|
||||||
|
default="",
|
||||||
|
)
|
||||||
|
parser_dashboard.add_argument(
|
||||||
|
"--password",
|
||||||
|
help="The optional password to require for authentication.",
|
||||||
|
type=str,
|
||||||
|
default="",
|
||||||
|
)
|
||||||
|
parser_dashboard.add_argument(
|
||||||
|
"--open-ui", help="Open the dashboard UI in a browser.", action="store_true"
|
||||||
|
)
|
||||||
|
parser_dashboard.add_argument(
|
||||||
|
"--ha-addon", help=argparse.SUPPRESS, action="store_true"
|
||||||
|
)
|
||||||
|
parser_dashboard.add_argument(
|
||||||
|
"--socket", help="Make the dashboard serve under a unix socket", type=str
|
||||||
)
|
)
|
||||||
|
|
||||||
parser_vscode = subparsers.add_parser("vscode")
|
parser_vscode = subparsers.add_parser("vscode")
|
||||||
@@ -2539,7 +2447,11 @@ def run_esphome(argv):
|
|||||||
elif args.quiet:
|
elif args.quiet:
|
||||||
args.log_level = "CRITICAL"
|
args.log_level = "CRITICAL"
|
||||||
|
|
||||||
setup_log(log_level=args.log_level)
|
setup_log(
|
||||||
|
log_level=args.log_level,
|
||||||
|
# Show timestamp for dashboard access logs
|
||||||
|
include_timestamp=args.command == "dashboard",
|
||||||
|
)
|
||||||
|
|
||||||
if args.command in PRE_CONFIG_ACTIONS:
|
if args.command in PRE_CONFIG_ACTIONS:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ from __future__ import annotations
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
import heapq
|
import heapq
|
||||||
import json
|
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import sys
|
import sys
|
||||||
@@ -21,7 +20,6 @@ from . import (
|
|||||||
RAM_SECTIONS,
|
RAM_SECTIONS,
|
||||||
MemoryAnalyzer,
|
MemoryAnalyzer,
|
||||||
)
|
)
|
||||||
from .toolchain import find_elf_path, find_idedata_path, idedata_candidates
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from . import ComponentMemory
|
from . import ComponentMemory
|
||||||
@@ -43,7 +41,7 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
|||||||
|
|
||||||
# Symbol size threshold for detailed analysis
|
# Symbol size threshold for detailed analysis
|
||||||
SYMBOL_SIZE_THRESHOLD: int = (
|
SYMBOL_SIZE_THRESHOLD: int = (
|
||||||
10 # Show symbols larger than this in detailed analysis
|
100 # Show symbols larger than this in detailed analysis
|
||||||
)
|
)
|
||||||
# Lower threshold for RAM symbols (RAM is more constrained)
|
# Lower threshold for RAM symbols (RAM is more constrained)
|
||||||
RAM_SYMBOL_SIZE_THRESHOLD: int = 24
|
RAM_SYMBOL_SIZE_THRESHOLD: int = 24
|
||||||
@@ -761,25 +759,45 @@ def main():
|
|||||||
print(f"Error: {build_path} is not a directory", file=sys.stderr)
|
print(f"Error: {build_path} is not a directory", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
elf_path = find_elf_path(build_path)
|
# Find firmware.elf
|
||||||
if not elf_path:
|
elf_file = None
|
||||||
print(f"Error: no firmware ELF found in {build_dir}", file=sys.stderr)
|
for elf_candidate in [
|
||||||
|
build_path / "firmware.elf",
|
||||||
|
build_path / ".pioenvs" / build_path.name / "firmware.elf",
|
||||||
|
]:
|
||||||
|
if elf_candidate.exists():
|
||||||
|
elf_file = str(elf_candidate)
|
||||||
|
break
|
||||||
|
|
||||||
|
if not elf_file:
|
||||||
|
print(f"Error: firmware.elf not found in {build_dir}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
elf_file = str(elf_path)
|
|
||||||
|
# Find idedata.json - check current directory first, then home
|
||||||
|
device_name = build_path.name
|
||||||
|
idedata_candidates = [
|
||||||
|
Path.cwd() / ".esphome" / "idedata" / f"{device_name}.json",
|
||||||
|
Path.home() / ".esphome" / "idedata" / f"{device_name}.json",
|
||||||
|
]
|
||||||
|
|
||||||
idedata = None
|
idedata = None
|
||||||
if idedata_path := find_idedata_path(build_path):
|
for idedata_path in idedata_candidates:
|
||||||
|
if not idedata_path.exists():
|
||||||
|
continue
|
||||||
try:
|
try:
|
||||||
with idedata_path.open(encoding="utf-8") as f:
|
with idedata_path.open(encoding="utf-8") as f:
|
||||||
raw_data = json.load(f)
|
raw_data = json.load(f)
|
||||||
idedata = IDEData(raw_data)
|
idedata = IDEData(raw_data)
|
||||||
print(f"Loaded idedata from: {idedata_path}", file=sys.stderr)
|
print(f"Loaded idedata from: {idedata_path}", file=sys.stderr)
|
||||||
|
break
|
||||||
except (json.JSONDecodeError, OSError) as e:
|
except (json.JSONDecodeError, OSError) as e:
|
||||||
print(f"Warning: Failed to load idedata: {e}", file=sys.stderr)
|
print(f"Warning: Failed to load idedata: {e}", file=sys.stderr)
|
||||||
|
|
||||||
if not idedata:
|
if not idedata:
|
||||||
searched = "\n ".join(str(p) for p in idedata_candidates(build_path))
|
print(
|
||||||
print(f"Warning: idedata not found, searched:\n {searched}", file=sys.stderr)
|
f"Warning: idedata not found (searched {idedata_candidates[0]} and {idedata_candidates[1]})",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
analyzer = MemoryAnalyzerCLI(elf_file, idedata=idedata)
|
analyzer = MemoryAnalyzerCLI(elf_file, idedata=idedata)
|
||||||
analyzer.analyze()
|
analyzer.analyze()
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ memory-constrained platforms like ESP8266.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
@@ -65,7 +65,6 @@ class RamSymbol:
|
|||||||
size: int
|
size: int
|
||||||
section: str
|
section: str
|
||||||
demangled: str = "" # Demangled name, set after batch demangling
|
demangled: str = "" # Demangled name, set after batch demangling
|
||||||
aliases: list[str] = field(default_factory=list) # Other names at same address
|
|
||||||
|
|
||||||
|
|
||||||
class RamStringsAnalyzer:
|
class RamStringsAnalyzer:
|
||||||
@@ -236,11 +235,6 @@ class RamStringsAnalyzer:
|
|||||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Track symbols by address so aliases (multiple names for the same
|
|
||||||
# object, e.g. the newlib __lock___* mutexes that all alias one
|
|
||||||
# StaticSemaphore_t) are reported once instead of once per name.
|
|
||||||
symbols_by_addr: dict[int, RamSymbol] = {}
|
|
||||||
|
|
||||||
for line in output.split("\n"):
|
for line in output.split("\n"):
|
||||||
parts = line.split()
|
parts = line.split()
|
||||||
if len(parts) < 4:
|
if len(parts) < 4:
|
||||||
@@ -259,18 +253,6 @@ class RamStringsAnalyzer:
|
|||||||
if sym_type not in DATA_SYMBOL_TYPES:
|
if sym_type not in DATA_SYMBOL_TYPES:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if (existing := symbols_by_addr.get(addr)) is not None:
|
|
||||||
# Prefer a global (uppercase type) name as the primary so
|
|
||||||
# nm output order can't hide it behind a local alias.
|
|
||||||
if sym_type.isupper() and existing.sym_type.islower():
|
|
||||||
existing.aliases.append(existing.name)
|
|
||||||
existing.name = name
|
|
||||||
existing.sym_type = sym_type
|
|
||||||
else:
|
|
||||||
existing.aliases.append(name)
|
|
||||||
existing.size = max(existing.size, size)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Check if symbol is in a RAM section
|
# Check if symbol is in a RAM section
|
||||||
for section_name in self.ram_sections:
|
for section_name in self.ram_sections:
|
||||||
if section_name not in self.sections:
|
if section_name not in self.sections:
|
||||||
@@ -278,15 +260,15 @@ class RamStringsAnalyzer:
|
|||||||
|
|
||||||
section = self.sections[section_name]
|
section = self.sections[section_name]
|
||||||
if section.address <= addr < section.address + section.size:
|
if section.address <= addr < section.address + section.size:
|
||||||
symbol = RamSymbol(
|
self.ram_symbols.append(
|
||||||
name=name,
|
RamSymbol(
|
||||||
sym_type=sym_type,
|
name=name,
|
||||||
address=addr,
|
sym_type=sym_type,
|
||||||
size=size,
|
address=addr,
|
||||||
section=section_name,
|
size=size,
|
||||||
|
section=section_name,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
symbols_by_addr[addr] = symbol
|
|
||||||
self.ram_symbols.append(symbol)
|
|
||||||
break
|
break
|
||||||
|
|
||||||
def _demangle_symbols(self) -> None:
|
def _demangle_symbols(self) -> None:
|
||||||
@@ -454,13 +436,7 @@ class RamStringsAnalyzer:
|
|||||||
for symbol in largest_symbols:
|
for symbol in largest_symbols:
|
||||||
# Use demangled name if available, otherwise raw name
|
# Use demangled name if available, otherwise raw name
|
||||||
display_name = symbol.demangled or symbol.name
|
display_name = symbol.demangled or symbol.name
|
||||||
# Truncate the name, not the alias note, so merged aliases stay
|
name_display = display_name[:49] if len(display_name) > 49 else display_name
|
||||||
# visible even for long demangled C++ names.
|
|
||||||
alias_note = f" (+{len(symbol.aliases)} aliases)" if symbol.aliases else ""
|
|
||||||
max_name_len = 49 - len(alias_note)
|
|
||||||
if len(display_name) > max_name_len:
|
|
||||||
display_name = display_name[:max_name_len]
|
|
||||||
name_display = display_name + alias_note
|
|
||||||
lines.append(
|
lines.append(
|
||||||
f"{name_display:<50} {symbol.sym_type:<6} {symbol.size:>8} B {symbol.section}"
|
f"{name_display:<50} {symbol.sym_type:<6} {symbol.size:>8} B {symbol.section}"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -23,78 +23,6 @@ TOOLCHAIN_PREFIXES = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def find_elf_path(build_path: Path) -> Path | None:
|
|
||||||
"""Locate the firmware ELF inside an ESPHome build directory.
|
|
||||||
|
|
||||||
The layout depends on the toolchain that produced the build, so try each
|
|
||||||
known one in turn.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
build_path: Path to an ESPHome build directory
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Path to the ELF file, or None if no known layout matches
|
|
||||||
"""
|
|
||||||
name = build_path.name
|
|
||||||
for candidate in (
|
|
||||||
# Native ESP-IDF: idf.py writes build/<name>.elf, which ESPHome copies
|
|
||||||
# to build/firmware.elf (see espidf.toolchain.create_elf_copy)
|
|
||||||
build_path / "build" / "firmware.elf",
|
|
||||||
# PlatformIO
|
|
||||||
build_path / "firmware.elf",
|
|
||||||
build_path / ".pioenvs" / name / "firmware.elf",
|
|
||||||
# LibreTiny uses raw_firmware.elf
|
|
||||||
build_path / "raw_firmware.elf",
|
|
||||||
build_path / ".pioenvs" / name / "raw_firmware.elf",
|
|
||||||
# Zephyr (nRF52); the SDK nests the artifacts one level deeper from 2.9.2
|
|
||||||
build_path / ".pioenvs" / name / "zephyr" / "zephyr" / "zephyr.elf",
|
|
||||||
build_path / ".pioenvs" / name / "zephyr" / "zephyr.elf",
|
|
||||||
):
|
|
||||||
if candidate.is_file():
|
|
||||||
return candidate
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def idedata_candidates(build_path: Path) -> list[Path]:
|
|
||||||
"""Return the idedata locations searched for a build directory, in order.
|
|
||||||
|
|
||||||
Exposed so a caller reporting "not found" can name the paths it tried
|
|
||||||
without keeping its own copy of the list.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
build_path: Path to an ESPHome build directory
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The candidate idedata JSON paths, most specific first
|
|
||||||
"""
|
|
||||||
name = build_path.name
|
|
||||||
return [
|
|
||||||
# In .pioenvs for test builds
|
|
||||||
build_path / ".pioenvs" / name / "idedata.json",
|
|
||||||
# Both toolchains cache it in the data dir, which holds this build dir:
|
|
||||||
# <data_dir>/idedata/<name>.json next to <data_dir>/build/<name>
|
|
||||||
build_path.parent.parent / "idedata" / f"{name}.json",
|
|
||||||
# Regular builds, invoked from the config dir or from anywhere
|
|
||||||
Path.cwd() / ".esphome" / "idedata" / f"{name}.json",
|
|
||||||
Path.home() / ".esphome" / "idedata" / f"{name}.json",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def find_idedata_path(build_path: Path) -> Path | None:
|
|
||||||
"""Locate the idedata JSON belonging to an ESPHome build directory.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
build_path: Path to an ESPHome build directory
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Path to the idedata JSON, or None if it was not found
|
|
||||||
"""
|
|
||||||
for candidate in idedata_candidates(build_path):
|
|
||||||
if candidate.is_file():
|
|
||||||
return candidate
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _find_in_platformio_packages(tool_name: str) -> str | None:
|
def _find_in_platformio_packages(tool_name: str) -> str | None:
|
||||||
"""Search for a tool in PlatformIO package directories.
|
"""Search for a tool in PlatformIO package directories.
|
||||||
|
|
||||||
|
|||||||
@@ -12,9 +12,12 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Awaitable, Callable
|
from collections.abc import Awaitable, Callable
|
||||||
import threading
|
import threading
|
||||||
|
from typing import Generic, TypeVar
|
||||||
|
|
||||||
|
_T = TypeVar("_T")
|
||||||
|
|
||||||
|
|
||||||
class AsyncThreadRunner[T](threading.Thread):
|
class AsyncThreadRunner(threading.Thread, Generic[_T]):
|
||||||
"""Run an async coroutine in a daemon thread and expose its result.
|
"""Run an async coroutine in a daemon thread and expose its result.
|
||||||
|
|
||||||
The runner catches all exceptions from the coroutine and stores them in
|
The runner catches all exceptions from the coroutine and stores them in
|
||||||
@@ -32,10 +35,10 @@ class AsyncThreadRunner[T](threading.Thread):
|
|||||||
result = runner.result
|
result = runner.result
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, coro_factory: Callable[[], Awaitable[T]]) -> None:
|
def __init__(self, coro_factory: Callable[[], Awaitable[_T]]) -> None:
|
||||||
super().__init__(daemon=True)
|
super().__init__(daemon=True)
|
||||||
self._coro_factory = coro_factory
|
self._coro_factory = coro_factory
|
||||||
self.result: T | None = None
|
self.result: _T | None = None
|
||||||
self.exception: BaseException | None = None
|
self.exception: BaseException | None = None
|
||||||
self.event = threading.Event()
|
self.event = threading.Event()
|
||||||
|
|
||||||
|
|||||||
+17
-37
@@ -6,23 +6,8 @@ from pathlib import Path
|
|||||||
from esphome.components.esp32 import get_esp32_variant, idf_version
|
from esphome.components.esp32 import get_esp32_variant, idf_version
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.core import CORE
|
from esphome.core import CORE
|
||||||
from esphome.framework_helpers import (
|
|
||||||
get_project_compile_flags,
|
|
||||||
get_project_cxx_compile_flags,
|
|
||||||
get_project_link_flags,
|
|
||||||
)
|
|
||||||
from esphome.helpers import mkdir_p, write_file_if_changed
|
from esphome.helpers import mkdir_p, write_file_if_changed
|
||||||
|
from esphome.writer import update_storage_json
|
||||||
# Replaces the IDF default C++ standard (-std=gnu++2b appended to
|
|
||||||
# CXX_COMPILE_OPTIONS by project.cmake's __build_init) with the one set via
|
|
||||||
# cg.set_cpp_standard(). Emitted between include(project.cmake) and project(),
|
|
||||||
# i.e. after IDF appends its default and before the options are consumed, and
|
|
||||||
# applies project-wide like PlatformIO build_unflags.
|
|
||||||
CPP_STANDARD_TEMPLATE = """\
|
|
||||||
idf_build_get_property(esphome_cxx_compile_options CXX_COMPILE_OPTIONS)
|
|
||||||
list(FILTER esphome_cxx_compile_options EXCLUDE REGEX "^-std=")
|
|
||||||
list(APPEND esphome_cxx_compile_options "-std={standard}")
|
|
||||||
idf_build_set_property(CXX_COMPILE_OPTIONS "${{esphome_cxx_compile_options}}")"""
|
|
||||||
|
|
||||||
|
|
||||||
def get_available_components() -> list[str] | None:
|
def get_available_components() -> list[str] | None:
|
||||||
@@ -89,26 +74,17 @@ def get_project_cmakelists(minimal: bool = False) -> str:
|
|||||||
# esphome__micro-mp3) rather than just src/. Required so suppressions
|
# esphome__micro-mp3) rather than just src/. Required so suppressions
|
||||||
# like ``-Wno-error=maybe-uninitialized`` actually silence warnings in
|
# like ``-Wno-error=maybe-uninitialized`` actually silence warnings in
|
||||||
# third-party components we don't author.
|
# third-party components we don't author.
|
||||||
project_compile_opts = get_project_compile_flags()
|
project_compile_opts = [
|
||||||
|
flag
|
||||||
|
for flag in sorted(CORE.build_flags)
|
||||||
|
if flag.startswith("-D")
|
||||||
|
or (flag.startswith("-W") and not flag.startswith("-Wl,"))
|
||||||
|
]
|
||||||
extra_compile_options = "\n".join(
|
extra_compile_options = "\n".join(
|
||||||
f'idf_build_set_property(COMPILE_OPTIONS "{flag}" APPEND)'
|
f'idf_build_set_property(COMPILE_OPTIONS "{flag}" APPEND)'
|
||||||
for flag in project_compile_opts
|
for flag in project_compile_opts
|
||||||
)
|
)
|
||||||
|
|
||||||
# Flags registered via cg.add_cxx_build_flag() go on CXX_COMPILE_OPTIONS
|
|
||||||
# (not COMPILE_OPTIONS) because GCC warns when a C++-only flag such as
|
|
||||||
# -Wno-volatile is passed on a C compile.
|
|
||||||
cxx_compile_options = "\n".join(
|
|
||||||
f'idf_build_set_property(CXX_COMPILE_OPTIONS "{flag}" APPEND)'
|
|
||||||
for flag in get_project_cxx_compile_flags()
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
# Per-project list exposed as a CMake variable so converted PIO libs
|
||||||
# can reference ${ESPHOME_PROJECT_MANAGED_COMPONENTS} without baking
|
# can reference ${ESPHOME_PROJECT_MANAGED_COMPONENTS} without baking
|
||||||
# project-specific names into their cached CMakeLists.
|
# project-specific names into their cached CMakeLists.
|
||||||
@@ -165,10 +141,6 @@ set(EXTRA_COMPONENT_DIRS ${{CMAKE_SOURCE_DIR}}/src)
|
|||||||
|
|
||||||
include($ENV{{IDF_PATH}}/tools/cmake/project.cmake)
|
include($ENV{{IDF_PATH}}/tools/cmake/project.cmake)
|
||||||
|
|
||||||
{cpp_standard_options}
|
|
||||||
|
|
||||||
{cxx_compile_options}
|
|
||||||
|
|
||||||
{extra_compile_options}
|
{extra_compile_options}
|
||||||
|
|
||||||
{managed_components_property}
|
{managed_components_property}
|
||||||
@@ -198,8 +170,8 @@ def get_component_cmakelists() -> str:
|
|||||||
# Extract linker options (-Wl, flags). Compile flags (-D, -W) are
|
# Extract linker options (-Wl, flags). Compile flags (-D, -W) are
|
||||||
# emitted project-wide via idf_build_set_property in
|
# emitted project-wide via idf_build_set_property in
|
||||||
# get_project_cmakelists so they reach every component, not just src/.
|
# get_project_cmakelists so they reach every component, not just src/.
|
||||||
link_opts = get_project_link_flags()
|
link_opts = [flag for flag in CORE.build_flags if flag.startswith("-Wl,")]
|
||||||
link_opts_str = "\n ".join(link_opts) if link_opts else ""
|
link_opts_str = "\n ".join(sorted(link_opts)) if link_opts else ""
|
||||||
|
|
||||||
return f"""\
|
return f"""\
|
||||||
# Auto-generated by ESPHome
|
# Auto-generated by ESPHome
|
||||||
@@ -229,6 +201,9 @@ idf_component_register(
|
|||||||
REQUIRES ${{ESPHOME_PROJECT_BUILTIN_COMPONENTS}}
|
REQUIRES ${{ESPHOME_PROJECT_BUILTIN_COMPONENTS}}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Apply C++ standard
|
||||||
|
target_compile_features(${{COMPONENT_LIB}} PUBLIC cxx_std_20)
|
||||||
|
|
||||||
# ESPHome linker options
|
# ESPHome linker options
|
||||||
target_link_options(${{COMPONENT_LIB}} PUBLIC
|
target_link_options(${{COMPONENT_LIB}} PUBLIC
|
||||||
{link_opts_str}
|
{link_opts_str}
|
||||||
@@ -238,6 +213,11 @@ target_link_options(${{COMPONENT_LIB}} PUBLIC
|
|||||||
|
|
||||||
def write_project(minimal: bool = False) -> None:
|
def write_project(minimal: bool = False) -> None:
|
||||||
"""Write ESP-IDF project files."""
|
"""Write ESP-IDF project files."""
|
||||||
|
# Refresh <data_dir>/storage/<name>.yaml.json so the dashboard's
|
||||||
|
# /info and /downloads endpoints can locate the build (they 404
|
||||||
|
# otherwise). This mirrors the PlatformIO build-gen path's call
|
||||||
|
# in build_gen/platformio.py:write_ini().
|
||||||
|
update_storage_json()
|
||||||
mkdir_p(CORE.build_path)
|
mkdir_p(CORE.build_path)
|
||||||
mkdir_p(CORE.relative_src_path())
|
mkdir_p(CORE.relative_src_path())
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from esphome.const import __version__
|
from esphome.const import __version__
|
||||||
from esphome.core import CORE
|
from esphome.core import CORE
|
||||||
from esphome.helpers import mkdir_p, read_file, write_file_if_changed
|
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_BEGIN = "; ========== AUTO GENERATED CODE BEGIN ==========="
|
||||||
INI_AUTO_GENERATE_END = "; =========== AUTO GENERATED CODE END ============"
|
INI_AUTO_GENERATE_END = "; =========== AUTO GENERATED CODE END ============"
|
||||||
@@ -33,27 +33,12 @@ def format_ini(data: dict[str, str | list[str]]) -> str:
|
|||||||
return content
|
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():
|
def get_ini_content():
|
||||||
CORE.add_platformio_option(
|
CORE.add_platformio_option(
|
||||||
"lib_deps",
|
"lib_deps",
|
||||||
[x.as_lib_dep for x in CORE.platformio_libraries.values()]
|
[x.as_lib_dep for x in CORE.platformio_libraries.values()]
|
||||||
+ ["${common.lib_deps}"],
|
+ ["${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
|
# Sort to avoid changing build flags order
|
||||||
CORE.add_platformio_option("build_flags", sorted(CORE.build_flags))
|
CORE.add_platformio_option("build_flags", sorted(CORE.build_flags))
|
||||||
|
|
||||||
@@ -73,6 +58,7 @@ def get_ini_content():
|
|||||||
|
|
||||||
|
|
||||||
def write_ini(content):
|
def write_ini(content):
|
||||||
|
update_storage_json()
|
||||||
path = CORE.relative_build_path("platformio.ini")
|
path = CORE.relative_build_path("platformio.ini")
|
||||||
|
|
||||||
if path.is_file():
|
if path.is_file():
|
||||||
@@ -108,6 +94,7 @@ Import("env")
|
|||||||
def write_cxx_flags_script() -> None:
|
def write_cxx_flags_script() -> None:
|
||||||
path = CORE.relative_build_path(CXX_FLAGS_FILE_NAME)
|
path = CORE.relative_build_path(CXX_FLAGS_FILE_NAME)
|
||||||
contents = CXX_FLAGS_FILE_CONTENTS
|
contents = CXX_FLAGS_FILE_CONTENTS
|
||||||
for flag in sorted(CORE.cxx_build_flags):
|
if not CORE.is_host:
|
||||||
contents += f'env.Append(CXXFLAGS=["{flag}"])\n'
|
contents += 'env.Append(CXXFLAGS=["-Wno-volatile"])'
|
||||||
|
contents += "\n"
|
||||||
write_file_if_changed(path, contents)
|
write_file_if_changed(path, contents)
|
||||||
|
|||||||
+2
-35
@@ -7,7 +7,7 @@ and compiled directly: ``esphome compile my_device.esphomebundle.tar.gz``
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass
|
||||||
from enum import StrEnum
|
from enum import StrEnum
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
@@ -32,8 +32,6 @@ from esphome.core import CORE, EsphomeError
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DOMAIN = "bundle"
|
|
||||||
|
|
||||||
BUNDLE_EXTENSION = ".esphomebundle.tar.gz"
|
BUNDLE_EXTENSION = ".esphomebundle.tar.gz"
|
||||||
MANIFEST_FILENAME = "manifest.json"
|
MANIFEST_FILENAME = "manifest.json"
|
||||||
CURRENT_MANIFEST_VERSION = 1
|
CURRENT_MANIFEST_VERSION = 1
|
||||||
@@ -122,32 +120,6 @@ def _find_used_secret_keys(yaml_files: list[Path]) -> set[str]:
|
|||||||
return keys
|
return keys
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class BundleData:
|
|
||||||
"""Files components asked to include, keyed under DOMAIN in CORE.data."""
|
|
||||||
|
|
||||||
extra_files: list[Path] = field(default_factory=list)
|
|
||||||
|
|
||||||
|
|
||||||
def _get_data() -> BundleData:
|
|
||||||
if DOMAIN not in CORE.data:
|
|
||||||
CORE.data[DOMAIN] = BundleData()
|
|
||||||
return CORE.data[DOMAIN]
|
|
||||||
|
|
||||||
|
|
||||||
def add_bundle_file(path: Path) -> None:
|
|
||||||
"""Register a file that a bundle must include.
|
|
||||||
|
|
||||||
Bundle discovery walks the validated config, so it only finds files the config
|
|
||||||
names. Components call this during validation for files it cannot see, such as a
|
|
||||||
file that is referenced from inside another file.
|
|
||||||
|
|
||||||
A relative path is taken as relative to the config directory. Files outside the
|
|
||||||
config directory are skipped when the bundle is built.
|
|
||||||
"""
|
|
||||||
_get_data().extra_files.append(CORE.relative_config_path(path))
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class BundleFile:
|
class BundleFile:
|
||||||
"""A file to include in the bundle."""
|
"""A file to include in the bundle."""
|
||||||
@@ -314,18 +286,13 @@ class ConfigBundleCreator:
|
|||||||
with known file extensions are also resolved and checked.
|
with known file extensions are also resolved and checked.
|
||||||
|
|
||||||
Core ESPHome concepts that use relative paths or directories
|
Core ESPHome concepts that use relative paths or directories
|
||||||
are handled explicitly. Files the config does not name at all are
|
are handled explicitly.
|
||||||
registered by their component with add_bundle_file().
|
|
||||||
"""
|
"""
|
||||||
config = self._config
|
config = self._config
|
||||||
|
|
||||||
# Generic walk: find all file paths in the validated config
|
# Generic walk: find all file paths in the validated config
|
||||||
self._walk_config_for_files(config)
|
self._walk_config_for_files(config)
|
||||||
|
|
||||||
# Files registered by components during validation
|
|
||||||
for extra_file in _get_data().extra_files:
|
|
||||||
self._add_file(extra_file)
|
|
||||||
|
|
||||||
# --- Core ESPHome concepts needing explicit handling ---
|
# --- Core ESPHome concepts needing explicit handling ---
|
||||||
|
|
||||||
# esphome.includes / includes_c - can be relative paths and directories
|
# esphome.includes / includes_c - can be relative paths and directories
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
# pylint: disable=unused-import
|
# pylint: disable=unused-import
|
||||||
from esphome.cpp_generator import ( # noqa: F401
|
from esphome.cpp_generator import ( # noqa: F401
|
||||||
ArrayInitializer,
|
ArrayInitializer,
|
||||||
ComponentMarker,
|
|
||||||
Expression,
|
Expression,
|
||||||
FlashStringLiteral,
|
FlashStringLiteral,
|
||||||
LineComment,
|
LineComment,
|
||||||
@@ -26,7 +25,6 @@ from esphome.cpp_generator import ( # noqa: F401
|
|||||||
add,
|
add,
|
||||||
add_build_flag,
|
add_build_flag,
|
||||||
add_build_unflag,
|
add_build_unflag,
|
||||||
add_cxx_build_flag,
|
|
||||||
add_define,
|
add_define,
|
||||||
add_global,
|
add_global,
|
||||||
add_library,
|
add_library,
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
# Importing `esphome.loader` here installs the component-alias
|
|
||||||
# ``sys.meta_path`` finder before any submodule lookup runs. Without this,
|
|
||||||
# `from esphome.components import <legacy_alias>` from a fresh interpreter
|
|
||||||
# can race the finder install and raise ImportError, since the legacy
|
|
||||||
# alias dir no longer exists on disk.
|
|
||||||
from esphome import loader as _loader # noqa: F401
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ void A01nyubComponent::check_buffer_() {
|
|||||||
if (this->buffer_[3] == checksum) {
|
if (this->buffer_[3] == checksum) {
|
||||||
float distance = (this->buffer_[1] << 8) + this->buffer_[2];
|
float distance = (this->buffer_[1] << 8) + this->buffer_[2];
|
||||||
if (distance > 280) {
|
if (distance > 280) {
|
||||||
float meters = distance / 1000.0f;
|
float meters = distance / 1000.0;
|
||||||
ESP_LOGV(TAG, "Distance from sensor: %f mm, %f m", distance, meters);
|
ESP_LOGV(TAG, "Distance from sensor: %f mm, %f m", distance, meters);
|
||||||
this->publish_state(meters);
|
this->publish_state(meters);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
namespace esphome::a01nyub {
|
namespace esphome::a01nyub {
|
||||||
|
|
||||||
class A01nyubComponent final : public sensor::Sensor, public Component, public uart::UARTDevice {
|
class A01nyubComponent : public sensor::Sensor, public Component, public uart::UARTDevice {
|
||||||
public:
|
public:
|
||||||
// Nothing really public.
|
// Nothing really public.
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
namespace esphome::a02yyuw {
|
namespace esphome::a02yyuw {
|
||||||
|
|
||||||
class A02yyuwComponent final : public sensor::Sensor, public Component, public uart::UARTDevice {
|
class A02yyuwComponent : public sensor::Sensor, public Component, public uart::UARTDevice {
|
||||||
public:
|
public:
|
||||||
// Nothing really public.
|
// Nothing really public.
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
namespace esphome::a4988 {
|
namespace esphome::a4988 {
|
||||||
|
|
||||||
class A4988 final : public stepper::Stepper, public Component {
|
class A4988 : public stepper::Stepper, public Component {
|
||||||
public:
|
public:
|
||||||
void set_step_pin(GPIOPin *step_pin) { step_pin_ = step_pin; }
|
void set_step_pin(GPIOPin *step_pin) { step_pin_ = step_pin; }
|
||||||
void set_dir_pin(GPIOPin *dir_pin) { dir_pin_ = dir_pin; }
|
void set_dir_pin(GPIOPin *dir_pin) { dir_pin_ = dir_pin; }
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ enum SaturationVaporPressureEquation {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/// This class implements calculation of absolute humidity from temperature and relative humidity.
|
/// This class implements calculation of absolute humidity from temperature and relative humidity.
|
||||||
class AbsoluteHumidityComponent final : public sensor::Sensor, public Component {
|
class AbsoluteHumidityComponent : public sensor::Sensor, public Component {
|
||||||
public:
|
public:
|
||||||
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; }
|
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; }
|
||||||
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
|
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
|
||||||
|
|||||||
@@ -216,7 +216,7 @@ void AcDimmer::setup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void AcDimmer::write_state(float state) {
|
void AcDimmer::write_state(float state) {
|
||||||
state = std::acos(1 - (2 * state)) / std::numbers::pi_v<float>; // RMS power compensation
|
state = std::acos(1 - (2 * state)) / std::numbers::pi; // RMS power compensation
|
||||||
auto new_value = static_cast<uint16_t>(roundf(state * 65535));
|
auto new_value = static_cast<uint16_t>(roundf(state * 65535));
|
||||||
if (new_value != 0 && this->store_.value == 0)
|
if (new_value != 0 && this->store_.value == 0)
|
||||||
this->store_.init_cycle = this->init_with_half_cycle_;
|
this->store_.init_cycle = this->init_with_half_cycle_;
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ struct AcDimmerDataStore {
|
|||||||
#endif
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
class AcDimmer final : public output::FloatOutput, public Component {
|
class AcDimmer : public output::FloatOutput, public Component {
|
||||||
public:
|
public:
|
||||||
void setup() override;
|
void setup() override;
|
||||||
|
|
||||||
|
|||||||
@@ -227,12 +227,12 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
|
|||||||
|
|
||||||
def validate_adc_pin(value):
|
def validate_adc_pin(value):
|
||||||
if str(value).upper() == "VCC":
|
if str(value).upper() == "VCC":
|
||||||
if CORE.is_rp2:
|
if CORE.is_rp2040:
|
||||||
return pins.internal_gpio_input_pin_schema(29)
|
return pins.internal_gpio_input_pin_schema(29)
|
||||||
return cv.only_on([PLATFORM_ESP8266])("VCC")
|
return cv.only_on([PLATFORM_ESP8266])("VCC")
|
||||||
|
|
||||||
if str(value).upper() == "TEMPERATURE":
|
if str(value).upper() == "TEMPERATURE":
|
||||||
return cv.only_on_rp2("TEMPERATURE")
|
return cv.only_on_rp2040("TEMPERATURE")
|
||||||
|
|
||||||
if CORE.is_esp32:
|
if CORE.is_esp32:
|
||||||
conf = pins.internal_gpio_input_pin_schema(value)
|
conf = pins.internal_gpio_input_pin_schema(value)
|
||||||
@@ -261,11 +261,11 @@ def validate_adc_pin(value):
|
|||||||
raise cv.Invalid("ESP8266: Only pin A0 (GPIO17) supports ADC")
|
raise cv.Invalid("ESP8266: Only pin A0 (GPIO17) supports ADC")
|
||||||
return conf
|
return conf
|
||||||
|
|
||||||
if CORE.is_rp2:
|
if CORE.is_rp2040:
|
||||||
conf = pins.internal_gpio_input_pin_schema(value)
|
conf = pins.internal_gpio_input_pin_schema(value)
|
||||||
number = conf[CONF_NUMBER]
|
number = conf[CONF_NUMBER]
|
||||||
if number not in (26, 27, 28, 29):
|
if number not in (26, 27, 28, 29):
|
||||||
raise cv.Invalid("RP2: Only pins 26, 27, 28 and 29 support ADC")
|
raise cv.Invalid("RP2040: Only pins 26, 27, 28 and 29 support ADC")
|
||||||
return conf
|
return conf
|
||||||
|
|
||||||
if CORE.is_libretiny:
|
if CORE.is_libretiny:
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ template<typename T> class Aggregator {
|
|||||||
SamplingMode mode_{SamplingMode::AVG};
|
SamplingMode mode_{SamplingMode::AVG};
|
||||||
};
|
};
|
||||||
|
|
||||||
class ADCSensor final : public sensor::Sensor, public PollingComponent, public voltage_sampler::VoltageSampler {
|
class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage_sampler::VoltageSampler {
|
||||||
public:
|
public:
|
||||||
/// Update the sensor's state by reading the current ADC value.
|
/// Update the sensor's state by reading the current ADC value.
|
||||||
/// This method is called periodically based on the update interval.
|
/// This method is called periodically based on the update interval.
|
||||||
@@ -123,9 +123,9 @@ class ADCSensor final : public sensor::Sensor, public PollingComponent, public v
|
|||||||
void set_autorange(bool autorange) { this->autorange_ = autorange; }
|
void set_autorange(bool autorange) { this->autorange_ = autorange; }
|
||||||
#endif // USE_ESP32
|
#endif // USE_ESP32
|
||||||
|
|
||||||
#ifdef USE_RP2
|
#ifdef USE_RP2040
|
||||||
void set_is_temperature() { this->is_temperature_ = true; }
|
void set_is_temperature() { this->is_temperature_ = true; }
|
||||||
#endif // USE_RP2
|
#endif // USE_RP2040
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
uint8_t sample_count_{1};
|
uint8_t sample_count_{1};
|
||||||
@@ -152,9 +152,9 @@ class ADCSensor final : public sensor::Sensor, public PollingComponent, public v
|
|||||||
static adc_oneshot_unit_handle_t shared_adc_handles[2];
|
static adc_oneshot_unit_handle_t shared_adc_handles[2];
|
||||||
#endif // USE_ESP32
|
#endif // USE_ESP32
|
||||||
|
|
||||||
#ifdef USE_RP2
|
#ifdef USE_RP2040
|
||||||
bool is_temperature_{false};
|
bool is_temperature_{false};
|
||||||
#endif // USE_RP2
|
#endif // USE_RP2040
|
||||||
|
|
||||||
#ifdef USE_ZEPHYR
|
#ifdef USE_ZEPHYR
|
||||||
const struct adc_dt_spec *channel_ = nullptr;
|
const struct adc_dt_spec *channel_ = nullptr;
|
||||||
|
|||||||
+8
-11
@@ -1,4 +1,4 @@
|
|||||||
#ifdef USE_RP2
|
#ifdef USE_RP2040
|
||||||
|
|
||||||
#include "adc_sensor.h"
|
#include "adc_sensor.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
namespace esphome::adc {
|
namespace esphome::adc {
|
||||||
|
|
||||||
static const char *const TAG = "adc.rp2";
|
static const char *const TAG = "adc.rp2040";
|
||||||
|
|
||||||
void ADCSensor::setup() {
|
void ADCSensor::setup() {
|
||||||
static bool initialized = false;
|
static bool initialized = false;
|
||||||
@@ -66,18 +66,15 @@ float ADCSensor::sample() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
uint8_t pin = this->pin_->get_pin();
|
uint8_t pin = this->pin_->get_pin();
|
||||||
#if defined(CYW43_USES_VSYS_PIN) && defined(USE_WIFI)
|
#ifdef CYW43_USES_VSYS_PIN
|
||||||
if (pin == PICO_VSYS_PIN) {
|
if (pin == PICO_VSYS_PIN) {
|
||||||
// Measuring VSYS on Raspberry Pico W needs to be wrapped with
|
// Measuring VSYS on Raspberry Pico W needs to be wrapped with
|
||||||
// `cyw43_thread_enter()`/`cyw43_thread_exit()` as discussed in
|
// `cyw43_thread_enter()`/`cyw43_thread_exit()` as discussed in
|
||||||
// https://github.com/raspberrypi/pico-sdk/issues/1222, since Wifi chip and
|
// https://github.com/raspberrypi/pico-sdk/issues/1222, since Wifi chip and
|
||||||
// VSYS ADC both share GPIO29.
|
// VSYS ADC both share GPIO29
|
||||||
// The USE_WIFI guard is required because CYW43_USES_VSYS_PIN can be defined
|
|
||||||
// transitively (e.g. via lwip_wrap.h) even on non-WiFi boards where the CYW43
|
|
||||||
// driver is never initialized; calling cyw43_thread_enter() there hard-faults.
|
|
||||||
cyw43_thread_enter();
|
cyw43_thread_enter();
|
||||||
}
|
}
|
||||||
#endif // defined(CYW43_USES_VSYS_PIN) && defined(USE_WIFI)
|
#endif // CYW43_USES_VSYS_PIN
|
||||||
|
|
||||||
adc_gpio_init(pin);
|
adc_gpio_init(pin);
|
||||||
adc_select_input(pin - 26);
|
adc_select_input(pin - 26);
|
||||||
@@ -87,11 +84,11 @@ float ADCSensor::sample() {
|
|||||||
aggr.add_sample(raw);
|
aggr.add_sample(raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
#if defined(CYW43_USES_VSYS_PIN) && defined(USE_WIFI)
|
#ifdef CYW43_USES_VSYS_PIN
|
||||||
if (pin == PICO_VSYS_PIN) {
|
if (pin == PICO_VSYS_PIN) {
|
||||||
cyw43_thread_exit();
|
cyw43_thread_exit();
|
||||||
}
|
}
|
||||||
#endif // defined(CYW43_USES_VSYS_PIN) && defined(USE_WIFI)
|
#endif // CYW43_USES_VSYS_PIN
|
||||||
|
|
||||||
if (this->output_raw_) {
|
if (this->output_raw_) {
|
||||||
return aggr.aggregate();
|
return aggr.aggregate();
|
||||||
@@ -102,4 +99,4 @@ float ADCSensor::sample() {
|
|||||||
|
|
||||||
} // namespace esphome::adc
|
} // namespace esphome::adc
|
||||||
|
|
||||||
#endif // USE_RP2
|
#endif // USE_RP2040
|
||||||
@@ -201,7 +201,7 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform(
|
|||||||
PlatformFramework.ESP32_IDF,
|
PlatformFramework.ESP32_IDF,
|
||||||
},
|
},
|
||||||
"adc_sensor_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
|
"adc_sensor_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
|
||||||
"adc_sensor_rp2.cpp": {PlatformFramework.RP2_ARDUINO},
|
"adc_sensor_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO},
|
||||||
"adc_sensor_libretiny.cpp": {
|
"adc_sensor_libretiny.cpp": {
|
||||||
PlatformFramework.BK72XX_ARDUINO,
|
PlatformFramework.BK72XX_ARDUINO,
|
||||||
PlatformFramework.RTL87XX_ARDUINO,
|
PlatformFramework.RTL87XX_ARDUINO,
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
|
|
||||||
namespace esphome::adc128s102 {
|
namespace esphome::adc128s102 {
|
||||||
|
|
||||||
class ADC128S102 final : public Component,
|
class ADC128S102 : public Component,
|
||||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
|
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
|
||||||
spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_10MHZ> {
|
spi::DATA_RATE_10MHZ> {
|
||||||
public:
|
public:
|
||||||
ADC128S102() = default;
|
ADC128S102() = default;
|
||||||
|
|
||||||
|
|||||||
@@ -9,10 +9,10 @@
|
|||||||
|
|
||||||
namespace esphome::adc128s102 {
|
namespace esphome::adc128s102 {
|
||||||
|
|
||||||
class ADC128S102Sensor final : public PollingComponent,
|
class ADC128S102Sensor : public PollingComponent,
|
||||||
public Parented<ADC128S102>,
|
public Parented<ADC128S102>,
|
||||||
public sensor::Sensor,
|
public sensor::Sensor,
|
||||||
public voltage_sampler::VoltageSampler {
|
public voltage_sampler::VoltageSampler {
|
||||||
public:
|
public:
|
||||||
ADC128S102Sensor(uint8_t channel);
|
ADC128S102Sensor(uint8_t channel);
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
namespace esphome::addressable_light {
|
namespace esphome::addressable_light {
|
||||||
|
|
||||||
class AddressableLightDisplay final : public display::DisplayBuffer {
|
class AddressableLightDisplay : public display::DisplayBuffer {
|
||||||
public:
|
public:
|
||||||
light::AddressableLight *get_light() const { return this->light_; }
|
light::AddressableLight *get_light() const { return this->light_; }
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
CODEOWNERS = ["@kpfleming"]
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user