Compare commits

..
Author SHA1 Message Date
CodSpeed Bot 2c4ed23a6a [api] Format packed varint counter 2026-10-08 00:46:46 +00:00
CodSpeed Bot dfd0a5a1f2 [api] Count packed varints in word-sized chunks 2026-10-08 00:21:27 +00:00
3444 changed files with 11845 additions and 83463 deletions

No files matched your search

+1 -1
View File
@@ -32,7 +32,7 @@ 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@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with: with:
enable-cache: true enable-cache: true
# Pull request saves land in per-PR scopes nothing else can # Pull request saves land in per-PR scopes nothing else can
+1 -1
View File
@@ -53,7 +53,7 @@ module.exports = async ({ github, context }) => {
}); });
const currentLabels = currentLabelsData.map(label => label.name); const currentLabels = currentLabelsData.map(label => label.name);
const managedLabels = currentLabels.filter(label => const managedLabels = currentLabels.filter(label =>
label.startsWith('component: ') || label.startsWith('platform: ') || MANAGED_LABELS.includes(label) label.startsWith('component: ') || MANAGED_LABELS.includes(label)
); );
// Check for mega-PR early - if present, skip most automatic labeling // Check for mega-PR early - if present, skip most automatic labeling
+10 -26
View File
@@ -42,8 +42,8 @@ function generateReviewMessages(finalLabels, originalLabelCount, deprecatedInfo,
let message = `${TOO_BIG_MARKER}\n### 📦 Pull Request Size\n\n`; let message = `${TOO_BIG_MARKER}\n### 📦 Pull Request Size\n\n`;
message += message +=
`Hey @${prAuthor}, thanks for the contribution. This PR is on the ` + `Hey @${prAuthor}, thanks for the contribution! Just a heads up, ` +
`large side `; `this PR is on the large side `;
if (tooManyLabels && tooManyChanges) { if (tooManyLabels && tooManyChanges) {
message += message +=
@@ -57,31 +57,16 @@ function generateReviewMessages(finalLabels, originalLabelCount, deprecatedInfo,
} }
message += `, which makes it harder for maintainers to review.\n\n`; message += `, which makes it harder for maintainers to review.\n\n`;
let condition;
if (tooManyLabels && tooManyChanges) {
condition =
`is at most ${TOO_BIG_THRESHOLD} lines (excluding tests) and touches ` +
`at most ${MAX_LABELS} components/areas`;
} else if (tooManyLabels) {
condition = `touches at most ${MAX_LABELS} components/areas`;
} else {
condition = `is at most ${TOO_BIG_THRESHOLD} lines (excluding tests)`;
}
message += message +=
`**This PR will not be reviewed until it ${condition}.** We know ` + `Smaller, focused PRs tend to be reviewed much faster since they ` +
`that is not what you want to hear after this much work, and we want ` + `fit into the short gaps between other maintainer work; large ones ` +
`to be upfront so you do not wait for a review that is not coming: ` + `often have to wait for a rare long uninterrupted block of time. ` +
`please split the change. The label is removed automatically once ` + `If you can break this up into smaller pieces that can be reviewed ` +
`the PR ${condition}.\n\n`; `independently, it will almost certainly land faster overall.\n\n`;
message += message +=
`There is no exception you can request. Rarely, a maintainer agrees in ` + `Before putting more time in, it's also worth popping into ` +
`\`#devs\` on [Discord](https://esphome.io/chat) to review a change that ` + `\`#devs\` on [Discord](https://esphome.io/chat) so we can help ` +
`genuinely cannot be split as one piece and adds the \`mega-pr\` label, ` + `you scope things and flag anything already in flight.\n\n`;
`but that is rare enough that you should plan on splitting rather than ` +
`wait for it. Smaller, focused PRs are picked up much faster because ` +
`they fit into the short gaps between other maintainer work. For a new ` +
`component, the first PR brings the hub and a single platform (for ` +
`example \`sensor\`); each further platform is its own PR on top of it.\n\n`;
message += message +=
`For more details (including how to split the work up), see: ` + `For more details (including how to split the work up), see: ` +
`https://developers.esphome.io/contributing/submitting-your-work/` + `https://developers.esphome.io/contributing/submitting-your-work/` +
@@ -229,7 +214,6 @@ async function handleMaintainerAccessComment(github, context, maintainerAccess)
} }
module.exports = { module.exports = {
generateReviewMessages,
handleReviews, handleReviews,
handleMaintainerAccessComment handleMaintainerAccessComment
}; };
@@ -1,38 +0,0 @@
const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const { generateReviewMessages } = require('../reviews');
const MAX_LABELS = 15;
const TOO_BIG_THRESHOLD = 1000;
function tooBigMessage(labelCount, additions) {
const files = [{ filename: 'esphome/components/a/a.cpp', additions, deletions: 0 }];
const messages = generateReviewMessages(['too-big'], labelCount, null, files, additions, 0, 'someone', MAX_LABELS, TOO_BIG_THRESHOLD);
assert.equal(messages.length, 1);
return messages[0];
}
describe('too-big review message', () => {
it('names the line cap when the PR is too long', () => {
const message = tooBigMessage(3, 1500);
assert.match(message, /will not be reviewed until it is at most 1000 lines \(excluding tests\)\./);
assert.match(message, /removed automatically once the PR is at most 1000 lines/);
assert.doesNotMatch(message, /components\/areas\./);
});
it('names the area cap when the PR touches too many areas but is short', () => {
const message = tooBigMessage(20, 200);
assert.match(message, /will not be reviewed until it touches at most 15 components\/areas\./);
assert.doesNotMatch(message, /until it is at most 1000 lines/);
});
it('names both caps when both are exceeded', () => {
const message = tooBigMessage(20, 1500);
assert.match(message, /is at most 1000 lines \(excluding tests\) and touches at most 15 components\/areas\./);
});
it('tells new-component authors to bring one platform first', () => {
assert.match(tooBigMessage(3, 1500), /hub and a single platform/);
assert.match(tooBigMessage(3, 1500), /mega-pr/);
});
});
+2 -2
View File
@@ -29,7 +29,7 @@ jobs:
- 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@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with: with:
enable-cache: true enable-cache: true
# Pull-request-only workflow: a save could never be shared and # Pull-request-only workflow: a save could never be shared and
@@ -97,7 +97,7 @@ jobs:
run: git diff run: git diff
- if: failure() - if: failure()
name: Archive artifacts name: Archive artifacts
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: generated-proto-files name: generated-proto-files
path: | path: |
+2 -2
View File
@@ -128,7 +128,7 @@ jobs:
- name: Upload compile-test image artifact - name: Upload compile-test image artifact
if: matrix.os == 'ubuntu-24.04' && matrix.build_type == 'docker' if: matrix.os == 'ubuntu-24.04' && matrix.build_type == 'docker'
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
# The tar is already compressed, so upload it as-is. archive: false # The tar is already compressed, so upload it as-is. archive: false
# skips the redundant zip and makes the file name the artifact name # skips the redundant zip and makes the file name the artifact name
@@ -210,7 +210,7 @@ jobs:
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Download image artifact - name: Download image artifact
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with: with:
name: compile-test-image.tar.zst name: compile-test-image.tar.zst
- name: Load image - name: Load image
+45 -147
View File
@@ -49,7 +49,7 @@ 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@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with: with:
enable-cache: true enable-cache: true
# Pull request saves land in per-PR scopes nothing else can # Pull request saves land in per-PR scopes nothing else can
@@ -81,8 +81,8 @@ jobs:
- name: Install apt packages (cached) - name: Install apt packages (cached)
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3 uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
with: with:
packages: libsdl2-dev libmbedtls-dev ccache packages: libsdl2-dev ccache
version: 1.2 version: 1.1
determine-jobs: determine-jobs:
name: Determine which jobs to run name: Determine which jobs to run
@@ -286,7 +286,7 @@ jobs:
fi fi
echo "version=$version" >> "$GITHUB_OUTPUT" echo "version=$version" >> "$GITHUB_OUTPUT"
- name: Run prek - name: Run prek
uses: j178/prek-action@1382ad72dea72f9d53832f2bcb65b3e87a947204 # v3.0.1 uses: j178/prek-action@4e14d07f9231acabce116ccfca13b13dd9755ece # v3.0.0
with: with:
prek-version: ${{ steps.prek.outputs.version }} prek-version: ${{ steps.prek.outputs.version }}
# This job only runs on pull requests, so nothing ever populates # This job only runs on pull requests, so nothing ever populates
@@ -415,8 +415,8 @@ jobs:
timeout-minutes: 10 timeout-minutes: 10
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3 uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
with: with:
packages: libsdl2-dev libmbedtls-dev ccache packages: libsdl2-dev ccache
version: 1.2 version: 1.1
- name: Set up Python 3.13 - name: Set up Python 3.13
id: python id: python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
@@ -438,7 +438,7 @@ jobs:
- 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@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with: with:
enable-cache: true enable-cache: true
# Pull request saves land in per-PR scopes nothing else can # Pull request saves land in per-PR scopes nothing else can
@@ -477,7 +477,7 @@ jobs:
# Tests that compare rendered output write the image they actually got here, so a # Tests that compare rendered output write the image they actually got here, so a
# failure can be looked at without reproducing the whole build locally. # failure can be looked at without reproducing the whole build locally.
if: failure() if: failure()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: integration-test-artifacts-${{ matrix.bucket.name }} name: integration-test-artifacts-${{ matrix.bucket.name }}
path: test_artifacts/ path: test_artifacts/
@@ -488,7 +488,7 @@ jobs:
# script/update_integration_test_durations.py; only full matrix dev # script/update_integration_test_durations.py; only full matrix dev
# runs produce usable data. # runs produce usable data.
if: github.ref == 'refs/heads/dev' && needs.determine-jobs.outputs.integration-run-all == 'true' if: github.ref == 'refs/heads/dev' && needs.determine-jobs.outputs.integration-run-all == 'true'
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: junit-integration-${{ strategy.job-index }} name: junit-integration-${{ strategy.job-index }}
path: junit-integration.xml path: junit-integration.xml
@@ -528,7 +528,7 @@ jobs:
script/check_import_time.py --check --har importtime.har script/check_import_time.py --check --har importtime.har
- name: Upload waterfall HAR - name: Upload waterfall HAR
if: always() if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: import-time-waterfall name: import-time-waterfall
path: importtime.har path: importtime.har
@@ -626,7 +626,7 @@ jobs:
apt-get install -y libc6-dbg apt-get install -y libc6-dbg
- name: Run CodSpeed benchmarks - name: Run CodSpeed benchmarks
uses: CodSpeedHQ/action@f842b9a3ffe213dbb81768fd7f7eeb4dfcf1c369 # v5.4.2 uses: CodSpeedHQ/action@c4fd08a3a159bd0cc208da1e0edf32b8c47d75e5 # v5.4.0
with: with:
run: | run: |
. venv/bin/activate . venv/bin/activate
@@ -680,7 +680,7 @@ jobs:
include: include:
- id: clang-tidy - id: clang-tidy
name: Run script/clang-tidy for ESP8266 name: Run script/clang-tidy for ESP8266
options: --environment esp8266-arduino-tidy --grep USE_ESP8266 --grep USE_SOCKET_IMPL_LWIP_TCP --grep USE_SHD_FIRMWARE_DATA 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 Arduino
@@ -693,16 +693,12 @@ jobs:
ignore_errors: false ignore_errors: false
- id: clang-tidy - id: clang-tidy
name: Run script/clang-tidy for RP2 name: Run script/clang-tidy for RP2
options: --environment rp2-tidy --grep USE_RP2 --grep USE_BLE_SCAN_RESPONSE_MERGER options: --environment rp2-tidy --grep USE_RP2
pio_cache_key: tidyrp2 pio_cache_key: tidyrp2
- id: clang-tidy
name: Run script/clang-tidy for host
options: --environment host-tidy --grep USE_HOST
pio_cache_key: tidyhost
- id: clang-tidy - id: clang-tidy
name: Run script/clang-tidy for LibreTiny name: Run script/clang-tidy for LibreTiny
environments: bk72xx-tidy ln882h-tidy rtl87xxb-tidy rtl87xxc-tidy environments: bk72xx-tidy ln882h-tidy rtl87xxb-tidy rtl87xxc-tidy
options: --grep USE_LIBRETINY --grep USE_BK72XX --grep USE_RTL87XX --grep USE_LN882X --grep USE_LN882H_BLE --grep USE_SOCKET_IMPL_LWIP_SOCKETS options: --grep USE_LIBRETINY --grep USE_BK72XX --grep USE_RTL87XX --grep USE_LN882X
pio_cache_key: tidylibretiny pio_cache_key: tidylibretiny
steps: steps:
@@ -711,16 +707,6 @@ jobs:
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
- name: Install apt packages (cached)
if: matrix.pio_cache_key == 'tidyhost'
# libsdl2-dev is needed by the sdl component on the host, libmbedtls-dev
# by dsmr and dlms_meter. Packages and version must match seed-apt-cache
# exactly.
timeout-minutes: 10
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
with:
packages: libsdl2-dev libmbedtls-dev ccache
version: 1.2
- name: Restore Python - name: Restore Python
id: restore-python id: restore-python
@@ -907,20 +893,14 @@ jobs:
matrix: matrix:
include: include:
- id: clang-tidy - id: clang-tidy
name: Run script/clang-tidy for ESP32 IDF 1/5 name: Run script/clang-tidy for ESP32 IDF 1/3
options: --environment esp32-idf-tidy --split-num 5 --split-at 1 options: --environment esp32-idf-tidy --split-num 3 --split-at 1
- id: clang-tidy - id: clang-tidy
name: Run script/clang-tidy for ESP32 IDF 2/5 name: Run script/clang-tidy for ESP32 IDF 2/3
options: --environment esp32-idf-tidy --split-num 5 --split-at 2 options: --environment esp32-idf-tidy --split-num 3 --split-at 2
- id: clang-tidy - id: clang-tidy
name: Run script/clang-tidy for ESP32 IDF 3/5 name: Run script/clang-tidy for ESP32 IDF 3/3
options: --environment esp32-idf-tidy --split-num 5 --split-at 3 options: --environment esp32-idf-tidy --split-num 3 --split-at 3
- id: clang-tidy
name: Run script/clang-tidy for ESP32 IDF 4/5
options: --environment esp32-idf-tidy --split-num 5 --split-at 4
- id: clang-tidy
name: Run script/clang-tidy for ESP32 IDF 5/5
options: --environment esp32-idf-tidy --split-num 5 --split-at 5
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
@@ -1003,14 +983,14 @@ jobs:
- id: clang-tidy - id: clang-tidy
name: Run script/clang-tidy for ESP32 S3 name: Run script/clang-tidy for ESP32 S3
# yamllint disable-line rule:line-length # 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 --grep SOC_I2S_SUPPORTS_TDM options: --environment esp32s3-idf-tidy --grep SOC_TEMP_SENSOR_SUPPORTED --grep USE_ESP32_VARIANT_ESP32S3 --grep USE_LOGGER_USB_CDC
tidy_environment: esp32s3-idf-tidy tidy_environment: esp32s3-idf-tidy
- id: clang-tidy - id: clang-tidy
name: Run script/clang-tidy for ESP32 P4 name: Run script/clang-tidy for ESP32 P4
# P4 has no native Wi-Fi/BLE; those run over the hosted co-processor, # 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. # so their code paths differ -- lint them under the P4 build too.
# yamllint disable-line rule:line-length # yamllint disable-line rule:line-length
options: --environment esp32p4-idf-tidy --grep USE_ESP32_VARIANT_ESP32P4 --grep CONFIG_IDF_TARGET_ESP32P4 --grep USE_ESP32_HOSTED --grep USE_WIFI --grep USE_BLE options: --environment esp32p4-idf-tidy --grep USE_ESP32_VARIANT_ESP32P4 --grep USE_ESP32_HOSTED --grep USE_WIFI --grep USE_BLE
tidy_environment: esp32p4-idf-tidy tidy_environment: esp32p4-idf-tidy
- id: clang-tidy - id: clang-tidy
name: Run script/clang-tidy for ESP32 C6 name: Run script/clang-tidy for ESP32 C6
@@ -1116,8 +1096,8 @@ jobs:
timeout-minutes: 10 timeout-minutes: 10
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3 uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
with: with:
packages: libsdl2-dev libmbedtls-dev ccache packages: libsdl2-dev ccache
version: 1.2 version: 1.1
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -1349,91 +1329,6 @@ jobs:
# config first, so a separate config pass is redundant. # config first, so a separate config pass is redundant.
python3 script/test_build_components.py -e compile -t esp8266-ard -c "$TEST_COMPONENTS" -f --toolchain arduino --fail-on-no-tests python3 script/test_build_components.py -e compile -t esp8266-ard -c "$TEST_COMPONENTS" -f --toolchain arduino --fail-on-no-tests
test-esp8266-native-windows:
name: Compile esp8266 with the native toolchain on Windows (ccache ${{ matrix.ccache }})
runs-on: windows-latest
needs:
- common
- determine-jobs
if: github.event_name == 'pull_request' && needs.determine-jobs.outputs.esp8266-native == 'true'
# A hung compiler must fail the job, not hold the runner for six hours.
timeout-minutes: 40
strategy:
fail-fast: false
matrix:
# Both ways the build runs on Windows: ESPHome Desktop bundles
# ccache and puts it on PATH, a pip install has none.
ccache: ["1", "0"]
env:
ESPHOME_CCACHE_ENABLE: ${{ matrix.ccache }}
# The test script prints characters the cp1252 console cannot encode
PYTHONUTF8: "1"
# The exact static build ESPHome Desktop ships
CCACHE_URL: https://github.com/ccache/ccache/releases/download/v4.14/ccache-4.14-windows-x86_64.zip
CCACHE_SHA256: 2568347a697e103ca1b073981c704ad76fb2507d066c38dba038dd73399d968f
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Install ccache
if: matrix.ccache == '1'
shell: pwsh
run: |
$dir = Join-Path $env:RUNNER_TEMP "ccache"
New-Item -ItemType Directory -Force $dir | Out-Null
$zip = Join-Path $dir "ccache.zip"
Invoke-WebRequest -Uri $env:CCACHE_URL -OutFile $zip
$hash = (Get-FileHash $zip -Algorithm SHA256).Hash.ToLower()
if ($hash -ne $env:CCACHE_SHA256) {
Write-Host "::error::ccache archive sha256 $hash does not match $env:CCACHE_SHA256"
exit 1
}
Expand-Archive -Path $zip -DestinationPath $dir
$bin = Get-ChildItem -Path $dir -Recurse -Filter ccache.exe | Select-Object -First 1
Add-Content -Path $env:GITHUB_PATH -Value $bin.DirectoryName
& $bin.FullName --version
- name: Compile
shell: pwsh
# One component test: web_server brings wifi, api and the async web
# server libraries, the shape of a typical ESP8266 device
run: |
. .\venv\Scripts\Activate.ps1
$arguments = "script/test_build_components.py", "-e", "compile", "-t", "esp8266-ard", "-c", "web_server", "--base-only", "--toolchain", "arduino", "--fail-on-no-tests"
$p = Start-Process -FilePath python -ArgumentList $arguments -NoNewWindow -PassThru
# Reading the handle keeps ExitCode available after the process exits
$null = $p.Handle
if (-not $p.WaitForExit(25 * 60 * 1000)) {
Write-Host "::error::The compile did not finish in 25 minutes; processes still running:"
Get-CimInstance Win32_Process |
Where-Object { $_.CommandLine -match 'xtensa|ccache|ninja|esphome' } |
Select-Object ProcessId, ParentProcessId, CreationDate, CommandLine |
Format-List | Out-String -Width 4096 | Write-Host
Get-Process |
Where-Object { $_.Name -match 'cc1|ccache|ninja|xtensa|python' } |
Select-Object Id, Name, CPU, WorkingSet64 |
Format-Table | Out-String | Write-Host
taskkill /PID $p.Id /T /F
exit 1
}
exit $p.ExitCode
- name: Show ccache statistics
# Also after a failed or killed compile: the counters say whether
# ccache was ever reached
if: always() && matrix.ccache == '1'
shell: pwsh
# The build keeps its store under the esphome tools cache
run: |
$env:CCACHE_DIR = Join-Path $env:LOCALAPPDATA "esphome\Cache\arduino8266\ccache"
ccache -s
device-builder: device-builder:
name: Test downstream esphome/device-builder name: Test downstream esphome/device-builder
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
@@ -1462,7 +1357,7 @@ 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@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with: with:
enable-cache: true enable-cache: true
# Pull request saves land in per-PR scopes nothing else can # Pull request saves land in per-PR scopes nothing else can
@@ -1521,27 +1416,31 @@ jobs:
echo "::warning::ci_memory_impact_extract.py not found on target branch, skipping memory impact analysis" echo "::warning::ci_memory_impact_extract.py not found on target branch, skipping memory impact analysis"
fi fi
# The build below uses --base-only, so skip unless the target branch has # Check if test files exist on the target branch for the requested
# test.<platform>.yaml for every requested component (test-*.yaml # components and platform. When a PR adds new test files for a platform,
# variants alone compile nothing, and a partial set would compare a # the target branch won't have them yet, so skip instead of failing.
# smaller target build against the PR build). Checked here, not in # This check must be done here (not in determine-jobs.py) because
# determine-jobs.py, because determine-jobs runs on the PR branch and # determine-jobs runs on the PR branch and cannot see what the target
# cannot see the target branch. # branch has.
- name: Check for test files on target branch - name: Check for test files on target branch
id: check-tests id: check-tests
if: steps.check-script.outputs.skip != 'true' if: steps.check-script.outputs.skip != 'true'
run: | run: |
components='${{ toJSON(fromJSON(needs.determine-jobs.outputs.memory_impact).components) }}' components='${{ toJSON(fromJSON(needs.determine-jobs.outputs.memory_impact).components) }}'
platform="${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}" platform="${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}"
missing="" found=false
for component in $(echo "$components" | jq -r '.[]'); do for component in $(echo "$components" | jq -r '.[]'); do
if [ ! -f "tests/components/${component}/test.${platform}.yaml" ]; then # Check for test files matching the platform (test.platform.yaml or test-*.platform.yaml)
missing="${missing} ${component}" for f in tests/components/${component}/test*.${platform}.yaml; do
fi if [ -f "$f" ]; then
found=true
break 2
fi
done
done done
if [ -n "$missing" ]; then if [ "$found" = false ]; then
echo "skip=true" >> $GITHUB_OUTPUT echo "skip=true" >> $GITHUB_OUTPUT
echo "::warning::No test.${platform}.yaml on target branch for${missing}, skipping memory impact analysis" echo "::warning::No test files found on target branch for platform ${platform}, skipping memory impact analysis"
else else
echo "skip=false" >> $GITHUB_OUTPUT echo "skip=false" >> $GITHUB_OUTPUT
fi fi
@@ -1661,7 +1560,7 @@ jobs:
fi fi
- name: Upload memory analysis JSON - name: Upload memory analysis JSON
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: memory-analysis-target name: memory-analysis-target
path: memory-analysis-target.json path: memory-analysis-target.json
@@ -1724,7 +1623,7 @@ jobs:
--platform "$platform" --platform "$platform"
- name: Upload memory analysis JSON - name: Upload memory analysis JSON
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: memory-analysis-pr name: memory-analysis-pr
path: memory-analysis-pr.json path: memory-analysis-pr.json
@@ -1754,13 +1653,13 @@ 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: Download target analysis JSON - name: Download target analysis JSON
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with: with:
name: memory-analysis-target name: memory-analysis-target
path: ./memory-analysis path: ./memory-analysis
continue-on-error: true continue-on-error: true
- name: Download PR analysis JSON - name: Download PR analysis JSON
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with: with:
name: memory-analysis-pr name: memory-analysis-pr
path: ./memory-analysis path: ./memory-analysis
@@ -1804,7 +1703,6 @@ jobs:
- test-build-components-split - test-build-components-split
- test-esp32-platformio - test-esp32-platformio
- test-esp8266-native - test-esp8266-native
- test-esp8266-native-windows
- device-builder - device-builder
- memory-impact-target-branch - memory-impact-target-branch
- memory-impact-pr-branch - memory-impact-pr-branch
+2 -2
View File
@@ -56,7 +56,7 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3 uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
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@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3 uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
with: with:
category: "/language:${{matrix.language}}" category: "/language:${{matrix.language}}"
+2 -2
View File
@@ -162,7 +162,7 @@ jobs:
# version: ${{ needs.init.outputs.tag }} # version: ${{ needs.init.outputs.tag }}
- name: Upload digests - name: Upload digests
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: digests-${{ matrix.platform.arch }} name: digests-${{ matrix.platform.arch }}
path: /tmp/digests path: /tmp/digests
@@ -195,7 +195,7 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Download digests - name: Download digests
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with: with:
pattern: digests-* pattern: digests-*
path: /tmp/digests path: /tmp/digests
+1 -15
View File
@@ -16,18 +16,16 @@ jobs:
# No GITHUB_TOKEN permissions: the reusable workflow mints an ESPHome # No GITHUB_TOKEN permissions: the reusable workflow mints an ESPHome
# GitHub App token so the labels, comments and closures come from # GitHub App token so the labels, comments and closures come from
# esphome[bot] instead of github-actions[bot]. # esphome[bot] instead of github-actions[bot].
uses: esphome/workflows/.github/workflows/stale.yml@4fbce13026c393d6eb6b247e394dcd9ef1af76c2 # main uses: esphome/workflows/.github/workflows/stale.yml@cc3e76de337dc59bc1cba8da58d963cd23b873f1 # main
secrets: secrets:
ESPHOME_GITHUB_APP_PRIVATE_KEY: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} ESPHOME_GITHUB_APP_PRIVATE_KEY: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
with: with:
# Live only on dev: a workflow_dispatch from any other branch is a dry run # Live only on dev: a workflow_dispatch from any other branch is a dry run
dry-run: ${{ github.ref != 'refs/heads/dev' }} dry-run: ${{ github.ref != 'refs/heads/dev' }}
days-before-stale: 90 days-before-stale: 90
days-before-stale-conflicting: 30
days-before-close: 7 days-before-close: 7
stale-label: stale stale-label: stale
exempt-label: not-stale exempt-label: not-stale
conflict-label: needs-rebase
ignored-users: esphbot,codecov-commenter ignored-users: esphbot,codecov-commenter
stale-pr-message: > stale-pr-message: >
There hasn't been any activity on this pull request recently. This There hasn't been any activity on this pull request recently. This
@@ -38,18 +36,6 @@ jobs:
to keep it open. Also, please merge the latest dev branch into your to keep it open. Also, please merge the latest dev branch into your
branch to ensure that it's up to date with the latest changes. branch to ensure that it's up to date with the latest changes.
Thank you for your contribution!
conflict-pr-message: >
This pull request has merge conflicts. Please merge the latest dev
branch into your branch to resolve them.
stale-conflicting-pr-message: >
The merge conflicts on this pull request have not been resolved for
30 days, so it has been marked as stale and will be closed if they
are not resolved within 7 days.
Please merge the latest dev branch into your branch. Once the pull
request is mergeable again the stale label is removed.
Thank you for your contribution! Thank you for your contribution!
stale-issue-message: > stale-issue-message: >
There hasn't been any activity on this issue recently. Due to the There hasn't been any activity on this issue recently. Due to the
+1 -1
View File
@@ -47,7 +47,7 @@ jobs:
# setup-python interpreter so subsequent ``prek`` / # setup-python interpreter so subsequent ``prek`` /
# ``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@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.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
+2 -2
View File
@@ -10,7 +10,7 @@ ci:
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: v0.17.0 rev: v0.16.10
hooks: hooks:
# Run the linter. # Run the linter.
- id: ruff - id: ruff
@@ -36,7 +36,7 @@ repos:
- id: end-of-file-fixer - id: end-of-file-fixer
- id: trailing-whitespace - id: trailing-whitespace
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v3.22.0 rev: v3.21.2
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: [--py312-plus] args: [--py312-plus]
-84
View File
@@ -67,7 +67,6 @@ file does, and it is the authority when they disagree. The most useful starting
- Function-local constants: `lower_snake_case` - Function-local constants: `lower_snake_case`
- 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
- Device components are named brand plus model (`siemens_ira211`), without a protocol-family prefix
- Enumerator names: prefix every value of an `enum class` with the enum name converted to - Enumerator names: prefix every value of an `enum class` with the enum name converted to
`UPPER_SNAKE_CASE` (e.g. `UARTFlushResult::UART_FLUSH_RESULT_SUCCESS`). Never use bare `UPPER_SNAKE_CASE` (e.g. `UARTFlushResult::UART_FLUSH_RESULT_SUCCESS`). Never use bare
names like `SUCCESS`, `FAILURE`, `OK`, or `FAIL`: platform SDK headers define macros with names like `SUCCESS`, `FAILURE`, `OK`, or `FAIL`: platform SDK headers define macros with
@@ -144,9 +143,6 @@ file does, and it is the authority when they disagree. The most useful starting
* **C++ Preprocessor Directives:** * **C++ Preprocessor Directives:**
* **Avoid `#define` for constants:** Using `#define` for constants is discouraged and should be replaced with `const` variables or enums. * **Avoid `#define` for constants:** Using `#define` for constants is discouraged and should be replaced with `const` variables or enums.
* **A constant with a compile-time initializer is `static constexpr`,** not `static const`, at namespace scope
and inside functions; `static const` is only for a value computed at run time. A function-local
`static constexpr` table is a static constant to clang-tidy, so it is `UPPER_SNAKE_CASE` too.
* **Use `#define` only for:** * **Use `#define` only for:**
- Conditional compilation (`#ifdef`, `#ifndef`) - Conditional compilation (`#ifdef`, `#ifndef`)
- Compile-time sizes calculated during Python code generation (e.g., configuring `std::array` or `StaticVector` dimensions via `cg.add_define()`) - Compile-time sizes calculated during Python code generation (e.g., configuring `std::array` or `StaticVector` dimensions via `cg.add_define()`)
@@ -181,15 +177,6 @@ file does, and it is the authority when they disagree. The most useful starting
Pick the primitive by cadence: under 250 ms use a gated `loop()`; 500 ms and above use Pick the primitive by cadence: under 250 ms use a gated `loop()`; 500 ms and above use
`set_interval`. Full reasoning, including why `set_interval` costs more below 500 ms: `set_interval`. Full reasoning, including why `set_interval` costs more below 500 ms:
https://developers.esphome.io/architecture/components/advanced/#quick-rule-of-thumb https://developers.esphome.io/architecture/components/advanced/#quick-rule-of-thumb
* **Repeating work:** use `set_interval`, never a `set_timeout` that re-arms itself; every re-arm
rebuilds the callback and re-queues a scheduler item, and the period drifts.
* **Scheduler ids:** name a timer only when it must be cancelled or replaced, and use a
`static constexpr uint32_t` id, never a string. Ids are per component instance and cannot clash with other
components, so number them from 0 and keep all of a component's ids together in one place.
```cpp
static constexpr uint32_t READ_TIMEOUT_ID = 0;
this->set_timeout(READ_TIMEOUT_ID, 50, [this]() { this->read_(); });
```
* **Don't override a default with the same value:** if a base class method already returns what you * **Don't override a default with the same value:** if a base class method already returns what you
want, do not override it. `Component::get_setup_priority()` returns `setup_priority::DATA`, so a want, do not override it. `Component::get_setup_priority()` returns `setup_priority::DATA`, so a
component that wants `DATA` should simply leave it alone. component that wants `DATA` should simply leave it alone.
@@ -197,36 +184,6 @@ file does, and it is the authority when they disagree. The most useful starting
// Bad - this is exactly what the base class already does // Bad - this is exactly what the base class already does
float get_setup_priority() const override { return setup_priority::DATA; } float get_setup_priority() const override { return setup_priority::DATA; }
``` ```
* **Constant data lives in RAM on ESP8266** unless marked `PROGMEM`: `const` tables, class-scope
constant arrays, and in-class `std::array` initializers (a `.rodata` template plus a copy). Mark the
table `PROGMEM` and read it with `progmem_memcpy()` or `progmem_read_byte()` (`esphome/core/hal.h`);
for a table of structs copy the whole entry out and return the copy, never a pointer into flash.
On other platforms they are a plain `memcpy` and dereference, so no `#ifdef` is needed. Declare log tags with
`ESPHOME_LOG_TAG(TAG, "name")`, pass status text as `LOG_STR`, e.g.
`this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL))`, and give an action whose payload is a byte array
a `TEMPLATABLE_BYTES` field so a static code stays in flash.
* **Fixed sets of names** (status words, model names): a `PROGMEM_STRING_TABLE` (`esphome/core/progmem.h`),
logged with `get_log_str` and `LOG_STR_ARG`, copied out with `ESPHOME_strncpy_P` before `publish_state`.
A `dump_config` helper that picks a name returns `const LogString *` from `LOG_STR(...)`.
* **CRCs and bit tricks** come from `esphome/core/helpers.h` (`crc8`, `crc16`, `reverse_bits`, `encode_uint16`);
pick the helper's polynomial and bit-order arguments instead of writing a CRC loop.
* **An unset float is `NAN`**, not `optional<float>`; the optional costs 4 to 8 bytes of padding per member.
* **`PollingComponent` first runs `update()`** after a random share of the interval; call `this->update()` at the
end of `setup()` when the entities need a state at boot.
* **A multi-step command sequence** leaves the step pending when the answer is malformed, so the existing
resend and give-up logic handles it; never advance on an answer that was only logged.
* **Entities that each watch a register field:** one class taking register, shift, mask and match from a Python
tuple table, in typed `StaticVector`s sized by `cg.slot_counter`; not a template per bit, not a virtual
listener base (a second vptr per entity).
* **A global feature define** says the code is compiled in, not that this instance uses it: keep the
per-instance check (`if (this->aux_device_ == ...)`) inside the `#ifdef`.
* **Model-specific code** sits behind a define the Python side emits only for configs that use that model
(`cg.add_define("USE_SELEC_METER_EM4M")`), so the other models' builds leave it out; the runtime model check
stays inside the `#ifdef`.
* **Failures after `setup()`:** `mark_failed()` is for `setup()` only. A runtime read or write failure
sets `status_set_warning()` and the next success clears it; writes skip work when `is_failed()`.
* **Fixed platform facts compile to nothing:** gate on `#ifdef USE_ESP8266` and friends, not on a
runtime check of something the build already knows.
* **Logging string literals:** wrap literals passed as `%s` arguments in `LOG_STR_LITERAL()` so they * **Logging string literals:** wrap literals passed as `%s` arguments in `LOG_STR_LITERAL()` so they
can be stored in flash rather than RAM. can be stored in flash rather than RAM.
```cpp ```cpp
@@ -273,10 +230,6 @@ file does, and it is the authority when they disagree. The most useful starting
``` ```
* **Component Metadata:** * **Component Metadata:**
- `DOMAIN`: Required in every component's `__init__.py`, set to the component's own name as a plain
string (e.g. `DOMAIN = "my_component"`). Other code refers to the component through it, e.g.
`from esphome.components.my_component import DOMAIN`. Keep it in alphabetical order with the other
metadata constants. CI (`lint_component_domain`) fails without it.
- `DEPENDENCIES`: List of required components - `DEPENDENCIES`: List of required components
- `AUTO_LOAD`: Components to automatically load - `AUTO_LOAD`: Components to automatically load
- `CONFLICTS_WITH`: Incompatible components - `CONFLICTS_WITH`: Incompatible components
@@ -290,7 +243,6 @@ file does, and it is the authority when they disagree. The most useful starting
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_KEY, CONF_ID from esphome.const import CONF_KEY, CONF_ID
DOMAIN = "my_component"
CONF_PARAM = "param" # A constant that does not yet exist in esphome/const.py CONF_PARAM = "param" # A constant that does not yet exist in esphome/const.py
my_component_ns = cg.esphome_ns.namespace("my_component") my_component_ns = cg.esphome_ns.namespace("my_component")
@@ -432,25 +384,6 @@ file does, and it is the authority when they disagree. The most useful starting
) )
``` ```
When the callback's parameters differ from the automation's arguments, or the trigger should only
fire for some values, pass `params`, `forward` and `when`; the helper then generates a capture-less
lambda (stored inline, nothing allocated). Name the parent with `automation.parent_ref(var)` and build
`forward` from `MockObj` calls, not f-strings of C++:
```python
parent = automation.parent_ref(var)
index = cg.RawExpression("index")
await automation.build_callback_automation(
var,
"add_on_state_callback",
[(cg.StringRef, "x"), (cg.size_t, "i")],
conf,
params=[(cg.size_t, "index")],
forward=[cg.StringRef(parent.option_at(index)), index],
)
```
`build_parent_callback_automation`, `build_trigger_callback`, `build_callback_automations` and
`build_trigger_automations` cover the other shapes; see their docstrings in `esphome/automation.py`.
**C++ -- no trigger class needed.** The callback registration method must be templatized to accept both `std::function` and lightweight forwarder structs (which avoid heap allocation): **C++ -- no trigger class needed.** The callback registration method must be templatized to accept both `std::function` and lightweight forwarder structs (which avoid heap allocation):
```cpp ```cpp
class MyComponent : public Component { class MyComponent : public Component {
@@ -570,12 +503,8 @@ file does, and it is the authority when they disagree. The most useful starting
`cover.is_open`, `rtttl.is_playing` and `component.is_idle` are in-tree examples. `cover.is_open`, `rtttl.is_playing` and `component.is_idle` are in-tree examples.
* **Type Hints:** Type-hint all function signatures, including test functions and config validators (e.g. `def validate_x(config: ConfigType) -> ConfigType:`, `def test_x() -> None:`). Import `ConfigType` from `esphome.types`. * **Type Hints:** Type-hint all function signatures, including test functions and config validators (e.g. `def validate_x(config: ConfigType) -> ConfigType:`, `def test_x() -> None:`). Import `ConfigType` from `esphome.types`.
An intentionally empty body (an unused `register_dumper` builder, for example) gets a one-line docstring, not `pass`.
* **Configuration Validation:** * **Configuration Validation:**
* **One schema per model:** when a component's options depend on a `model` key, use `cv.typed_schema({...},
key=CONF_MODEL, default_type=...)` with each model's own keys and defaults, not one schema plus a validator
that rejects keys and fills defaults afterwards.
* **Reuse existing validators:** Before writing a custom validator, check for an existing one in `config_validation.py` and compose it in `cv.All(...)` rather than duplicating logic across components. For example, rename a config key with `cv.rename_key(CONF_OLD, CONF_NEW, removed_in="2026.6.0")`, and reject mutually-exclusive keys with `cv.has_at_most_one_key(...)` / `cv.has_exactly_one_key(...)`. See how `api` composes `cv.has_exactly_one_key` + `cv.rename_key`. * **Reuse existing validators:** Before writing a custom validator, check for an existing one in `config_validation.py` and compose it in `cv.All(...)` rather than duplicating logic across components. For example, rename a config key with `cv.rename_key(CONF_OLD, CONF_NEW, removed_in="2026.6.0")`, and reject mutually-exclusive keys with `cv.has_at_most_one_key(...)` / `cv.has_exactly_one_key(...)`. See how `api` composes `cv.has_exactly_one_key` + `cv.rename_key`.
* **Common Validators:** `cv.int_`, `cv.float_`, `cv.string`, `cv.boolean`, `cv.int_range(min=0, max=100)`, `cv.positive_int`, `cv.percentage`. * **Common Validators:** `cv.int_`, `cv.float_`, `cv.string`, `cv.boolean`, `cv.int_range(min=0, max=100)`, `cv.positive_int`, `cv.percentage`.
* **Complex Validation:** `cv.All(cv.string, cv.Length(min=1, max=50))`, `cv.Any(cv.int_, cv.string)`. * **Complex Validation:** `cv.All(cv.string, cv.Length(min=1, max=50))`, `cv.Any(cv.int_, cv.string)`.
@@ -609,18 +538,6 @@ file does, and it is the authority when they disagree. The most useful starting
* **Testing:** * **Testing:**
* **Python:** Run unit tests with `pytest`. * **Python:** Run unit tests with `pytest`.
* **C++:** Use `clang-tidy` for static analysis. * **C++:** Use `clang-tidy` for static analysis.
* **C++ unit tests:** gtests in `tests/components/<component>/*.cpp`, run with
`script/cpp_unit_test.py <component>`; see `tests/components/README.md`. Set the loop clock in a test
with `LoopBlockingGuard{nullptr, nullptr, now_ms}`; the runner resets it before every test.
* **Integration tests:** `tests/integration/` runs real firmware on the host. Serial components prove
timeouts and recovery with the `uart_mock` fixture (`tests/integration/test_uart_mock_*.py`). Its
`responses` match on content every time they see it; to answer a repeated request differently, use an
`on_tx` lambda with a static counter and `inject_to_rx_buffer` (see `uart_mock_ld2412_requery_after_giveup.yaml`).
* **Keep build-test variants few:** one `test.<platform>.yaml` per platform the code actually differs
on. A variant per board for platform-identical code only adds CI time.
* **A test that needs a shared component in a particular state** (`esp32_ble` security settings, an option
that PSRAM rejects) cannot be grouped: the merged block wins and the whole batch fails. Add the component to
`ISOLATED_COMPONENTS` in `script/analyze_component_buses.py`, or move the option to a `test-<variant>` file.
* **Component Tests:** YAML-based compilation tests are located in `tests/`. The structure is as follows: * **Component Tests:** YAML-based compilation tests are located in `tests/`. The structure is as follows:
``` ```
tests/ tests/
@@ -667,7 +584,6 @@ file does, and it is the authority when they disagree. The most useful starting
name: My Sensor name: My Sensor
``` ```
Components that define buses directly are flagged as "NEEDS MIGRATION" and cannot be grouped, increasing CI build time. Components that define buses directly are flagged as "NEEDS MIGRATION" and cannot be grouped, increasing CI build time.
`!extend` and `!remove` in a `test.*.yaml` also make the component ungroupable; add a standalone entity with its own id instead.
* **Testing All Components Together:** To verify that all components can be tested together without ID conflicts or configuration issues, use: * **Testing All Components Together:** To verify that all components can be tested together without ID conflicts or configuration issues, use:
```bash ```bash
+3 -53
View File
@@ -39,12 +39,9 @@ esphome/components/am43/sensor/* @buxtronix
esphome/components/analog_threshold/* @ianchi esphome/components/analog_threshold/* @ianchi
esphome/components/animation/* @syndlex esphome/components/animation/* @syndlex
esphome/components/anova/* @buxtronix esphome/components/anova/* @buxtronix
esphome/components/apc1/* @ademuri
esphome/components/apds9306/* @aodrenah esphome/components/apds9306/* @aodrenah
esphome/components/apds9930/* @iyesin
esphome/components/api/* @esphome/core esphome/components/api/* @esphome/core
esphome/components/aqi/* @freekode @jasstrong @ximex esphome/components/aqi/* @freekode @jasstrong @ximex
esphome/components/aranet/* @patrickod
esphome/components/as5600/* @ammmze esphome/components/as5600/* @ammmze
esphome/components/as5600/sensor/* @ammmze esphome/components/as5600/sensor/* @ammmze
esphome/components/as7341/* @mrgnr esphome/components/as7341/* @mrgnr
@@ -68,7 +65,6 @@ esphome/components/bedjet/climate/* @jhansche
esphome/components/bedjet/fan/* @jhansche esphome/components/bedjet/fan/* @jhansche
esphome/components/bedjet/sensor/* @javawizard @jhansche esphome/components/bedjet/sensor/* @javawizard @jhansche
esphome/components/beken_spi_led_strip/* @Mat931 esphome/components/beken_spi_led_strip/* @Mat931
esphome/components/bh1745/* @latonita
esphome/components/bh1750/* @OttoWinter esphome/components/bh1750/* @OttoWinter
esphome/components/bh1900nux/* @B48D81EFCC esphome/components/bh1900nux/* @B48D81EFCC
esphome/components/binary_sensor/* @esphome/core esphome/components/binary_sensor/* @esphome/core
@@ -104,9 +100,6 @@ esphome/components/bmp581_i2c/* @danielkent-net @kahrendt
esphome/components/bmp581_spi/* @danielkent-net @kahrendt esphome/components/bmp581_spi/* @danielkent-net @kahrendt
esphome/components/bp1658cj/* @Cossid esphome/components/bp1658cj/* @Cossid
esphome/components/bp5758d/* @Cossid esphome/components/bp5758d/* @Cossid
esphome/components/bq25186/* @Rapsssito
esphome/components/bq27220/* @rggammon
esphome/components/bq76972/* @limpkin
esphome/components/bridge/* @kbx81 esphome/components/bridge/* @kbx81
esphome/components/bthome_mithermometer/* @nagyrobi esphome/components/bthome_mithermometer/* @nagyrobi
esphome/components/button/* @esphome/core esphome/components/button/* @esphome/core
@@ -163,10 +156,8 @@ esphome/components/ds1307/* @badbadc0ffee
esphome/components/ds1603l/* @JakeLC15 esphome/components/ds1603l/* @JakeLC15
esphome/components/ds2484/* @mrk-its esphome/components/ds2484/* @mrk-its
esphome/components/ds248x/* @tomwellnitz esphome/components/ds248x/* @tomwellnitz
esphome/components/ds3231/* @linkedupbits
esphome/components/dsmr/* @glmnet @PolarGoose esphome/components/dsmr/* @glmnet @PolarGoose
esphome/components/duty_time/* @dudanov esphome/components/duty_time/* @dudanov
esphome/components/ecocomfort2/* @gledian
esphome/components/ee895/* @Stock-M esphome/components/ee895/* @Stock-M
esphome/components/ektf2232/touchscreen/* @jesserockz esphome/components/ektf2232/touchscreen/* @jesserockz
esphome/components/emc2101/* @ellull esphome/components/emc2101/* @ellull
@@ -183,7 +174,6 @@ esphome/components/es7243e/* @kbx81
esphome/components/es8156/* @kbx81 esphome/components/es8156/* @kbx81
esphome/components/es8311/* @kahrendt @kroimon esphome/components/es8311/* @kahrendt @kroimon
esphome/components/es8388/* @P4uLT esphome/components/es8388/* @P4uLT
esphome/components/es9038q2m_katana/* @GioMez
esphome/components/esp32/* @esphome/core esphome/components/esp32/* @esphome/core
esphome/components/esp32_ble/* @bdraco @jesserockz @Rapsssito esphome/components/esp32_ble/* @bdraco @jesserockz @Rapsssito
esphome/components/esp32_ble_client/* @bdraco @jesserockz esphome/components/esp32_ble_client/* @bdraco @jesserockz
@@ -193,13 +183,11 @@ esphome/components/esp32_camera_web_server/* @ayufan
esphome/components/esp32_can/* @Sympatron esphome/components/esp32_can/* @Sympatron
esphome/components/esp32_hosted/* @swoboda1337 esphome/components/esp32_hosted/* @swoboda1337
esphome/components/esp32_hosted/update/* @swoboda1337 esphome/components/esp32_hosted/update/* @swoboda1337
esphome/components/esp32_power_management/* @rwrozelle
esphome/components/esp32_rmt/* @jesserockz esphome/components/esp32_rmt/* @jesserockz
esphome/components/esp32_rmt_led_strip/* @jesserockz esphome/components/esp32_rmt_led_strip/* @jesserockz
esphome/components/esp8266/* @esphome/core esphome/components/esp8266/* @esphome/core
esphome/components/esp_ldo/* @clydebarrow esphome/components/esp_ldo/* @clydebarrow
esphome/components/espectre/* @francescopace esphome/components/espectre/* @francescopace
esphome/components/esphome/ota/* @esphome/core
esphome/components/espnow/* @jesserockz esphome/components/espnow/* @jesserockz
esphome/components/espnow/packet_transport/* @EasilyBoredEngineer esphome/components/espnow/packet_transport/* @EasilyBoredEngineer
esphome/components/ethernet_info/* @gtjadsonsantos esphome/components/ethernet_info/* @gtjadsonsantos
@@ -208,19 +196,15 @@ esphome/components/exponential_moving_average/* @clydebarrow
esphome/components/exposure_notifications/* @OttoWinter esphome/components/exposure_notifications/* @OttoWinter
esphome/components/ezo/* @ssieb esphome/components/ezo/* @ssieb
esphome/components/ezo_pmp/* @carlos-sarmiento esphome/components/ezo_pmp/* @carlos-sarmiento
esphome/components/ezsp_proxy_tap/* @kbx81 @puddly
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/file/* @esphome/core
esphome/components/filter_lifetime/* @tuct
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/friedrich/* @rwrozelle
esphome/components/fs3000/* @kahrendt esphome/components/fs3000/* @kahrendt
esphome/components/ft5x06/* @clydebarrow esphome/components/ft5x06/* @clydebarrow
esphome/components/ft63x6/* @gpambrozio esphome/components/ft63x6/* @gpambrozio
esphome/components/fusb302b/* @remcom
esphome/components/gcja5/* @gcormier esphome/components/gcja5/* @gcormier
esphome/components/gdk101/* @Szewcson esphome/components/gdk101/* @Szewcson
esphome/components/gl_r01_i2c/* @pkejval esphome/components/gl_r01_i2c/* @pkejval
@@ -242,13 +226,9 @@ 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
esphome/components/haier/button/* @paveldn esphome/components/haier/button/* @paveldn
esphome/components/haier/select/* @paveldn
esphome/components/haier/sensor/* @paveldn esphome/components/haier/sensor/* @paveldn
esphome/components/haier/switch/* @paveldn esphome/components/haier/switch/* @paveldn
esphome/components/haier/text_sensor/* @paveldn esphome/components/haier/text_sensor/* @paveldn
esphome/components/haier_tundra/* @fauxpark
esphome/components/haier_tundra/button/* @fauxpark
esphome/components/haier_tundra/switch/* @fauxpark
esphome/components/havells_solar/* @sourabhjaiswal esphome/components/havells_solar/* @sourabhjaiswal
esphome/components/hbridge/fan/* @WeekendWarrior esphome/components/hbridge/fan/* @WeekendWarrior
esphome/components/hbridge/light/* @DotNetDann esphome/components/hbridge/light/* @DotNetDann
@@ -257,7 +237,6 @@ esphome/components/hc8/* @omartijn
esphome/components/hdc2010/* @optimusprimespace @ssieb esphome/components/hdc2010/* @optimusprimespace @ssieb
esphome/components/hdc2080/* @G-Pereira @jesserockz esphome/components/hdc2080/* @G-Pereira @jesserockz
esphome/components/hdc302x/* @joshuasing esphome/components/hdc302x/* @joshuasing
esphome/components/hdmi_cec/* @JosVanEijndhoven
esphome/components/he60r/* @clydebarrow esphome/components/he60r/* @clydebarrow
esphome/components/heatpumpir/* @rob-deutsch esphome/components/heatpumpir/* @rob-deutsch
esphome/components/hitachi_ac424/* @sourabhjaiswal esphome/components/hitachi_ac424/* @sourabhjaiswal
@@ -284,7 +263,6 @@ esphome/components/http_request/ota/* @oarcher
esphome/components/http_request/update/* @jesserockz esphome/components/http_request/update/* @jesserockz
esphome/components/htu31d/* @betterengineering esphome/components/htu31d/* @betterengineering
esphome/components/hub75/* @stuartparmenter esphome/components/hub75/* @stuartparmenter
esphome/components/husb238/* @latonita
esphome/components/hydreon_rgxx/* @functionpointer esphome/components/hydreon_rgxx/* @functionpointer
esphome/components/hyt271/* @Philippe12 esphome/components/hyt271/* @Philippe12
esphome/components/i2c/* @esphome/core esphome/components/i2c/* @esphome/core
@@ -292,9 +270,7 @@ esphome/components/i2c_device/* @gabest11
esphome/components/i2s_audio/* @jesserockz esphome/components/i2s_audio/* @jesserockz
esphome/components/i2s_audio/microphone/* @jesserockz esphome/components/i2s_audio/microphone/* @jesserockz
esphome/components/i2s_audio/speaker/* @jesserockz @kahrendt esphome/components/i2s_audio/speaker/* @jesserockz @kahrendt
esphome/components/i2s_clockless_led_strip/* @j9brown
esphome/components/iaqcore/* @yozik04 esphome/components/iaqcore/* @yozik04
esphome/components/icm20689/* @lboue
esphome/components/icnt86/* @danepowell esphome/components/icnt86/* @danepowell
esphome/components/ili9xxx/* @clydebarrow @nielsnl68 esphome/components/ili9xxx/* @clydebarrow @nielsnl68
esphome/components/improv_base/* @esphome/core esphome/components/improv_base/* @esphome/core
@@ -323,11 +299,9 @@ esphome/components/kuntze/* @ssieb
esphome/components/lc709203f/* @ilikecake esphome/components/lc709203f/* @ilikecake
esphome/components/lcd_menu/* @numo68 esphome/components/lcd_menu/* @numo68
esphome/components/ld2410/* @regevbr @sebcaps esphome/components/ld2410/* @regevbr @sebcaps
esphome/components/ld2410s/* @NovakIrs
esphome/components/ld2412/* @Rihan9 esphome/components/ld2412/* @Rihan9
esphome/components/ld2420/* @descipher esphome/components/ld2420/* @descipher
esphome/components/ld2450/* @hareeshmu esphome/components/ld2450/* @hareeshmu
esphome/components/ld2460/* @jesusvallejo
esphome/components/ld24xx/* @kbx81 esphome/components/ld24xx/* @kbx81
esphome/components/ld6002b/* @hepter esphome/components/ld6002b/* @hepter
esphome/components/ledc/* @OttoWinter esphome/components/ledc/* @OttoWinter
@@ -336,10 +310,6 @@ esphome/components/libretiny_pwm/* @kuba2k2
esphome/components/light/* @esphome/core esphome/components/light/* @esphome/core
esphome/components/lightwaverf/* @max246 esphome/components/lightwaverf/* @max246
esphome/components/lilygo_t5_47/touchscreen/* @jesserockz esphome/components/lilygo_t5_47/touchscreen/* @jesserockz
esphome/components/lis2dw12/* @latonita
esphome/components/lis2dw12_i2c/* @latonita
esphome/components/lis2dw12_spi/* @latonita
esphome/components/lis3dh/* @zebble
esphome/components/lm75b/* @beormund esphome/components/lm75b/* @beormund
esphome/components/ln882h_ble/* @Bl00d-B0b esphome/components/ln882h_ble/* @Bl00d-B0b
esphome/components/ln882h_ble_tracker/* @Bl00d-B0b esphome/components/ln882h_ble_tracker/* @Bl00d-B0b
@@ -347,7 +317,6 @@ esphome/components/ln882x/* @lamauny
esphome/components/lock/* @esphome/core 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/lp5562/* @DennisGaida
esphome/components/lps22/* @nagisa esphome/components/lps22/* @nagisa
esphome/components/lsm6ds/* @clydebarrow esphome/components/lsm6ds/* @clydebarrow
esphome/components/ltr390/* @latonita @sjtrny esphome/components/ltr390/* @latonita @sjtrny
@@ -358,9 +327,7 @@ esphome/components/m5stack_8angle/* @rnauber
esphome/components/mapping/* @clydebarrow esphome/components/mapping/* @clydebarrow
esphome/components/matrix_keypad/* @ssieb esphome/components/matrix_keypad/* @ssieb
esphome/components/max17043/* @blacknell esphome/components/max17043/* @blacknell
esphome/components/max17048/* @B48D81EFCC
esphome/components/max31865/* @DAVe3283 esphome/components/max31865/* @DAVe3283
esphome/components/max31888/* @JiriPrchal
esphome/components/max44009/* @berfenger esphome/components/max44009/* @berfenger
esphome/components/max6956/* @looping40 esphome/components/max6956/* @looping40
esphome/components/max7219digit/* @rspaargaren esphome/components/max7219digit/* @rspaargaren
@@ -379,6 +346,7 @@ esphome/components/mcp4461/* @p1ngb4ck
esphome/components/mcp4728/* @berfenger esphome/components/mcp4728/* @berfenger
esphome/components/mcp47a1/* @jesserockz esphome/components/mcp47a1/* @jesserockz
esphome/components/mcp9600/* @mreditor97 esphome/components/mcp9600/* @mreditor97
esphome/components/mcp9808/* @k7hpn
esphome/components/md5/* @esphome/core esphome/components/md5/* @esphome/core
esphome/components/mdns/* @esphome/core esphome/components/mdns/* @esphome/core
esphome/components/media_player/* @jesserockz esphome/components/media_player/* @jesserockz
@@ -389,11 +357,9 @@ esphome/components/microphone/* @jesserockz @kahrendt
esphome/components/mics_4514/* @jesserockz esphome/components/mics_4514/* @jesserockz
esphome/components/midea/* @dudanov esphome/components/midea/* @dudanov
esphome/components/midea_ir/* @dudanov esphome/components/midea_ir/* @dudanov
esphome/components/midea_portasplit/* @Fexiven
esphome/components/mipi_dsi/* @clydebarrow esphome/components/mipi_dsi/* @clydebarrow
esphome/components/mipi_rgb/* @clydebarrow esphome/components/mipi_rgb/* @clydebarrow
esphome/components/mipi_spi/* @clydebarrow esphome/components/mipi_spi/* @clydebarrow
esphome/components/mistral_ir/* @lerhum
esphome/components/mitsubishi/* @RubyBailey esphome/components/mitsubishi/* @RubyBailey
esphome/components/mitsubishi_cn105/* @crnjan esphome/components/mitsubishi_cn105/* @crnjan
esphome/components/mixer/speaker/* @kahrendt esphome/components/mixer/speaker/* @kahrendt
@@ -432,8 +398,6 @@ esphome/components/noblex/* @AGalfra
esphome/components/noise/* @esphome/core esphome/components/noise/* @esphome/core
esphome/components/npi19/* @bakerkj esphome/components/npi19/* @bakerkj
esphome/components/nrf52/* @tomaszduda23 esphome/components/nrf52/* @tomaszduda23
esphome/components/ns2009/* @tchilov
esphome/components/ns2009/touchscreen/* @tchilov
esphome/components/number/* @esphome/core esphome/components/number/* @esphome/core
esphome/components/one_wire/* @ssieb esphome/components/one_wire/* @ssieb
esphome/components/online_image/* @clydebarrow @guillempages esphome/components/online_image/* @clydebarrow @guillempages
@@ -444,12 +408,10 @@ esphome/components/ota/* @esphome/core
esphome/components/output/* @esphome/core esphome/components/output/* @esphome/core
esphome/components/packet_transport/* @clydebarrow esphome/components/packet_transport/* @clydebarrow
esphome/components/pca6416a/* @Mat931 esphome/components/pca6416a/* @Mat931
esphome/components/pca9505/* @ajfriesen @ssieb
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/pcm5122/* @remcom
esphome/components/pd_pioneer_ir/* @martinsah
esphome/components/pi4ioe5v6408/* @jesserockz esphome/components/pi4ioe5v6408/* @jesserockz
esphome/components/pid/* @OttoWinter esphome/components/pid/* @OttoWinter
esphome/components/pipsolar/* @andreashergert1984 esphome/components/pipsolar/* @andreashergert1984
@@ -468,7 +430,6 @@ esphome/components/pn7160/* @jesserockz @kbx81
esphome/components/pn7160_i2c/* @jesserockz @kbx81 esphome/components/pn7160_i2c/* @jesserockz @kbx81
esphome/components/pn7160_spi/* @jesserockz @kbx81 esphome/components/pn7160_spi/* @jesserockz @kbx81
esphome/components/pn71xx/* @jesserockz @kbx81 esphome/components/pn71xx/* @jesserockz @kbx81
esphome/components/power_management/* @rwrozelle
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/provisioning/* @esphome/core
@@ -489,8 +450,6 @@ esphome/components/rc522/* @glmnet
esphome/components/rc522_i2c/* @glmnet esphome/components/rc522_i2c/* @glmnet
esphome/components/rc522_spi/* @glmnet esphome/components/rc522_spi/* @glmnet
esphome/components/rd03d/* @jasstrong esphome/components/rd03d/* @jasstrong
esphome/components/remko_ar715/* @naKruul
esphome/components/renogy_inverter_ble/* @emilioaray-dev
esphome/components/resampler/speaker/* @kahrendt esphome/components/resampler/speaker/* @kahrendt
esphome/components/restart/* @esphome/core esphome/components/restart/* @esphome/core
esphome/components/rf_bridge/* @jesserockz esphome/components/rf_bridge/* @jesserockz
@@ -510,7 +469,6 @@ esphome/components/runtime_stats/* @bdraco
esphome/components/rx8025t/* @remcom esphome/components/rx8025t/* @remcom
esphome/components/rx8130/* @beormund esphome/components/rx8130/* @beormund
esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti
esphome/components/samsung/* @jorofi
esphome/components/scd4x/* @martgras @sjtrny esphome/components/scd4x/* @martgras @sjtrny
esphome/components/script/* @esphome/core esphome/components/script/* @esphome/core
esphome/components/sdl/* @bdm310 @clydebarrow esphome/components/sdl/* @bdm310 @clydebarrow
@@ -539,14 +497,11 @@ esphome/components/sfa30/* @ghsensdev
esphome/components/sfa40/* @NoQuarrel esphome/components/sfa40/* @NoQuarrel
esphome/components/sgp40/* @SenexCrenshaw esphome/components/sgp40/* @SenexCrenshaw
esphome/components/sgp4x/* @martgras @SenexCrenshaw esphome/components/sgp4x/* @martgras @SenexCrenshaw
esphome/components/sh1122_base/* @pl4nkton
esphome/components/sh1122_spi/* @pl4nkton
esphome/components/sha256/* @esphome/core esphome/components/sha256/* @esphome/core
esphome/components/shelly_dimmer/* @edge90 @rnauber esphome/components/shelly_dimmer/* @edge90 @rnauber
esphome/components/sht3xd/* @mrtoy-me esphome/components/sht3xd/* @mrtoy-me
esphome/components/sht4x/* @sjtrny esphome/components/sht4x/* @sjtrny
esphome/components/shutdown/* @esphome/core @jsuanet esphome/components/shutdown/* @esphome/core @jsuanet
esphome/components/siemens_ira211/* @fonix232
esphome/components/sigma_delta_output/* @Cat-Ion esphome/components/sigma_delta_output/* @Cat-Ion
esphome/components/sim800l/* @glmnet esphome/components/sim800l/* @glmnet
esphome/components/sm10bit_base/* @Cossid esphome/components/sm10bit_base/* @Cossid
@@ -563,7 +518,6 @@ esphome/components/sound_level/* @kahrendt
esphome/components/spa06_base/* @danielkent-net esphome/components/spa06_base/* @danielkent-net
esphome/components/spa06_i2c/* @danielkent-net esphome/components/spa06_i2c/* @danielkent-net
esphome/components/spa06_spi/* @danielkent-net esphome/components/spa06_spi/* @danielkent-net
esphome/components/spd2010/* @grischard
esphome/components/speaker/* @jesserockz @kahrendt esphome/components/speaker/* @jesserockz @kahrendt
esphome/components/speaker/media_player/* @kahrendt @synesthesiam esphome/components/speaker/media_player/* @kahrendt @synesthesiam
esphome/components/speaker_source/* @kahrendt esphome/components/speaker_source/* @kahrendt
@@ -609,13 +563,12 @@ esphome/components/t6615/* @tylermenezes
esphome/components/tas2780/* @remcom esphome/components/tas2780/* @remcom
esphome/components/tas58xx/* @mrtoy-me @remcom esphome/components/tas58xx/* @mrtoy-me @remcom
esphome/components/tc74/* @sethgirvan esphome/components/tc74/* @sethgirvan
esphome/components/tca8418/* @zebble
esphome/components/tca9548a/* @andreashergert1984 esphome/components/tca9548a/* @andreashergert1984
esphome/components/tca9555/* @mobrembski esphome/components/tca9555/* @mobrembski
esphome/components/tcl112/* @glmnet esphome/components/tcl112/* @glmnet
esphome/components/tcp_uart/* @Bascht74 esphome/components/tcp_uart/* @Bascht74
esphome/components/tee501/* @Stock-M esphome/components/tee501/* @Stock-M
esphome/components/teleinfo/* @0hax @FredM67 esphome/components/teleinfo/* @0hax
esphome/components/tem3200/* @bakerkj esphome/components/tem3200/* @bakerkj
esphome/components/template/alarm_control_panel/* @grahambrown11 @hwstar esphome/components/template/alarm_control_panel/* @grahambrown11 @hwstar
esphome/components/template/datetime/* @rfdarter esphome/components/template/datetime/* @rfdarter
@@ -627,15 +580,12 @@ esphome/components/thermopro_ble/* @sittner
esphome/components/thermostat/* @kbx81 esphome/components/thermostat/* @kbx81
esphome/components/time/* @esphome/core esphome/components/time/* @esphome/core
esphome/components/tinyusb/* @kbx81 esphome/components/tinyusb/* @kbx81
esphome/components/tinyusb_keyboard/* @frederikbolding
esphome/components/tlc5947/* @rnauber esphome/components/tlc5947/* @rnauber
esphome/components/tlc5971/* @IJIJI esphome/components/tlc5971/* @IJIJI
esphome/components/tm1621/* @Philippe12 esphome/components/tm1621/* @Philippe12
esphome/components/tm1637/* @glmnet esphome/components/tm1637/* @glmnet
esphome/components/tm1638/* @skykingjwc esphome/components/tm1638/* @skykingjwc
esphome/components/tm1651/* @mrtoy-me esphome/components/tm1651/* @mrtoy-me
esphome/components/tmc2209/* @remcom
esphome/components/tmc22xx/* @remcom
esphome/components/tmp102/* @timsavage esphome/components/tmp102/* @timsavage
esphome/components/tmp1075/* @sybrenstuvel esphome/components/tmp1075/* @sybrenstuvel
esphome/components/tmp117/* @Azimath esphome/components/tmp117/* @Azimath
@@ -687,7 +637,7 @@ esphome/components/web_server_idf/* @dentra
esphome/components/weikai/* @DrCoolZic esphome/components/weikai/* @DrCoolZic
esphome/components/weikai_i2c/* @DrCoolZic esphome/components/weikai_i2c/* @DrCoolZic
esphome/components/weikai_spi/* @DrCoolZic esphome/components/weikai_spi/* @DrCoolZic
esphome/components/whirlpool/* @arpena @glmnet esphome/components/whirlpool/* @glmnet
esphome/components/whynter/* @aeonsablaze esphome/components/whynter/* @aeonsablaze
esphome/components/wiegand/* @ssieb esphome/components/wiegand/* @ssieb
esphome/components/wireguard/* @droscy @lhoracek @thomas0bernard esphome/components/wireguard/* @droscy @lhoracek @thomas0bernard
+1 -1
View File
@@ -22,7 +22,7 @@ RUN \
-r /requirements.txt -r /requirements.txt
# Install the ESPHome Device Builder dashboard. # Install the ESPHome Device Builder dashboard.
RUN uv pip install --no-cache-dir esphome-device-builder==1.23.1 RUN uv pip install --no-cache-dir esphome-device-builder==1.21.0
RUN \ RUN \
platformio settings set enable_telemetry No \ platformio settings set enable_telemetry No \
+31 -221
View File
@@ -7,7 +7,6 @@ import importlib
import logging import logging
import os import os
from pathlib import Path from pathlib import Path
import platform
import re import re
import sys import sys
import time import time
@@ -48,18 +47,14 @@ from esphome.const import (
CONF_WEB_SERVER, CONF_WEB_SERVER,
CONF_WIFI, CONF_WIFI,
ENV_NOGITIGNORE, ENV_NOGITIGNORE,
KEY_CORE,
KEY_ESP32, KEY_ESP32,
KEY_TARGET_PLATFORM,
KEY_VARIANT, KEY_VARIANT,
PLATFORM_HOST,
SECRETS_FILES, SECRETS_FILES,
ErrorFormat,
Toolchain, Toolchain,
) )
from esphome.core import CORE, EsphomeError, coroutine from esphome.core import CORE, EsphomeError, coroutine
from esphome.enum import StrEnum from esphome.enum import StrEnum
from esphome.helpers import IS_MACOS, get_bool_env, indent, is_ip_address from esphome.helpers import get_bool_env, indent, is_ip_address
from esphome.log import AnsiFore, color, setup_log from esphome.log import AnsiFore, color, setup_log
from esphome.stacktrace import LogLineProcessor from esphome.stacktrace import LogLineProcessor
from esphome.types import ConfigType from esphome.types import ConfigType
@@ -73,7 +68,6 @@ from esphome.util import (
get_serial_ports, get_serial_ports,
is_picotool_usb_permission_error, is_picotool_usb_permission_error,
list_yaml_files, list_yaml_files,
read_secret_line,
run_external_command, run_external_command,
run_external_process, run_external_process,
safe_print, safe_print,
@@ -483,9 +477,12 @@ def has_ota() -> bool:
def has_native_ota() -> bool: def has_native_ota() -> bool:
"""Check if native API OTA upload is available (``platform: esphome``).""" """Check if native API OTA upload is available (``platform: esphome``)."""
from esphome.espota2 import esphome_ota_items if CONF_OTA not in CORE.config:
return False
return bool(esphome_ota_items(CORE.config)) return any(
ota_item.get(CONF_PLATFORM) == CONF_ESPHOME
for ota_item in CORE.config[CONF_OTA]
)
def has_web_server_ota() -> bool: def has_web_server_ota() -> bool:
@@ -1233,28 +1230,14 @@ def upload_program(
config: ConfigType, args: ArgsProtocol, devices: list[str] config: ConfigType, args: ArgsProtocol, devices: list[str]
) -> tuple[int, str | None]: ) -> tuple[int, str | None]:
host = devices[0] host = devices[0]
port_type = get_port_type(host)
ota_key = getattr(args, "ota_key", None)
if ota_key and port_type in (PortType.SERIAL, PortType.BOOTSEL):
_LOGGER.warning(
"The presented OTA key only applies to network uploads; ignored for %s",
host,
)
platform_upload = platform_hooks.get_platform_hook( platform_upload = platform_hooks.get_platform_hook(
CORE.target_platform, "upload_program" CORE.target_platform, "upload_program"
) )
if platform_upload is not None and platform_upload(config, args, host): if platform_upload is not None and platform_upload(config, args, host):
if ota_key and port_type not in (PortType.SERIAL, PortType.BOOTSEL):
# A platform's own network path (nrf52 mcumgr) has no key handshake
_LOGGER.warning(
"The presented OTA key only applies to the %s OTA platform; "
"ignored for %s",
CONF_ESPHOME,
host,
)
return 0, host return 0, host
port_type = get_port_type(host)
# MQTT and MQTTIP are also OTA paths; MQTTIP gets resolved to a real IP later by # MQTT and MQTTIP are also OTA paths; MQTTIP gets resolved to a real IP later by
# _resolve_network_devices(). Only SERIAL and BOOTSEL are non-OTA upload paths. # _resolve_network_devices(). Only SERIAL and BOOTSEL are non-OTA upload paths.
is_partition_table = getattr(args, "partition_table", False) is_partition_table = getattr(args, "partition_table", False)
@@ -1297,8 +1280,6 @@ def upload_program(
network_devices = _resolve_network_devices(devices, config, args) network_devices = _resolve_network_devices(devices, config, args)
if chosen_platform == CONF_WEB_SERVER: if chosen_platform == CONF_WEB_SERVER:
if ota_key:
raise EsphomeError(OTA_KEY_PLATFORM_ERROR)
if is_partition_table or is_bootloader: if is_partition_table or is_bootloader:
raise EsphomeError( raise EsphomeError(
f"{option_string} is only supported with the esphome OTA platform; " f"{option_string} is only supported with the esphome OTA platform; "
@@ -1312,17 +1293,6 @@ def upload_program(
return _upload_via_native_api(config, network_devices, args) return _upload_via_native_api(config, network_devices, args)
def _ota_upload_platforms(config: ConfigType) -> dict[str, None]:
"""The network OTA platforms the config provides, one entry each, in
config order; the web_server final-validate hook merges duplicates."""
return {
ota_platform: None
for ota_item in config.get(CONF_OTA, [])
if (ota_platform := ota_item.get(CONF_PLATFORM))
in (CONF_ESPHOME, CONF_WEB_SERVER)
}
def _choose_ota_platform(config: ConfigType, requested: str | None) -> str: def _choose_ota_platform(config: ConfigType, requested: str | None) -> str:
"""Pick the OTA platform to use, optionally honoring ``--ota-platform``. """Pick the OTA platform to use, optionally honoring ``--ota-platform``.
@@ -1335,7 +1305,16 @@ def _choose_ota_platform(config: ConfigType, requested: str | None) -> str:
stream on ESP32/LibreTiny, which inflate it as it arrives.) Falls back to stream on ESP32/LibreTiny, which inflate it as it arrives.) Falls back to
``web_server`` only when that is the only available platform. ``web_server`` only when that is the only available platform.
""" """
available = _ota_upload_platforms(config) # Use a dict (insertion-ordered) instead of a list so error messages and
# membership checks see one entry per platform even if the user has
# multiple ``ota:`` items of the same platform; the web_server OTA
# platform's final-validate hook merges duplicates anyway.
available: dict[str, None] = {}
for ota_item in config.get(CONF_OTA, []):
platform = ota_item.get(CONF_PLATFORM)
if platform in (CONF_ESPHOME, CONF_WEB_SERVER):
available[platform] = None
if not available: if not available:
raise EsphomeError( raise EsphomeError(
f"Cannot upload Over the Air as the {CONF_OTA} configuration is not " f"Cannot upload Over the Air as the {CONF_OTA} configuration is not "
@@ -1356,17 +1335,14 @@ def _choose_ota_platform(config: ConfigType, requested: str | None) -> str:
return CONF_WEB_SERVER return CONF_WEB_SERVER
def _esphome_ota_conf(config: ConfigType) -> ConfigType | None:
"""The validated esphome OTA platform block, if configured."""
from esphome.espota2 import esphome_ota_items
return next(iter(esphome_ota_items(config)), None)
def _upload_via_native_api( def _upload_via_native_api(
config: ConfigType, network_devices: list[str], args: ArgsProtocol config: ConfigType, network_devices: list[str], args: ArgsProtocol
) -> tuple[int, str | None]: ) -> tuple[int, str | None]:
ota_conf = _esphome_ota_conf(config) or {} ota_conf: ConfigType = {}
for ota_item in config.get(CONF_OTA, []):
if ota_item.get(CONF_PLATFORM) == CONF_ESPHOME:
ota_conf = ota_item
break
from esphome import espota2 from esphome import espota2
from esphome.components.noise import static_encryption_key from esphome.components.noise import static_encryption_key
@@ -1376,15 +1352,9 @@ def _upload_via_native_api(
# Fail closed: an encryption block whose key did not resolve must never # Fail closed: an encryption block whose key did not resolve must never
# fall back to a plaintext upload # fall back to a plaintext upload
noise_psk = None noise_psk = None
old_noise_psk = None
plaintext_fallback = False plaintext_fallback = False
allow_plaintext_upload = False allow_plaintext_upload = False
if ota_key := getattr(args, "ota_key", None): if (encryption_conf := ota_conf.get(CONF_ENCRYPTION)) is not None:
# The user presents a key the device still runs instead of the
# configured one (a key change in progress); an explicit key never
# downgrades to plaintext
noise_psk = ota_key
elif (encryption_conf := ota_conf.get(CONF_ENCRYPTION)) is not None:
noise_psk = encryption_conf.get(CONF_KEY) noise_psk = encryption_conf.get(CONF_KEY)
allow_plaintext_upload = bool( allow_plaintext_upload = bool(
encryption_conf.get(espota2.CONF_ALLOW_PLAINTEXT_UPLOAD) encryption_conf.get(espota2.CONF_ALLOW_PLAINTEXT_UPLOAD)
@@ -1397,8 +1367,6 @@ def _upload_via_native_api(
# Ensure the key is a string, as required by the underlying OTA implementation. # Ensure the key is a string, as required by the underlying OTA implementation.
# It arrives here as a SensitiveStr which aioesphomeapi rejects. # It arrives here as a SensitiveStr which aioesphomeapi rejects.
noise_psk = str(noise_psk) noise_psk = str(noise_psk)
if old_key := encryption_conf.get(espota2.CONF_OLD_KEY):
old_noise_psk = str(old_key)
elif api_key := static_encryption_key(config.get(CONF_API) or {}): elif api_key := static_encryption_key(config.get(CONF_API) or {}):
# Remove before 2027.3.0: the api key is tried, falling back to plaintext # Remove before 2027.3.0: the api key is tried, falling back to plaintext
noise_psk = str(api_key) noise_psk = str(api_key)
@@ -1448,30 +1416,19 @@ def _upload_via_native_api(
noise_psk, noise_psk,
plaintext_fallback=plaintext_fallback, plaintext_fallback=plaintext_fallback,
allow_plaintext_upload=allow_plaintext_upload, allow_plaintext_upload=allow_plaintext_upload,
old_noise_psk=old_noise_psk,
) )
def _old_key_notice(config: ConfigType) -> None:
"""After `run`, whose image was just built from this config; a plain
`upload` may send a stale image that still carries the previous key."""
from esphome import espota2
for item in espota2.esphome_ota_items(config):
if (item.get(CONF_ENCRYPTION) or {}).get(espota2.CONF_OLD_KEY):
_LOGGER.warning(espota2.OLD_KEY_REMOVE_NOTICE)
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]:
from esphome import web_server_ota from esphome import web_server_ota
from esphome.espota2 import esphome_ota_items
from esphome.web_server_helpers import get_web_server_connection from esphome.web_server_helpers import get_web_server_connection
if any( if any(
ota_item.get(CONF_ENCRYPTION) is not None ota_item.get(CONF_PLATFORM) == CONF_ESPHOME
for ota_item in esphome_ota_items(config) and ota_item.get(CONF_ENCRYPTION) is not None
for ota_item in config.get(CONF_OTA, [])
): ):
_LOGGER.warning( _LOGGER.warning(
"This config has OTA encryption, but the web_server OTA path sends " "This config has OTA encryption, but the web_server OTA path sends "
@@ -1779,60 +1736,7 @@ def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None:
return 0 return 0
ENV_OTA_KEY = "ESPHOME_OTA_KEY"
OTA_KEY_PLATFORM_ERROR = (
f"--prompt-ota-key and {ENV_OTA_KEY} only apply to the {CONF_ESPHOME} OTA "
"platform; to flash by serial instead, name the port with --device"
)
def _read_ota_key(args: ArgsProtocol, config: ConfigType) -> None:
"""Take the key to present to the device for this upload, from the
prompt or from ESPHOME_OTA_KEY, before any compile so the prompt is
not buried in build output."""
prompt = getattr(args, "prompt_ota_key", False)
env_key = getattr(args, "env_ota_key", None)
if not prompt and env_key is None:
return
if CORE.data.get(KEY_CORE, {}).get(KEY_TARGET_PLATFORM) == PLATFORM_HOST:
_LOGGER.warning("A host program runs locally; the presented OTA key is ignored")
return
# The HTTP path has no key handshake; decide before prompting or
# compiling. A serial target is a serial flash: the key is not even
# asked for, and --ota-platform means nothing to it. Every entry must
# be serial: a missing port is skipped and the next entry may be a host
devices = getattr(args, "device", None) or []
serial_target = bool(devices) and all(
device == "SERIAL"
or get_port_type(device) in (PortType.SERIAL, PortType.BOOTSEL)
for device in devices
)
chosen = (
_choose_ota_platform(config, getattr(args, "ota_platform", None))
if not serial_target and _ota_upload_platforms(config)
else None
)
if chosen == CONF_WEB_SERVER:
raise EsphomeError(OTA_KEY_PLATFORM_ERROR)
from esphome.components.noise import validate_encryption_key
from esphome.config_validation import Invalid
if serial_target:
_LOGGER.warning(
"The presented OTA key only applies to network uploads; ignored for %s",
", ".join(devices),
)
return
key = read_secret_line("OTA encryption key: ") if prompt else env_key
try:
validate_encryption_key(key)
except Invalid as err:
raise EsphomeError(f"Invalid OTA encryption key: {err}") from err
args.ota_key = key
def command_upload(args: ArgsProtocol, config: ConfigType) -> int | None: def command_upload(args: ArgsProtocol, config: ConfigType) -> int | None:
_read_ota_key(args, config)
# Get devices, resolving special identifiers like OTA # Get devices, resolving special identifiers like OTA
devices = choose_upload_log_host( devices = choose_upload_log_host(
default=args.device, default=args.device,
@@ -1880,7 +1784,6 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
# Elsewhere the flag is ignored, so serial stays fine there. # Elsewhere the flag is ignored, so serial stays fine there.
_LOGGER.error("--skip-bootloader builds cannot be flashed over serial") _LOGGER.error("--skip-bootloader builds cannot be flashed over serial")
return 1 return 1
_read_ota_key(args, config)
exit_code = write_cpp(config) exit_code = write_cpp(config)
if exit_code != 0: if exit_code != 0:
return exit_code return exit_code
@@ -1906,7 +1809,6 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
exit_code, successful_device = upload_program(config, args, devices) exit_code, successful_device = upload_program(config, args, devices)
if exit_code == 0: if exit_code == 0:
_LOGGER.info("Successfully uploaded program.") _LOGGER.info("Successfully uploaded program.")
_old_key_notice(config)
else: else:
_LOGGER.warning("Failed to upload to %s", devices) _LOGGER.warning("Failed to upload to %s", devices)
return exit_code return exit_code
@@ -2043,10 +1945,7 @@ def run_multiple_configs(
safe_print() safe_print()
cmd = command_builder(f) cmd = command_builder(f)
# The parent already logged the Intel macOS warning; children skip it. rc = run_external_process(*cmd)
rc = run_external_process(
*cmd, env={**os.environ, _INTEL_MACOS_WARNED_ENV: "1"}
)
if rc == 0: if rc == 0:
print_bar(f"[{color(AnsiFore.BOLD_GREEN, 'SUCCESS')}] {str(f)}") print_bar(f"[{color(AnsiFore.BOLD_GREEN, 'SUCCESS')}] {str(f)}")
@@ -2302,25 +2201,6 @@ def _add_states_args(parser: argparse.ArgumentParser) -> None:
) )
def _error_format(value: str) -> ErrorFormat:
try:
return ErrorFormat(value.lower())
except ValueError:
choices = ", ".join(ErrorFormat)
raise argparse.ArgumentTypeError(
f"invalid choice: '{value}' (choose from {choices})"
) from None
ARGUMENT_HELP_PROMPT_OTA_KEY = (
"Ask for the OTA encryption key to present to the device instead of using "
"the one in the configuration, for a device that still runs a previous key. "
"The key is read without echo, or as one line from stdin when that is not "
"a terminal; it is never taken from the command line. A calling program "
f"can hand the key over in the {ENV_OTA_KEY} environment variable instead."
)
def parse_args(argv): def parse_args(argv):
options_parser = argparse.ArgumentParser(add_help=False) options_parser = argparse.ArgumentParser(add_help=False)
options_parser.add_argument( options_parser.add_argument(
@@ -2383,17 +2263,6 @@ def parse_args(argv):
), ),
) )
options_parser.add_argument(
"--error-format",
type=_error_format,
metavar="{" + ",".join(f.value for f in ErrorFormat) + "}",
help=(
"How configuration errors are shown. 'yaml' shows them inside the "
"failing YAML; 'line' prints one 'file:line:column: error: message' "
"line per error. Default: $ESPHOME_ERROR_FORMAT, else 'yaml'."
),
)
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description=f"ESPHome {const.__version__}", parents=[options_parser] description=f"ESPHome {const.__version__}", parents=[options_parser]
) )
@@ -2489,11 +2358,6 @@ def parse_args(argv):
help="Upload as partition table (OTA).", help="Upload as partition table (OTA).",
action="store_true", action="store_true",
) )
parser_upload.add_argument(
"--prompt-ota-key",
action="store_true",
help=ARGUMENT_HELP_PROMPT_OTA_KEY,
)
parser_upload.add_argument( parser_upload.add_argument(
"--bootloader", "--bootloader",
help="Upload as bootloader (OTA).", help="Upload as bootloader (OTA).",
@@ -2574,11 +2438,6 @@ def parse_args(argv):
"(HTTP Basic auth) when that is the only configured platform." "(HTTP Basic auth) when that is the only configured platform."
), ),
) )
parser_run.add_argument(
"--prompt-ota-key",
action="store_true",
help=ARGUMENT_HELP_PROMPT_OTA_KEY,
)
parser_clean = subparsers.add_parser( parser_clean = subparsers.add_parser(
"clean-mqtt", "clean-mqtt",
@@ -2707,18 +2566,9 @@ def parse_args(argv):
args, unknown_args = parser.parse_known_args(arguments) args, unknown_args = parser.parse_known_args(arguments)
if unknown_args: if unknown_args:
_LOGGER.warning("Ignored unrecognized arguments: %s", unknown_args) _LOGGER.warning("Ignored unrecognized arguments: %s", unknown_args)
else: return args
args = parser.parse_args(arguments)
# Read here rather than as the option default, so a bad value is reported return parser.parse_args(arguments)
# against the environment variable instead of --error-format
if args.error_format is None:
env_value = os.getenv("ESPHOME_ERROR_FORMAT", ErrorFormat.YAML.value)
try:
args.error_format = _error_format(env_value)
except argparse.ArgumentTypeError as err:
parser.error(f"ESPHOME_ERROR_FORMAT: {err}")
return args
def _warn_if_source_tree_mismatch() -> None: def _warn_if_source_tree_mismatch() -> None:
@@ -2764,34 +2614,12 @@ def _warn_if_source_tree_mismatch() -> None:
) )
_INTEL_MACOS_REMOVAL = "2027.6.0"
_INTEL_MACOS_WARNED_ENV = "ESPHOME_INTEL_MACOS_WARNED"
def _warn_if_intel_macos() -> None:
"""Warn that Intel (x86_64) Python on macOS loses support by _INTEL_MACOS_REMOVAL."""
if (
not IS_MACOS
or platform.machine() != "x86_64"
or _INTEL_MACOS_WARNED_ENV in os.environ
):
return
_LOGGER.warning(
"Support for Intel Macs will end in ESPHome %s or earlier. The Python "
"packages ESPHome depends on have stopped publishing Intel macOS builds, "
"so future releases will not install on this machine. On an Apple "
"Silicon Mac, switch to a native arm64 Python.",
_INTEL_MACOS_REMOVAL,
)
def run_esphome(argv): def run_esphome(argv):
from esphome.address_cache import AddressCache from esphome.address_cache import AddressCache
args = parse_args(argv) args = parse_args(argv)
CORE.dashboard = args.dashboard CORE.dashboard = args.dashboard
CORE.testing_mode = args.testing_mode CORE.testing_mode = args.testing_mode
CORE.error_format = args.error_format
# Create address cache from command-line arguments # Create address cache from command-line arguments
CORE.address_cache = AddressCache.from_cli_args( CORE.address_cache = AddressCache.from_cli_args(
@@ -2804,25 +2632,7 @@ def run_esphome(argv):
args.log_level = "CRITICAL" args.log_level = "CRITICAL"
setup_log(log_level=args.log_level) setup_log(log_level=args.log_level)
# Taken out of the environment before any command runs, so no build
# tool inherits a key a calling program handed over
args.env_ota_key = os.environ.pop(ENV_OTA_KEY, None)
if args.command in ("upload", "run"):
# One key is one device's; several configs run in child processes
# that would never see it
if (args.env_ota_key is not None or args.prompt_ota_key) and len(
args.configuration
) > 1:
raise EsphomeError(
f"--prompt-ota-key and {ENV_OTA_KEY} take one configuration; "
f"{len(args.configuration)} were given"
)
elif args.env_ota_key is not None:
_LOGGER.warning(
"%s is set but the %s command does not use it", ENV_OTA_KEY, args.command
)
_warn_if_source_tree_mismatch() _warn_if_source_tree_mismatch()
_warn_if_intel_macos()
if args.command in PRE_CONFIG_ACTIONS: if args.command in PRE_CONFIG_ACTIONS:
try: try:
+14 -14
View File
@@ -49,40 +49,40 @@ class FrameworkRelease(NamedTuple):
# Arduino core version -> its build in esphome-libs/arduino-esp8266 # Arduino core version -> its build in esphome-libs/arduino-esp8266
FRAMEWORK_RELEASES: dict[Version, FrameworkRelease] = { FRAMEWORK_RELEASES: dict[Version, FrameworkRelease] = {
Version(3, 1, 2): FrameworkRelease( Version(3, 1, 2): FrameworkRelease(
"3.1.2-esphome.3", "3.1.2-esphome.1",
"3af533a85163804e164500a5b254dc23b922080e25ecb7dc37b3dd8ac84651db", "e80751e3123676b967143e39c61f2d8693946db4c7806f2a83dcaaf797ecd582",
37185650, 37189311,
), ),
} }
TOOLCHAIN_PACKAGE = "toolchain-xtensa-lx106-elf" TOOLCHAIN_PACKAGE = "toolchain-xtensa-lx106-elf"
# gcc 10.3, the toolchain Arduino core 3.x builds with; the build # gcc 10.3, the toolchain Arduino core 3.x builds with; the build
# generator's compile flags are tuned to it. # generator's compile flags are tuned to it.
TOOLCHAIN_VERSION = "10.3.0-esphome.3" TOOLCHAIN_VERSION = "10.3.0-esphome.2"
_TOOLCHAIN_RELEASES = ( _TOOLCHAIN_RELEASES = (
"https://github.com/esphome-libs/xtensa-lx106-elf-toolchain/releases/" "https://github.com/esphome-libs/xtensa-lx106-elf-toolchain/releases/"
) )
# Registry system tag -> (sha256, size) of that host's archive # Registry system tag -> (sha256, size) of that host's archive
TOOLCHAIN_BUILDS: dict[str, tuple[str, int]] = { TOOLCHAIN_BUILDS: dict[str, tuple[str, int]] = {
"darwin_arm64": ( "darwin_arm64": (
"ad0ea929238d9b527c43e0d20257acc2a5680bdb0d2b1cf8f2c99169786217e9", "849cede44d4d5c6ea0f14099783239f559f46327bea314281814f2652b486201",
60747260, 60830321,
), ),
"darwin_x86_64": ( "darwin_x86_64": (
"997090cac80c8eb59604ec52b73fb3568e5f2d9b2971f83c3f2a5c545c9c949c", "ca69904daabf0c5983b372423e5e62f49182a793e992c052e94666852470c897",
64065752, 64149487,
), ),
"linux_aarch64": ( "linux_aarch64": (
"403f14d0d5755682fcd6eaea1e4c457a1d8cad30690e52fb0c804d8db266b908", "60a49a4f082bf246544bd409a9517dbbcab19bb30ac9decbee544b896aaccbd6",
67487067, 67573397,
), ),
"linux_x86_64": ( "linux_x86_64": (
"64f21fa3ba736b363dc8c234dbbb39e005e574e33de08a78e9a00c55a64ae706", "1fba33ca1494ec79f2776e0e37eca93282d30f8bb9992f5f4f9a655d6fff1db4",
68343668, 68431336,
), ),
"windows_amd64": ( "windows_amd64": (
"1e62abf8df041ce5158f5770dffef955f2f981ce7f7adee88fcd5a8197705aa6", "af9066b0e5bf036f04f2bd9d08b89b81a7f183c57dac0abcaff71dd861cf5f3b",
67536405, 67664137,
), ),
} }
+111 -311
View File
@@ -1,4 +1,4 @@
from collections.abc import Awaitable, Callable, Sequence from collections.abc import Callable
from dataclasses import dataclass, field from dataclasses import dataclass, field
import logging import logging
import string import string
@@ -23,7 +23,7 @@ from esphome.const import (
) )
from esphome.core import CORE, ID, EsphomeError, HexInt, Lambda from esphome.core import CORE, ID, EsphomeError, HexInt, Lambda
from esphome.cpp_generator import ( from esphome.cpp_generator import (
Expression, FlashStringLiteral,
LambdaExpression, LambdaExpression,
MockObj, MockObj,
MockObjClass, MockObjClass,
@@ -245,26 +245,11 @@ ApplyCondition = cg.esphome_ns.class_("ApplyCondition", Condition)
def flash_string(config: ConfigType, value: str) -> str: def flash_string(config: ConfigType, value: str) -> str:
"""Default renderer for ``std::string`` constants; copies the literal out of flash on ESP8266.""" """Default renderer for ``std::string`` constants; copies the literal out of flash on ESP8266."""
return str(cg.progmem_string(value))
def flash_compare(config: ConfigType, value: str) -> str:
"""Default renderer for ``std::string`` constants in conditions; compares in flash on ESP8266."""
if CORE.is_esp8266: if CORE.is_esp8266:
return f"::esphome::ProgmemStringRef{{PSTR({cg.safe_exp(value)}), {len(value.encode('utf-8'))}}}" return f"progmem_string({FlashStringLiteral(value)})"
return str(cg.safe_exp(value)) return str(cg.safe_exp(value))
def _default_std_string(
members: list[tuple[str, Any, Any]], renderer: Callable[[ConfigType, str], str]
) -> list[tuple[str, Any, Any]]:
"""Give ``cg.std_string`` members without a renderer the given default."""
return [
(key, t, fn or (renderer if t is cg.std_string else None))
for key, t, fn in members
]
def literal_with_length(config: ConfigType, value: str) -> str: def literal_with_length(config: ConfigType, value: str) -> str:
"""Renderer for a ``(const char *, size_t)`` target: a plain literal plus its byte length. """Renderer for a ``(const char *, size_t)`` target: a plain literal plus its byte length.
@@ -274,24 +259,13 @@ def literal_with_length(config: ConfigType, value: str) -> str:
return f"{cg.safe_exp(value)}, {len(value.encode('utf-8'))}" return f"{cg.safe_exp(value)}, {len(value.encode('utf-8'))}"
def string_ref_literal(config: ConfigType, value: str) -> str:
"""Renderer for a ``StringRef`` comparison: a flash literal on ESP8266, else ``StringRef(literal, length)``."""
if CORE.is_esp8266:
return str(cg.FlashStringLiteral(value))
return f"StringRef({literal_with_length(config, value)})"
def _names_parent(text: str) -> bool:
return any(f == "parent" for _, f, _, _ in string.Formatter().parse(text))
@dataclass(frozen=True) @dataclass(frozen=True)
class ApplyCall: class ApplyCall:
"""One statement from config keys, e.g. ``"set_range({}, {})"`` with ``((CONF_LOW, cg.float_), ...)``. """One statement from config keys, e.g. ``"set_range({}, {})"`` with ``((CONF_LOW, cg.float_), ...)``.
Each arg is ``(conf_key, type_)`` or ``(conf_key, type_, const_fn)``. A ``conf_key`` may be a Each arg is ``(conf_key, type_)`` or ``(conf_key, type_, const_fn)``. A ``conf_key`` may be a
path into nested sections. ``target`` and a plain ``str`` ``type_`` (raw C++ type text) may path into nested sections. A plain ``str`` ``type_`` is raw C++ type text and may use
name the parent object as ``{parent}``. ``const_fn(config, value)`` renders a constant's argument text; a lambda or an ``{parent}``. ``const_fn(config, value)`` renders a constant's argument text; a lambda or an
id bypasses it. The statement is skipped when none of its keys is set, always emitted when it id bypasses it. The statement is skipped when none of its keys is set, always emitted when it
has no keys, and a partial set is a config error. has no keys, and a partial set is a config error.
""" """
@@ -303,13 +277,13 @@ class ApplyCall:
fields = [ fields = [
f for _, f, _, _ in string.Formatter().parse(self.target) if f is not None f for _, f, _, _ in string.Formatter().parse(self.target) if f is not None
] ]
if any(f not in ("", "parent") for f in fields): if any(fields):
raise ValueError( raise ValueError(
f"apply target {self.target!r}: only {{}} and {{parent}} placeholders" f"apply target {self.target!r}: only bare {{}} placeholders"
) )
if (count := fields.count("")) != len(self.args): if len(fields) != len(self.args):
raise ValueError( raise ValueError(
f"apply target {self.target!r} has {count} " f"apply target {self.target!r} has {len(fields)} "
f"placeholder(s) for {len(self.args)} config key(s)" f"placeholder(s) for {len(self.args)} config key(s)"
) )
if any(len(arg) not in (2, 3) for arg in self.args): if any(len(arg) not in (2, 3) for arg in self.args):
@@ -317,12 +291,6 @@ class ApplyCall:
f"apply target {self.target!r}: each arg is (conf_key, type_[, const_fn])" f"apply target {self.target!r}: each arg is (conf_key, type_[, const_fn])"
) )
@property
def names_parent(self) -> bool:
return _names_parent(self.target) or any(
isinstance(arg[1], str) and _names_parent(arg[1]) for arg in self.args
)
@property @property
def members(self) -> list[tuple[Any, Any, Any]]: def members(self) -> list[tuple[Any, Any, Any]]:
"""Each arg as ``(conf_key, type_, const_fn or None)``.""" """Each arg as ``(conf_key, type_, const_fn or None)``."""
@@ -397,16 +365,9 @@ def _check_key_in_schema(
schema = schema.schema[markers[part]] schema = schema.schema[markers[part]]
def parent_ref(var: MockObj) -> MockObj:
"""``var`` named from global scope, so a trigger argument cannot shadow it.
Also how a generated callback names its Automation.
"""
return MockObj(f"::{var}", "->")
async def _apply_parent(config: ConfigType, id_key: str = CONF_ID) -> str: async def _apply_parent(config: ConfigType, id_key: str = CONF_ID) -> str:
return str(parent_ref(await cg.get_variable(config[id_key]))) # Global-scope qualified so a trigger arg named like the id cannot shadow it.
return f"::{await cg.get_variable(config[id_key])}"
def _apply_lambda_args(args: TemplateArgsType) -> TemplateArgsType: def _apply_lambda_args(args: TemplateArgsType) -> TemplateArgsType:
@@ -438,7 +399,7 @@ async def _render_values(
members: list[tuple[Any, Any, Any]], members: list[tuple[Any, Any, Any]],
values: list[Any], values: list[Any],
config: ConfigType, config: ConfigType,
parent: str | None, parent: str,
lambda_args: TemplateArgsType, lambda_args: TemplateArgsType,
compare: bool = False, compare: bool = False,
) -> list[str]: ) -> list[str]:
@@ -482,22 +443,6 @@ def register_apply_action(
) -> None: ) -> None:
"""Register an action that only forwards config values to its parent, with no C++ class. """Register an action that only forwards config values to its parent, with no C++ class.
See ``apply_action_builder``.
"""
register_action(name, ApplyAction, schema, synchronous=True)(
apply_action_builder(name, schema, *fields, call=call, id_key=id_key)
)
def apply_action_builder(
name: str,
schema: cv.Schema,
*fields: ApplyField | ApplyCall,
call: str | None = None,
id_key: str = CONF_ID,
) -> Callable[..., Awaitable[MockObj]]:
"""The builder ``register_apply_action`` registers, for a caller that wraps it.
Generates one static function with the parent (read from ``id_key``) and constants baked Generates one static function with the parent (read from ``id_key``) and constants baked
in, lambdas called inline with the trigger args, and an ``ApplyAction`` templated on it. in, lambdas called inline with the trigger args, and an ``ApplyAction`` templated on it.
A constant that is an id (``cv.use_id`` under ``cv.templatable``) is the object it names. A constant that is an id (``cv.use_id`` under ``cv.templatable``) is the object it names.
@@ -508,7 +453,10 @@ def apply_action_builder(
statements_spec = [ statements_spec = [
( (
c.target, c.target,
_default_std_string(c.members, flash_string), [
(key, t, fn or (flash_string if t is cg.std_string else None))
for key, t, fn in c.members
],
) )
for c in (f if isinstance(f, ApplyCall) else f.call() for f in fields) for c in (f if isinstance(f, ApplyCall) else f.call() for f in fields)
] ]
@@ -529,12 +477,12 @@ def apply_action_builder(
statements: list[str] = [] statements: list[str] = []
for target, members in statements_spec: for target, members in statements_spec:
values = _apply_values(config, members) values = _apply_values(config, members)
if not _apply_call_active(members, values): if members and all(value is None for value in values):
continue continue
exprs = await _render_values( exprs = await _render_values(
name, target, members, values, config, parent, lambda_args name, target, members, values, config, parent, lambda_args
) )
statements.append(f"{receiver}{target.format(*exprs, parent=parent)};") statements.append(f"{receiver}{target.format(*exprs)};")
if call: if call:
statements = [ statements = [
f"auto apply_call = {parent}->{call}();", f"auto apply_call = {parent}->{call}();",
@@ -545,28 +493,7 @@ def apply_action_builder(
action_id, cg.void, template_arg, lambda_args, statements action_id, cg.void, template_arg, lambda_args, statements
) )
return builder register_action(name, ApplyAction, schema, synchronous=True)(builder)
def _apply_call_active(members: list[tuple[Any, Any, Any]], values: list[Any]) -> bool:
"""An ``ApplyCall`` is emitted unless it has keys and none of them is set."""
return not members or any(value is not None for value in values)
async def _render_check(
name: str,
target: str,
members: list[tuple[Any, Any, Any]],
values: list[Any],
config: ConfigType,
parent: str | None,
lambda_args: TemplateArgsType,
) -> str:
"""Render a boolean check with ``values`` compared against config."""
exprs = await _render_values(
name, target, members, values, config, parent, lambda_args, compare=True
)
return target.format(*exprs, parent=parent)
def register_apply_condition( def register_apply_condition(
@@ -577,12 +504,11 @@ def register_apply_condition(
``check`` is applied to the parent: ``"is_playing()"`` becomes ``parent->is_playing()``; an ``check`` is applied to the parent: ``"is_playing()"`` becomes ``parent->is_playing()``; an
``ApplyCall`` such as ``ApplyCall("state == {}", ((CONF_STATE, cg.bool_),))`` compares ``ApplyCall`` such as ``ApplyCall("state == {}", ((CONF_STATE, cg.bool_),))`` compares
against config values, all of which must be present. Write ``== false`` to negate. against config values, all of which must be present. Write ``== false`` to negate.
A ``cg.std_string`` constant stays in flash on ESP8266 and supports only ``==`` or ``!=`` String constants are plain literals, so compare a ``std::string`` or ``StringRef`` member.
against a ``std::string`` or ``StringRef`` member; other string types are plain literals.
Generates one static predicate and an ``ApplyCondition`` templated on it. Generates one static predicate and an ``ApplyCondition`` templated on it.
""" """
call = check if isinstance(check, ApplyCall) else ApplyCall(check) call = check if isinstance(check, ApplyCall) else ApplyCall(check)
members = _default_std_string(call.members, flash_compare) members = call.members
_check_key_in_schema(name, schema, id_key) _check_key_in_schema(name, schema, id_key)
for conf_key, _, _ in members: for conf_key, _, _ in members:
_check_key_in_schema(name, schema, conf_key) _check_key_in_schema(name, schema, conf_key)
@@ -595,16 +521,22 @@ def register_apply_condition(
) -> MockObj: ) -> MockObj:
parent = await _apply_parent(config, id_key) parent = await _apply_parent(config, id_key)
lambda_args = _apply_lambda_args(args) lambda_args = _apply_lambda_args(args)
values = _apply_values(config, members) exprs = await _render_values(
check = await _render_check( name,
name, call.target, members, values, config, parent, lambda_args call.target,
members,
_apply_values(config, members),
config,
parent,
lambda_args,
compare=True,
) )
return _apply_function( return _apply_function(
condition_id, condition_id,
cg.bool_, cg.bool_,
template_arg, template_arg,
lambda_args, lambda_args,
[f"return {parent}->{check};"], [f"return {parent}->{call.target.format(*exprs)};"],
) )
register_condition(name, ApplyCondition, schema)(builder) register_condition(name, ApplyCondition, schema)(builder)
@@ -740,22 +672,54 @@ XorCondition = cg.esphome_ns.class_("XorCondition", Condition)
@register_condition("and", AndCondition, validate_condition_list) @register_condition("and", AndCondition, validate_condition_list)
@register_condition("all", AndCondition, validate_condition_list) async def and_condition_to_code(
@register_condition("or", OrCondition, validate_condition_list)
@register_condition("any", OrCondition, validate_condition_list)
@register_condition("xor", XorCondition, validate_condition_list)
async def condition_group_to_code(
config: ConfigType, config: ConfigType,
condition_id: ID, condition_id: ID,
template_arg: cg.TemplateArguments, template_arg: cg.TemplateArguments,
args: TemplateArgsType, args: TemplateArgsType,
) -> MockObj: ) -> MockObj:
conditions = await build_condition_list(config, template_arg, args) conditions = await build_condition_list(config, template_arg, args)
# A group of one is that condition for and, or and xor; skip the wrapper.
if len(conditions) == 1:
return conditions[0]
return cg.new_Pvariable( return cg.new_Pvariable(
condition_id, cg.TemplateArguments(len(conditions), *template_arg), *conditions condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions
)
@register_condition("or", OrCondition, validate_condition_list)
async def or_condition_to_code(
config: ConfigType,
condition_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
conditions = await build_condition_list(config, template_arg, args)
return cg.new_Pvariable(
condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions
)
@register_condition("all", AndCondition, validate_condition_list)
async def all_condition_to_code(
config: ConfigType,
condition_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
conditions = await build_condition_list(config, template_arg, args)
return cg.new_Pvariable(
condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions
)
@register_condition("any", OrCondition, validate_condition_list)
async def any_condition_to_code(
config: ConfigType,
condition_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
conditions = await build_condition_list(config, template_arg, args)
return cg.new_Pvariable(
condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions
) )
@@ -770,6 +734,19 @@ async def not_condition_to_code(
return cg.new_Pvariable(condition_id, template_arg, condition) return cg.new_Pvariable(condition_id, template_arg, condition)
@register_condition("xor", XorCondition, validate_condition_list)
async def xor_condition_to_code(
config: ConfigType,
condition_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
conditions = await build_condition_list(config, template_arg, args)
return cg.new_Pvariable(
condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions
)
@register_condition("lambda", LambdaCondition, cv.returning_lambda) @register_condition("lambda", LambdaCondition, cv.returning_lambda)
async def lambda_condition_to_code( async def lambda_condition_to_code(
config: ConfigType, config: ConfigType,
@@ -871,13 +848,11 @@ async def if_action_to_code(
condition = await build_condition(config[cond_conf], template_arg, args) condition = await build_condition(config[cond_conf], template_arg, args)
var = cg.new_Pvariable(action_id, if_template_arg, condition) var = cg.new_Pvariable(action_id, if_template_arg, condition)
if CONF_THEN in config: if CONF_THEN in config:
for action in await build_action_list(config[CONF_THEN], template_arg, args): actions = await build_action_list(config[CONF_THEN], template_arg, args)
cg.add(var.add_then_action(action)) cg.add(var.add_then(actions))
cg.add(var.finish_then())
if has_else: if has_else:
for action in await build_action_list(config[CONF_ELSE], template_arg, args): actions = await build_action_list(config[CONF_ELSE], template_arg, args)
cg.add(var.add_else_action(action)) cg.add(var.add_else(actions))
cg.add(var.finish_else())
return var return var
@@ -900,27 +875,11 @@ async def while_action_to_code(
) -> MockObj: ) -> MockObj:
condition = await build_condition(config[CONF_CONDITION], template_arg, args) condition = await build_condition(config[CONF_CONDITION], template_arg, args)
var = cg.new_Pvariable(action_id, template_arg, condition) var = cg.new_Pvariable(action_id, template_arg, condition)
for action in await build_action_list(config[CONF_THEN], template_arg, args): actions = await build_action_list(config[CONF_THEN], template_arg, args)
cg.add(var.add_then_action(action)) cg.add(var.add_then(actions))
cg.add(var.finish_then())
return var return var
def _shadow_iteration(args: TemplateArgsType) -> TemplateArgsType:
"""Rename an inherited `iteration` so the inner repeat's own counter can shadow it."""
names = {name for _, name in args}
renamed: TemplateArgsType = []
for type_, name in args:
if name == "iteration":
depth = 1
while f"iteration_{depth}" in names:
depth += 1
name = f"iteration_{depth}"
names.add(name)
renamed.append((type_, name))
return renamed
@register_action( @register_action(
"repeat", "repeat",
RepeatAction, RepeatAction,
@@ -944,11 +903,9 @@ async def repeat_action_to_code(
actions = await build_action_list( actions = await build_action_list(
config[CONF_THEN], config[CONF_THEN],
cg.TemplateArguments(cg.uint32, *template_arg.args), cg.TemplateArguments(cg.uint32, *template_arg.args),
[(cg.uint32, "iteration"), *_shadow_iteration(args)], [(cg.uint32, "iteration"), *args],
) )
for action in actions: cg.add(var.add_then(actions))
cg.add(var.add_then_action(action))
cg.add(var.finish_then())
return var return var
@@ -1084,141 +1041,30 @@ def has_non_synchronous_actions(actions: ConfigType) -> bool:
Non-synchronous actions (delay, wait_until, script.wait, etc.) store Non-synchronous actions (delay, wait_until, script.wait, etc.) store
trigger args for later execution, making non-owning types like StringRef trigger args for later execution, making non-owning types like StringRef
unsafe. unsafe.
Accepts an action list, a single action, or an automation. Only the keys of
action entries are looked up as action names; the options of an action are
only searched for nested action lists (``then:``, ``else:``, ...), so an
option that shares a name with an action, such as the ``delay:`` option of
``speaker_source.set_playlist_delay``, is not mistaken for that action.
""" """
if isinstance(actions, list): if isinstance(actions, list):
return any(has_non_synchronous_actions(item) for item in actions) return any(has_non_synchronous_actions(item) for item in actions)
if isinstance(actions, dict): if isinstance(actions, dict):
for key, options in actions.items(): for key in actions:
if key in ACTION_REGISTRY and not ACTION_REGISTRY[key].synchronous: if key in ACTION_REGISTRY and not ACTION_REGISTRY[key].synchronous:
return True return True
if _options_have_non_synchronous_actions(options): return any(
return True has_non_synchronous_actions(v)
for v in actions.values()
if isinstance(v, (list, dict))
)
return False return False
def _options_have_non_synchronous_actions(options: Any) -> bool:
"""Search an action's options for nested action lists, without treating option keys as actions."""
if isinstance(options, list):
return has_non_synchronous_actions(options)
if isinstance(options, dict):
return any(_options_have_non_synchronous_actions(v) for v in options.values())
return False
def validate_no_deferring_actions(
context: str, data: str = "the trigger data"
) -> Callable[[ConfigType], ConfigType]:
"""Return a validator that rejects deferring actions in a validated automation.
Use it for triggers whose arguments only live while the callback runs (views into a reused
buffer, a JSON document freed on return, ...): deferring actions keep the arguments for later.
:param context: Where the automation is, for the error message, e.g. ``"on_json and on_data"``.
:param data: What is only valid while the automation runs, for the error message.
"""
def validator(value: ConfigType) -> ConfigType:
if has_non_synchronous_actions(value):
raise cv.Invalid(
f"Deferring actions (delay, wait_until, script.wait, ...) are not allowed in "
f"{context}: {data} is only valid while the automation runs. Copy what you need "
"into globals first, then defer in a separate script or automation."
)
return value
return validator
def validate_synchronous_automation(
context: str,
data: str = "the trigger data",
extra_schema=None,
extra_validators=None,
single: bool = False,
) -> cv.All:
"""``validate_automation()`` that also rejects deferring actions.
See ``validate_no_deferring_actions()`` for ``context`` and ``data``; the other parameters are
passed to ``validate_automation()``.
"""
return cv.All(
validate_automation(
extra_schema=extra_schema,
extra_validators=extra_validators,
single=single,
),
validate_no_deferring_actions(context, data),
)
async def _new_automation(
args: TemplateArgsType, config: ConfigType, *ctor_args: MockObj
) -> MockObj:
"""Create the Automation for ``config`` with its actions."""
templ = cg.TemplateArguments(*(arg[0] for arg in args))
obj = cg.new_Pvariable(config[CONF_AUTOMATION_ID], templ, *ctor_args)
actions = await build_action_list(config[CONF_THEN], templ, args)
for action in actions:
cg.add(obj.add_action(action))
return obj
async def build_automation( async def build_automation(
trigger: MockObj, args: TemplateArgsType, config: ConfigType trigger: MockObj, args: TemplateArgsType, config: ConfigType
) -> MockObj: ) -> MockObj:
return await _new_automation(args, config, trigger) arg_types = [arg[0] for arg in args]
templ = cg.TemplateArguments(*arg_types)
obj = cg.new_Pvariable(config[CONF_AUTOMATION_ID], templ, trigger)
async def build_trigger_callback( actions = await build_action_list(config[CONF_THEN], templ, args)
args: TemplateArgsType, cg.add(obj.add_actions(actions))
config: ConfigType, return obj
params: TemplateArgsType,
forward: Sequence[str | Expression] | None = None,
when: str | ApplyCall | None = None,
parent: MockObj | None = None,
) -> LambdaExpression:
"""Build the Automation for ``config`` and return a stateless callback that triggers it.
``params`` are the parent callback's parameters, ``forward`` the expressions passed to
``trigger()`` (default: the parameter names; write the parent as ``parent_ref(var)``),
``when`` a filter the callback returns early on, skipped like any ``ApplyCall`` when none
of its keys is set. ``when`` may name ``parent`` as ``{parent}``, e.g.
``"{parent}->is_fully_open()"``.
"""
members: list[tuple[Any, Any, Any]] = []
if when is not None:
call = when if isinstance(when, ApplyCall) else ApplyCall(when)
members = call.members
if parent is None and call.names_parent:
raise ValueError(f"trigger filter {call.target!r} names {{parent}}")
obj = await _new_automation(args, config)
lambda_args = _apply_lambda_args(params)
statements: list[str] = []
if when is not None:
values = _apply_values(config, members)
if _apply_call_active(members, values):
check = await _render_check(
"trigger filter",
call.target,
members,
values,
config,
None if parent is None else str(parent_ref(parent)),
lambda_args,
)
statements.append(f"if (!({check}))\n return;")
if forward is None:
forward = [name for _, name in params]
statements.append(f"{parent_ref(obj)}->trigger({', '.join(map(str, forward))});")
return LambdaExpression(
["\n".join(statements)], lambda_args, capture="", return_type=cg.void
)
async def build_callback_automation( async def build_callback_automation(
@@ -1227,9 +1073,6 @@ async def build_callback_automation(
args: TemplateArgsType, args: TemplateArgsType,
config: ConfigType, config: ConfigType,
forwarder: MockObj | MockObjClass | None = None, forwarder: MockObj | MockObjClass | None = None,
params: TemplateArgsType | None = None,
forward: Sequence[str | Expression] | None = None,
when: str | ApplyCall | None = None,
) -> None: ) -> None:
"""Build an Automation and register it as a callback on the parent. """Build an Automation and register it as a callback on the parent.
@@ -1241,9 +1084,6 @@ async def build_callback_automation(
pointer-sized (single Automation* field) to fit inline in Callback::ctx_ pointer-sized (single Automation* field) to fit inline in Callback::ctx_
and avoid heap allocation. and avoid heap allocation.
With ``params``, ``forward`` or ``when`` the callback is instead the stateless
lambda of ``build_trigger_callback``; ``forwarder`` cannot be combined with them.
:param parent: The component object (e.g., button, sensor). :param parent: The component object (e.g., button, sensor).
:param callback_method: Name of the callback method (e.g., "add_on_press_callback"). :param callback_method: Name of the callback method (e.g., "add_on_press_callback").
:param args: Automation template args as list of (type, name) tuples. :param args: Automation template args as list of (type, name) tuples.
@@ -1252,56 +1092,22 @@ async def build_callback_automation(
TriggerForwarder<Ts...>. Pass any struct type whose aggregate init takes TriggerForwarder<Ts...>. Pass any struct type whose aggregate init takes
a single Automation pointer (e.g., TriggerOnTrueForwarder). a single Automation pointer (e.g., TriggerOnTrueForwarder).
""" """
if params is not None or forward is not None or when is not None: arg_types = [arg[0] for arg in args]
if forwarder is not None: templ = cg.TemplateArguments(*arg_types)
raise ValueError( obj = cg.new_Pvariable(config[CONF_AUTOMATION_ID], templ)
"forwarder cannot be combined with params, forward or when" actions = await build_action_list(config[CONF_THEN], templ, args)
) cg.add(obj.add_actions(actions))
callback = await build_trigger_callback(
args, config, args if params is None else params, forward, when, parent
)
cg.add(getattr(parent, callback_method)(callback))
return
obj = await _new_automation(args, config)
# Use template forwarder structs for deduplication. The compiler generates # Use template forwarder structs for deduplication. The compiler generates
# one operator() per forwarder type; different automation pointers are just # one operator() per forwarder type; different automation pointers are just
# data in the struct. # data in the struct.
if forwarder is None: if forwarder is None:
forwarder = TriggerForwarder.template(*(arg[0] for arg in args)) forwarder = TriggerForwarder.template(templ)
# RawExpression for aggregate init — both forwarder and obj are codegen # RawExpression for aggregate init — both forwarder and obj are codegen
# MockObjs (not user input), and there's no Expression type for positional # MockObjs (not user input), and there's no Expression type for positional
# aggregate initialization (StructInitializer uses named fields). # aggregate initialization (StructInitializer uses named fields).
cg.add(getattr(parent, callback_method)(cg.RawExpression(f"{forwarder}{{{obj}}}"))) cg.add(getattr(parent, callback_method)(cg.RawExpression(f"{forwarder}{{{obj}}}")))
async def build_parent_callback_automation(
parent: MockObj, callback_method: str, arg: tuple[Any, str], config: ConfigType
) -> None:
"""Register an Automation that receives ``parent`` from a callback that carries nothing.
``arg`` is the automation's ``(type, name)``, e.g. ``(Fan.operator("ptr"), "x")``.
"""
await build_callback_automation(
parent, callback_method, [arg], config, params=[], forward=[parent_ref(parent)]
)
async def build_trigger_automations(
parent: MockObj | None,
config: ConfigType,
entries: tuple[tuple[str, TemplateArgsType], ...],
) -> None:
"""Instantiate each entry's Trigger class, with ``parent`` when given, and build its automations.
``entries`` are ``(conf_key, args)`` pairs; the class comes from the entry's ``CONF_TRIGGER_ID``.
"""
ctor_args = () if parent is None else (parent,)
for conf_key, args in entries:
for conf in config.get(conf_key, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], *ctor_args)
await build_automation(trigger, args, conf)
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
class CallbackAutomation: class CallbackAutomation:
"""A single callback automation entry for build_callback_automations.""" """A single callback automation entry for build_callback_automations."""
@@ -1310,9 +1116,6 @@ class CallbackAutomation:
callback_method: str callback_method: str
args: TemplateArgsType = field(default_factory=list) args: TemplateArgsType = field(default_factory=list)
forwarder: MockObj | MockObjClass | None = None forwarder: MockObj | MockObjClass | None = None
params: TemplateArgsType | None = None
forward: Sequence[str | Expression] | None = None
when: str | ApplyCall | None = None
async def build_callback_automations( async def build_callback_automations(
@@ -1334,7 +1137,4 @@ async def build_callback_automations(
entry.args, entry.args,
conf, conf,
forwarder=entry.forwarder, forwarder=entry.forwarder,
params=entry.params,
forward=entry.forward,
when=entry.when,
) )
+27 -42
View File
@@ -54,7 +54,6 @@ from esphome.components.esp8266.const import (
KEY_ESP8266, KEY_ESP8266,
KEY_FLASH_SIZE, KEY_FLASH_SIZE,
KEY_SCANF_FLOAT, KEY_SCANF_FLOAT,
THROW_STUBS_HEADER,
) )
from esphome.core import CORE, EsphomeError from esphome.core import CORE, EsphomeError
from esphome.framework_helpers import get_project_cxx_compile_flags from esphome.framework_helpers import get_project_cxx_compile_flags
@@ -1057,7 +1056,7 @@ def write_project(paths: InstalledPaths, ccache: str | None) -> bool:
cxx = (toolchain_tool(paths.toolchain, "g++"),) cxx = (toolchain_tool(paths.toolchain, "g++"),)
lines = [ lines = [
*tool_lines((toolchain_tool(paths.toolchain, "gcc"),), cxx, ccache), *tool_lines((toolchain_tool(paths.toolchain, "gcc"),), cxx, ccache),
*compile_rule_lines(ccache), *compile_rule_lines(),
*ar_rule_lines(toolchain_tool(paths.toolchain, "ar")), *ar_rule_lines(toolchain_tool(paths.toolchain, "ar")),
*pch_rule_lines(), *pch_rule_lines(),
"rule link", "rule link",
@@ -1088,8 +1087,32 @@ def write_project(paths: InstalledPaths, ccache: str | None) -> bool:
if "USE_ESP8266_WAVEFORM_STUBS" in flag_defines: if "USE_ESP8266_WAVEFORM_STUBS" in flag_defines:
core_exclude |= _CORE_EXCLUDE_WAVEFORM core_exclude |= _CORE_EXCLUDE_WAVEFORM
# esp8266/__init__ pins build_src_flags (the throw_stubs force-include), as on archives = []
# the PlatformIO path; -include paths resolve against the source root # variant_dir existence was already enforced with the include dirs
variant_sources = collect_sources(variant_dir)
if variant_sources:
objs = compile_edges(lines, variant_sources, variant_dir, "variant")
lines.append(f"build libFrameworkArduinoVariant.a: ar {' '.join(objs)}")
archives.append("libFrameworkArduinoVariant.a")
core_objs = compile_edges(
lines, collect_sources(core_dir, core_exclude), core_dir, "core"
)
if not core_objs:
# An empty archive would link into a wall of undefined references
# (app_entry, the exception vectors) far from the cause
raise EsphomeError(
f"{_INCOMPLETE_INSTALL}: no core sources in {core_dir}; {_CLEAN_HINT}"
)
lines.append(f"build libFrameworkArduino.a: ar {' '.join(core_objs)}")
archives.append("libFrameworkArduino.a")
lib_archives, direct_objs = library_edges(lines, libraries)
archives += lib_archives
# One source of truth with the PlatformIO path: esp8266/__init__ pins
# build_src_flags (the throw_stubs force-include); -include paths
# resolve against the source root
src_other: list[str] = [] src_other: list[str] = []
src_includes: list[str] = [] src_includes: list[str] = []
src_it = iter( src_it = iter(
@@ -1108,45 +1131,7 @@ def write_project(paths: InstalledPaths, ccache: str | None) -> bool:
src_includes.append(tok[len("-include") :]) src_includes.append(tok[len("-include") :])
else: else:
src_other.append(_shell_token(tok)) src_other.append(_shell_token(tok))
# The throw stubs abort: none anywhere with exceptions on, else the core and
# libraries get them too (src keeps them after its pch include)
if config.exceptions:
src_includes = [h for h in src_includes if h != THROW_STUBS_HEADER]
include_flags = [f"-include {_q(src_dir / h)}" for h in src_includes] include_flags = [f"-include {_q(src_dir / h)}" for h in src_includes]
framework_flags = ""
if THROW_STUBS_HEADER in src_includes:
stubs_flag = include_flags[src_includes.index(THROW_STUBS_HEADER)]
lines.append(f"frameworkflags = {stubs_flag}")
framework_flags = "$frameworkflags"
archives = []
# variant_dir existence was already enforced with the include dirs
variant_sources = collect_sources(variant_dir)
if variant_sources:
objs = compile_edges(
lines, variant_sources, variant_dir, "variant", flags=framework_flags
)
lines.append(f"build libFrameworkArduinoVariant.a: ar {' '.join(objs)}")
archives.append("libFrameworkArduinoVariant.a")
core_objs = compile_edges(
lines,
collect_sources(core_dir, core_exclude),
core_dir,
"core",
flags=framework_flags,
)
if not core_objs:
# An empty archive would link into a wall of undefined references
# (app_entry, the exception vectors) far from the cause
raise EsphomeError(
f"{_INCOMPLETE_INSTALL}: no core sources in {core_dir}; {_CLEAN_HINT}"
)
lines.append(f"build libFrameworkArduino.a: ar {' '.join(core_objs)}")
archives.append("libFrameworkArduino.a")
lib_archives, direct_objs = library_edges(lines, libraries, framework_flags)
archives += lib_archives
# One shared variable instead of repeating the flags line on every src # One shared variable instead of repeating the flags line on every src
# edge (hundreds of edges in a real project) # edge (hundreds of edges in a real project)
lines.append(f"srcflags = {' '.join(src_other + include_flags)}") lines.append(f"srcflags = {' '.join(src_other + include_flags)}")
+15 -51
View File
@@ -237,35 +237,6 @@ def idf_macro_matches(idf_path: Path) -> bool:
return live == _EXPECTED_MACRO return live == _EXPECTED_MACRO
def _cmake_args_for_write(minimal: bool) -> dict[str, str]:
"""The cg.add_cmake_arg() values for one project write.
The discovery (minimal) write lets every built-in component register so
the list it finds, cached per target, serves any exclusion set; the full
write drops the excluded names from that list and passes all of them to
IDF. Names outside the framework's components directory (managed
components) stay excluded on the discovery write too, since the list
only holds built-in ones. A checkout supplied through IDF_PATH caches
nothing, so its discovery keeps every exclusion.
"""
from esphome.espidf.toolchain import _esphome_manages_idf, _get_idf_path
args = dict(CORE.cmake_args)
if (
not minimal
or not _esphome_manages_idf()
or not (excluded := args.get("EXCLUDE_COMPONENTS"))
):
return args
root = _get_idf_path() / "components"
outside = [name for name in excluded.split(";") if not (root / name).is_dir()]
if outside:
args["EXCLUDE_COMPONENTS"] = ";".join(outside)
else:
del args["EXCLUDE_COMPONENTS"]
return args
def get_project_cmakelists( def get_project_cmakelists(
minimal: bool = False, builtin_components: list[str] | None = None minimal: bool = False, builtin_components: list[str] | None = None
) -> str: ) -> str:
@@ -329,10 +300,11 @@ def get_project_cmakelists(
# CMake variables registered via cg.add_cmake_arg(). Emitted before # CMake variables registered via cg.add_cmake_arg(). Emitted before
# include(project.cmake) so values like EXCLUDE_COMPONENTS are already # include(project.cmake) so values like EXCLUDE_COMPONENTS are already
# set when project.cmake seeds the component list. # set when project.cmake seeds the component list, and on minimal
# (discovery) writes too so excluded components never register.
cmake_args = "\n".join( cmake_args = "\n".join(
f"set({name} {_cmake_quote(value)})" f"set({name} {_cmake_quote(value)})"
for name, value in sorted(_cmake_args_for_write(minimal).items()) for name, value in sorted(CORE.cmake_args.items())
) )
# Per-project list exposed as a CMake variable so converted PIO libs # Per-project list exposed as a CMake variable so converted PIO libs
@@ -355,10 +327,10 @@ def get_project_cmakelists(
# component's REQUIRES including real IDF components). Referenced by # component's REQUIRES including real IDF components). Referenced by
# src/CMakeLists and by each converted PIO lib's CMakeLists. Skipped # src/CMakeLists and by each converted PIO lib's CMakeLists. Skipped
# on minimal writes because project_description.json may be stale. # on minimal writes because project_description.json may be stale.
# Excluded components are dropped here as well: the discovery list # Excluded components are dropped here as well: a stale
# (cached per target) still holds them, and requiring an excluded # project_description.json from a build without exclusions may still
# component pulls it back into the build (IDF requirement expansion # list them, and requiring an excluded component pulls it back into
# overrides EXCLUDE_COMPONENTS). # the build (IDF requirement expansion overrides EXCLUDE_COMPONENTS).
# Derived from the EXCLUDE_COMPONENTS cmake arg emitted above so the # Derived from the EXCLUDE_COMPONENTS cmake arg emitted above so the
# two can never disagree within one generated file. # two can never disagree within one generated file.
builtin_components_property = ( builtin_components_property = (
@@ -425,24 +397,15 @@ project({CORE.name})
# Emit per-memory-type JSON size data for ESPHome to read post-build. # Emit per-memory-type JSON size data for ESPHome to read post-build.
# json2 stays small; raw dumps every symbol (~2s on a large map) and # json2 stays small; raw dumps every symbol (~2s on a large map) and
# this command runs inside the link edge, blocking everything downstream. # this command runs inside the link edge, blocking everything downstream.
# IDF's size target depends on the map, so ninja has to know the link # The map is a BYPRODUCT so ninja knows the link writes it; IDF's size
# writes it for size to build in the same run as all. IDF declares the # target depends on the map and can then be built in the same run as all.
# map itself since espressif/esp-idf#19201 (and in cmakev2), and ninja # IDF's cmakev2 declares the map itself, so drop this line on that switch.
# rejects two rules for one output, so declare it here only when IDF has
# not. add_custom_command(OUTPUT) marks its outputs GENERATED.
get_source_file_property(esphome_map_declared
${{CMAKE_BINARY_DIR}}/${{CMAKE_PROJECT_NAME}}.map GENERATED)
if(esphome_map_declared)
set(esphome_map_byproducts)
else()
set(esphome_map_byproducts BYPRODUCTS ${{CMAKE_BINARY_DIR}}/${{CMAKE_PROJECT_NAME}}.map)
endif()
add_custom_command( add_custom_command(
TARGET ${{CMAKE_PROJECT_NAME}}.elf POST_BUILD TARGET ${{CMAKE_PROJECT_NAME}}.elf POST_BUILD
COMMAND ${{PYTHON}} -m esp_idf_size {size_ng_flag} --format=json2 COMMAND ${{PYTHON}} -m esp_idf_size {size_ng_flag} --format=json2
-o ${{CMAKE_BINARY_DIR}}/esp_idf_size.json -o ${{CMAKE_BINARY_DIR}}/esp_idf_size.json
${{CMAKE_PROJECT_NAME}}.map ${{CMAKE_PROJECT_NAME}}.map
${{esphome_map_byproducts}} BYPRODUCTS ${{CMAKE_BINARY_DIR}}/${{CMAKE_PROJECT_NAME}}.map
WORKING_DIRECTORY ${{CMAKE_BINARY_DIR}} WORKING_DIRECTORY ${{CMAKE_BINARY_DIR}}
VERBATIM VERBATIM
) )
@@ -591,9 +554,10 @@ def write_project(
) )
# Snapshot the exclusion set so has_outdated_files() can trigger a # Snapshot the exclusion set so has_outdated_files() can trigger a
# reconfigure when it changes. The ESPHOME_PROJECT_BUILTIN_COMPONENTS # discovery reconfigure when it changes. Excluded components never
# property drops excluded names, so re-including one (e.g. a config # register in project_description.json, so re-including one (e.g. a
# gains mqtt) needs a reconfigure to rebuild it from the discovery list. # config gains mqtt) requires a fresh discovery pass before the
# ESPHOME_PROJECT_BUILTIN_COMPONENTS property can list it.
write_file_if_changed( write_file_if_changed(
CORE.relative_build_path("exclude_components.esphomeinternal"), CORE.relative_build_path("exclude_components.esphomeinternal"),
";".join(get_excluded_builtin_components()), ";".join(get_excluded_builtin_components()),
+1 -1
View File
@@ -240,7 +240,7 @@ def write_project(compilers: HostCompilers, ccache: str | None) -> bool:
lines = [ lines = [
*tool_lines(compilers.cc, compilers.cxx, ccache), *tool_lines(compilers.cc, compilers.cxx, ccache),
*compile_rule_lines(ccache), *compile_rule_lines(),
*pch_rule_lines(), *pch_rule_lines(),
"rule link", "rule link",
" command = $cxx -o $out $linkflags @$out.rsp $archives $libdirflags $libflags", " command = $cxx -o $out $linkflags @$out.rsp $archives $libdirflags $libflags",
+9 -18
View File
@@ -74,40 +74,33 @@ def tool_lines(
# path gets # path gets
f"python = {_q(strip_win_long_path_prefix(sys.executable))}", f"python = {_q(strip_win_long_path_prefix(sys.executable))}",
f"buildtool = {_q(_BUILD_TOOL)}", f"buildtool = {_q(_BUILD_TOOL)}",
*([f"ccache = {_q(ccache)}"] if ccache else []), f"ccache = {_q(ccache) if ccache else ''}",
"", "",
] ]
def compile_rule_lines(ccache: str | None) -> list[str]: def compile_rule_lines() -> list[str]:
"""The compile rules; ``$own_includes`` is empty unless an edge sets it. """The compile rules; ``$own_includes`` is empty unless an edge sets it."""
``ccache`` is the launcher ``tool_lines`` was given; the same test here
keeps the variable and the rules in step. It is spliced in rather than
left as an empty variable: a leading space on the command line makes
CreateProcess fail on Windows.
"""
launcher = "$ccache " if ccache else ""
return [ return [
"rule c", "rule c",
f" command = {launcher}$cc -MMD -MF $out.d $own_includes $cflags $flags -c $in -o $out", " command = $ccache $cc -MMD -MF $out.d $own_includes $cflags $flags -c $in -o $out",
" depfile = $out.d", " depfile = $out.d",
" deps = gcc", " deps = gcc",
" description = CC $out", " description = CC $out",
"rule cxx", "rule cxx",
f" command = {launcher}$cxx -MMD -MF $out.d $own_includes $cxxflags $flags -c $in -o $out", " command = $ccache $cxx -MMD -MF $out.d $own_includes $cxxflags $flags -c $in -o $out",
" depfile = $out.d", " depfile = $out.d",
" deps = gcc", " deps = gcc",
" description = CXX $out", " description = CXX $out",
"rule aspp", "rule aspp",
f" command = {launcher}$cc -MMD -MF $out.d -x assembler-with-cpp $own_includes $asflags $flags -c $in -o $out", " command = $ccache $cc -MMD -MF $out.d -x assembler-with-cpp $own_includes $asflags $flags -c $in -o $out",
" depfile = $out.d", " depfile = $out.d",
" deps = gcc", " deps = gcc",
" description = AS $out", " description = AS $out",
# Plain assembler, as SCons's ASCOM: no preprocessor, so no # Plain assembler, as SCons's ASCOM: no preprocessor, so no
# depfile and no $flags (defines/includes) either # depfile and no $flags (defines/includes) either
"rule asm", "rule asm",
f" command = {launcher}$cc -x assembler $asflags -c $in -o $out", " command = $ccache $cc -x assembler $asflags -c $in -o $out",
" description = AS $out", " description = AS $out",
] ]
@@ -214,12 +207,10 @@ def compile_edges(
def library_edges( def library_edges(
lines: list[str], libraries: list[ArduinoLibrary], extra_flags: str = "" lines: list[str], libraries: list[ArduinoLibrary]
) -> tuple[list[str], list[str]]: ) -> tuple[list[str], list[str]]:
"""Emit every library's compile and archive edges. """Emit every library's compile and archive edges.
``extra_flags`` (raw ninja text) precede each library's own flags.
Returns the archive names and the objects that link directly. A Returns the archive names and the objects that link directly. A
library's own include dirs lead its compile lines, as PlatformIO searched library's own include dirs lead its compile lines, as PlatformIO searched
them first: the include path is one global list, so another library's them first: the include path is one global list, so another library's
@@ -241,7 +232,7 @@ def library_edges(
lib.sources, lib.sources,
common_parent(lib.sources), common_parent(lib.sources),
f"lib/{lib.name}", f"lib/{lib.name}",
flags=" ".join(filter(None, [extra_flags, *map(_shell_token, lib.flags)])), flags=" ".join(_shell_token(f) for f in lib.flags),
own_includes=" ".join(f"-I{_q(d)}" for d in lib.include_dirs), own_includes=" ".join(f"-I{_q(d)}" for d in lib.include_dirs),
) )
if not lib.lib_archive: if not lib.lib_archive:
+12 -20
View File
@@ -102,21 +102,25 @@ _KNOWN_FILE_EXTENSIONS = frozenset(
) )
# Matches !secret references in YAML text. An optional surrounding
# quote pair around the key is allowed and ignored: YAML treats
# ``!secret 'foo'`` and ``!secret foo`` as the same key. This is
# intentionally a simple regex scan rather than a YAML parse — it may
# match inside comments or multi-line strings, which is the conservative
# direction (include more secrets rather than fewer).
_SECRET_RE = re.compile(r"""!secret\s+['"]?([^\s'"]+)""")
def _find_used_secret_keys(yaml_files: list[Path]) -> set[str]: def _find_used_secret_keys(yaml_files: list[Path]) -> set[str]:
"""Scan YAML files for ``!secret <key>`` references.""" """Scan YAML files for ``!secret <key>`` references."""
keys: set[str] = set() keys: set[str] = set()
for fpath in yaml_files: for fpath in yaml_files:
try: try:
text = fpath.read_text(encoding="utf-8") text = fpath.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError) as err: except (OSError, UnicodeDecodeError):
_LOGGER.warning(
"Could not scan %s for !secret references (%s); the bundled "
"secret set may be incomplete",
fpath,
err,
)
continue continue
keys |= yaml_util.find_secret_references(text) for match in _SECRET_RE.finditer(text):
keys.add(match.group(1))
return keys return keys
@@ -418,18 +422,6 @@ class ConfigBundleCreator:
must ship every candidate so the remote build can pick any one. must ship every candidate so the remote build can pick any one.
""" """
discovered = yaml_util.discover_user_yaml_files(self._config_path) discovered = yaml_util.discover_user_yaml_files(self._config_path)
if discovered.load_errors:
_LOGGER.warning(
"Bundle may be incomplete; could not load all configuration files: %s",
"; ".join(discovered.load_errors),
)
if discovered.unresolved:
_LOGGER.warning(
"Bundle may be incomplete; %d !include path(s) use "
"substitutions and cannot be captured: %s",
len(discovered.unresolved),
", ".join(discovered.unresolved),
)
self._secrets_paths.update(discovered.secrets) self._secrets_paths.update(discovered.secrets)
# A !secret inside a file this re-parse does not reach (for example # A !secret inside a file this re-parse does not reach (for example
# a git-fetched package the builder re-fetches) still resolves # a git-fetched package the builder re-fetches) still resolves
-3
View File
@@ -34,17 +34,14 @@ from esphome.cpp_generator import ( # noqa: F401
extern_progmem_array, extern_progmem_array,
get_variable, get_variable,
get_variable_with_full_id, get_variable_with_full_id,
is_static_pointer,
is_template, is_template,
new_Pvariable, new_Pvariable,
new_variable, new_variable,
process_lambda, process_lambda,
progmem_array, progmem_array,
progmem_string,
safe_exp, safe_exp,
set_cpp_standard, set_cpp_standard,
shared_progmem_array, shared_progmem_array,
shared_progmem_string,
statement, statement,
static_const_array, static_const_array,
static_function, static_function,
-1
View File
@@ -1,2 +1 @@
CODEOWNERS = ["@MrSuicideParrot"] CODEOWNERS = ["@MrSuicideParrot"]
DOMAIN = "a01nyub"
+1 -1
View File
@@ -6,7 +6,7 @@
namespace esphome::a01nyub { namespace esphome::a01nyub {
ESPHOME_LOG_TAG(TAG, "a01nyub.sensor"); static const char *const TAG = "a01nyub.sensor";
void A01nyubComponent::loop() { void A01nyubComponent::loop() {
uint8_t data; uint8_t data;
-1
View File
@@ -1,2 +1 @@
CODEOWNERS = ["@TH-Braemer"] CODEOWNERS = ["@TH-Braemer"]
DOMAIN = "a02yyuw"
+1 -1
View File
@@ -6,7 +6,7 @@
namespace esphome::a02yyuw { namespace esphome::a02yyuw {
ESPHOME_LOG_TAG(TAG, "a02yyuw.sensor"); static const char *const TAG = "a02yyuw.sensor";
void A02yyuwComponent::loop() { void A02yyuwComponent::loop() {
uint8_t data; uint8_t data;
-1
View File
@@ -1 +0,0 @@
DOMAIN = "a4988"
+1 -1
View File
@@ -3,7 +3,7 @@
namespace esphome::a4988 { namespace esphome::a4988 {
ESPHOME_LOG_TAG(TAG, "a4988.stepper"); static const char *const TAG = "a4988.stepper";
void A4988::setup() { void A4988::setup() {
if (this->sleep_pin_ != nullptr) { if (this->sleep_pin_ != nullptr) {
@@ -1,2 +1 @@
CODEOWNERS = ["@DAVe3283"] CODEOWNERS = ["@DAVe3283"]
DOMAIN = "absolute_humidity"
@@ -3,7 +3,7 @@
namespace esphome::absolute_humidity { namespace esphome::absolute_humidity {
ESPHOME_LOG_TAG(TAG, "absolute_humidity.sensor"); static const char *const TAG{"absolute_humidity.sensor"};
void AbsoluteHumidityComponent::setup() { void AbsoluteHumidityComponent::setup() {
this->temperature_sensor_->add_on_state_callback([this](float state) { this->temperature_sensor_->add_on_state_callback([this](float state) {
-1
View File
@@ -1 +0,0 @@
DOMAIN = "ac_dimmer"
+1 -1
View File
@@ -14,7 +14,7 @@
namespace esphome::ac_dimmer { namespace esphome::ac_dimmer {
ESPHOME_LOG_TAG(TAG, "ac_dimmer"); static const char *const TAG = "ac_dimmer";
// Global array to store dimmer objects // Global array to store dimmer objects
static AcDimmerDataStore *all_dimmers[32]; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static AcDimmerDataStore *all_dimmers[32]; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
@@ -9,7 +9,7 @@
#include "esp_clk_tree.h" #include "esp_clk_tree.h"
#include "soc/clk_tree_defs.h" #include "soc/clk_tree_defs.h"
ESPHOME_LOG_TAG(TAG, "hw_timer_esp_idf"); static const char *const TAG = "hw_timer_esp_idf";
namespace esphome::ac_dimmer { namespace esphome::ac_dimmer {
-1
View File
@@ -9,7 +9,6 @@ from esphome.cpp_generator import MockObj
from esphome.types import ConfigType from esphome.types import ConfigType
DEPENDENCIES = ["uart"] DEPENDENCIES = ["uart"]
DOMAIN = "adalight"
adalight_ns = cg.esphome_ns.namespace("adalight") adalight_ns = cg.esphome_ns.namespace("adalight")
AdalightLightEffect = adalight_ns.class_( AdalightLightEffect = adalight_ns.class_(
@@ -3,7 +3,7 @@
namespace esphome::adalight { namespace esphome::adalight {
ESPHOME_LOG_TAG(TAG, "adalight_light_effect"); static const char *const TAG = "adalight_light_effect";
static const uint32_t ADALIGHT_ACK_INTERVAL = 1000; static const uint32_t ADALIGHT_ACK_INTERVAL = 1000;
static const uint32_t ADALIGHT_RECEIVE_TIMEOUT = 1000; static const uint32_t ADALIGHT_RECEIVE_TIMEOUT = 1000;
-1
View File
@@ -22,7 +22,6 @@ from esphome.core import CORE
from esphome.types import ConfigType from esphome.types import ConfigType
CODEOWNERS = ["@esphome/core"] CODEOWNERS = ["@esphome/core"]
DOMAIN = "adc"
adc_ns = cg.esphome_ns.namespace("adc") adc_ns = cg.esphome_ns.namespace("adc")
+4 -4
View File
@@ -128,10 +128,10 @@ class ADCSensor final : public sensor::Sensor, public PollingComponent, public v
#endif // USE_RP2 #endif // USE_RP2
protected: protected:
uint8_t sample_count_{1}; // Must match DEFAULT_SAMPLES in sensor.py uint8_t sample_count_{1};
bool output_raw_{false}; // Must match CONF_RAW default in sensor.py bool output_raw_{false};
InternalGPIOPin *pin_; InternalGPIOPin *pin_;
SamplingMode sampling_mode_{SamplingMode::AVG}; // Must match DEFAULT_SAMPLING_MODE in sensor.py SamplingMode sampling_mode_{SamplingMode::AVG};
#ifdef USE_ESP32 #ifdef USE_ESP32
float sample_autorange_(); float sample_autorange_();
@@ -139,7 +139,7 @@ class ADCSensor final : public sensor::Sensor, public PollingComponent, public v
bool autorange_{false}; bool autorange_{false};
adc_oneshot_unit_handle_t adc_handle_{nullptr}; adc_oneshot_unit_handle_t adc_handle_{nullptr};
adc_cali_handle_t calibration_handle_{nullptr}; adc_cali_handle_t calibration_handle_{nullptr};
adc_atten_t attenuation_{ADC_ATTEN_DB_0}; // Must match DEFAULT_ATTENUATION in sensor.py adc_atten_t attenuation_{ADC_ATTEN_DB_0};
adc_channel_t channel_{}; adc_channel_t channel_{};
adc_unit_t adc_unit_{}; adc_unit_t adc_unit_{};
struct SetupFlags { struct SetupFlags {
+1 -1
View File
@@ -3,7 +3,7 @@
namespace esphome::adc { namespace esphome::adc {
ESPHOME_LOG_TAG(TAG, "adc"); static const char *const TAG = "adc";
const LogString *sampling_mode_to_str(SamplingMode mode) { const LogString *sampling_mode_to_str(SamplingMode mode) {
switch (mode) { switch (mode) {
+1 -1
View File
@@ -6,7 +6,7 @@
namespace esphome::adc { namespace esphome::adc {
ESPHOME_LOG_TAG(TAG, "adc"); static const char *const TAG = "adc";
adc_oneshot_unit_handle_t ADCSensor::shared_adc_handles[2] = {nullptr, nullptr}; adc_oneshot_unit_handle_t ADCSensor::shared_adc_handles[2] = {nullptr, nullptr};
@@ -13,7 +13,7 @@ ADC_MODE(ADC_VCC)
namespace esphome::adc { namespace esphome::adc {
ESPHOME_LOG_TAG(TAG, "adc"); static const char *const TAG = "adc";
void ADCSensor::setup() { void ADCSensor::setup() {
#ifndef USE_ADC_SENSOR_VCC #ifndef USE_ADC_SENSOR_VCC
@@ -5,7 +5,7 @@
namespace esphome::adc { namespace esphome::adc {
ESPHOME_LOG_TAG(TAG, "adc"); static const char *const TAG = "adc";
void ADCSensor::setup() { void ADCSensor::setup() {
#ifndef USE_ADC_SENSOR_VCC #ifndef USE_ADC_SENSOR_VCC
+1 -1
View File
@@ -17,7 +17,7 @@
namespace esphome::adc { namespace esphome::adc {
ESPHOME_LOG_TAG(TAG, "adc"); static const char *const TAG = "adc";
// The on-die temperature sensor sits on the last ADC channel: input 4 on RP2040 // The on-die temperature sensor sits on the last ADC channel: input 4 on RP2040
// and RP2350A, but input 8 on RP2350B, which has eight external channels rather // and RP2350A, but input 8 on RP2350B, which has eight external channels rather
+1 -1
View File
@@ -7,7 +7,7 @@
namespace esphome::adc { namespace esphome::adc {
ESPHOME_LOG_TAG(TAG, "adc"); static const char *const TAG = "adc";
void ADCSensor::setup() { void ADCSensor::setup() {
if (!adc_is_ready_dt(this->channel_)) { if (!adc_is_ready_dt(this->channel_)) {
+8 -24
View File
@@ -48,11 +48,6 @@ AUTO_LOAD = ["voltage_sampler"]
CONF_SAMPLES = "samples" CONF_SAMPLES = "samples"
CONF_SAMPLING_MODE = "sampling_mode" CONF_SAMPLING_MODE = "sampling_mode"
# Keep in sync with the initializers in adc_sensor.h
DEFAULT_ATTENUATION = "0db"
DEFAULT_SAMPLING_MODE = "avg"
DEFAULT_SAMPLES = 1
_attenuation = cv.enum(ATTENUATION_MODES, lower=True) _attenuation = cv.enum(ATTENUATION_MODES, lower=True)
_sampling_mode = cv.enum(SAMPLING_MODES, lower=True) _sampling_mode = cv.enum(SAMPLING_MODES, lower=True)
@@ -70,10 +65,7 @@ def validate_config(config: ConfigType) -> ConfigType:
): ):
raise cv.Invalid("ESP32-S31 only supports 'attenuation: 0db'") raise cv.Invalid("ESP32-S31 only supports 'attenuation: 0db'")
if ( if config.get(CONF_ATTENUATION, None) == "auto" and config.get(CONF_SAMPLES, 1) > 1:
config.get(CONF_ATTENUATION, None) == "auto"
and config.get(CONF_SAMPLES, DEFAULT_SAMPLES) > 1
):
raise cv.Invalid( raise cv.Invalid(
"Automatic attenuation cannot be used when multisampling is set" "Automatic attenuation cannot be used when multisampling is set"
) )
@@ -121,16 +113,12 @@ CONFIG_SCHEMA = cv.All(
{ {
cv.Required(CONF_PIN): validate_adc_pin, cv.Required(CONF_PIN): validate_adc_pin,
cv.Optional(CONF_RAW, default=False): cv.boolean, cv.Optional(CONF_RAW, default=False): cv.boolean,
cv.SplitDefault(CONF_ATTENUATION, esp32=DEFAULT_ATTENUATION): cv.All( cv.SplitDefault(CONF_ATTENUATION, esp32="0db"): cv.All(
cv.only_on_esp32, _attenuation cv.only_on_esp32, _attenuation
), ),
cv.OnlyWith(CONF_NRF_SAADC, PLATFORM_NRF52): cv.declare_id(adc_dt_spec), cv.OnlyWith(CONF_NRF_SAADC, PLATFORM_NRF52): cv.declare_id(adc_dt_spec),
cv.Optional(CONF_SAMPLES, default=DEFAULT_SAMPLES): cv.int_range( cv.Optional(CONF_SAMPLES, default=1): cv.int_range(min=1, max=255),
min=1, max=255 cv.Optional(CONF_SAMPLING_MODE, default="avg"): _sampling_mode,
),
cv.Optional(
CONF_SAMPLING_MODE, default=DEFAULT_SAMPLING_MODE
): _sampling_mode,
} }
) )
.extend(cv.polling_component_schema("60s")), .extend(cv.polling_component_schema("60s")),
@@ -167,13 +155,9 @@ async def to_code(config: ConfigType) -> None:
pin = await cg.gpio_pin_expression(config[CONF_PIN]) pin = await cg.gpio_pin_expression(config[CONF_PIN])
cg.add(var.set_pin(pin)) cg.add(var.set_pin(pin))
# Skip the setters when the config matches the C++ initializers. cg.add(var.set_output_raw(config[CONF_RAW]))
if config[CONF_RAW]: cg.add(var.set_sample_count(config[CONF_SAMPLES]))
cg.add(var.set_output_raw(True)) cg.add(var.set_sampling_mode(config[CONF_SAMPLING_MODE]))
if (samples := config[CONF_SAMPLES]) != DEFAULT_SAMPLES:
cg.add(var.set_sample_count(samples))
if (sampling_mode := config[CONF_SAMPLING_MODE]) != DEFAULT_SAMPLING_MODE:
cg.add(var.set_sampling_mode(sampling_mode))
if CORE.is_esp32: if CORE.is_esp32:
# Re-enable ESP-IDF's ADC driver (excluded by default to save compile time) # Re-enable ESP-IDF's ADC driver (excluded by default to save compile time)
@@ -182,7 +166,7 @@ async def to_code(config: ConfigType) -> None:
if attenuation := config.get(CONF_ATTENUATION): if attenuation := config.get(CONF_ATTENUATION):
if attenuation == "auto": if attenuation == "auto":
cg.add(var.set_autorange(cg.global_ns.true)) cg.add(var.set_autorange(cg.global_ns.true))
elif attenuation != DEFAULT_ATTENUATION: else:
cg.add(var.set_attenuation(attenuation)) cg.add(var.set_attenuation(attenuation))
variant = get_esp32_variant() variant = get_esp32_variant()
@@ -5,7 +5,6 @@ from esphome.const import CONF_ID
from esphome.types import ConfigType from esphome.types import ConfigType
DEPENDENCIES = ["spi"] DEPENDENCIES = ["spi"]
DOMAIN = "adc128s102"
MULTI_CONF = True MULTI_CONF = True
CODEOWNERS = ["@DeerMaximum"] CODEOWNERS = ["@DeerMaximum"]
+1 -1
View File
@@ -3,7 +3,7 @@
namespace esphome::adc128s102 { namespace esphome::adc128s102 {
ESPHOME_LOG_TAG(TAG, "adc128s102"); static const char *const TAG = "adc128s102";
float ADC128S102::get_setup_priority() const { return setup_priority::HARDWARE; } float ADC128S102::get_setup_priority() const { return setup_priority::HARDWARE; }
@@ -4,7 +4,7 @@
namespace esphome::adc128s102 { namespace esphome::adc128s102 {
ESPHOME_LOG_TAG(TAG, "adc128s102.sensor"); static const char *const TAG = "adc128s102.sensor";
ADC128S102Sensor::ADC128S102Sensor(uint8_t channel) : channel_(channel) {} ADC128S102Sensor::ADC128S102Sensor(uint8_t channel) : channel_(channel) {}
@@ -1 +0,0 @@
DOMAIN = "addressable_light"
@@ -3,7 +3,7 @@
namespace esphome::addressable_light { namespace esphome::addressable_light {
ESPHOME_LOG_TAG(TAG, "addressable_light.display"); static const char *const TAG = "addressable_light.display";
int AddressableLightDisplay::get_width_internal() { return this->width_; } int AddressableLightDisplay::get_width_internal() { return this->width_; }
int AddressableLightDisplay::get_height_internal() { return this->height_; } int AddressableLightDisplay::get_height_internal() { return this->height_; }
-1
View File
@@ -1 +0,0 @@
DOMAIN = "ade7880"
+1 -1
View File
@@ -15,7 +15,7 @@
namespace esphome::ade7880 { namespace esphome::ade7880 {
ESPHOME_LOG_TAG(TAG, "ade7880"); static const char *const TAG = "ade7880";
void IRAM_ATTR ADE7880Store::gpio_intr(ADE7880Store *arg) { arg->reset_done = true; } void IRAM_ATTR ADE7880Store::gpio_intr(ADE7880Store *arg) { arg->reset_done = true; }
-1
View File
@@ -1,2 +1 @@
CODEOWNERS = ["@angelnu"] CODEOWNERS = ["@angelnu"]
DOMAIN = "ade7953"
@@ -27,7 +27,6 @@ from esphome.cpp_generator import MockObj
from esphome.types import ConfigType from esphome.types import ConfigType
CODEOWNERS = ["@angelnu"] CODEOWNERS = ["@angelnu"]
DOMAIN = "ade7953_base"
CONF_CURRENT_A = "current_a" CONF_CURRENT_A = "current_a"
CONF_CURRENT_B = "current_b" CONF_CURRENT_B = "current_b"
@@ -5,7 +5,7 @@
namespace esphome::ade7953_base { namespace esphome::ade7953_base {
ESPHOME_LOG_TAG(TAG, "ade7953"); static const char *const TAG = "ade7953";
constexpr uint16_t CONFIG_DEFAULT = 0x8004u; constexpr uint16_t CONFIG_DEFAULT = 0x8004u;
constexpr uint16_t CONFIG_LOCK_BIT = 0x8000u; constexpr uint16_t CONFIG_LOCK_BIT = 0x8000u;
@@ -1,2 +1 @@
CODEOWNERS = ["@angelnu"] CODEOWNERS = ["@angelnu"]
DOMAIN = "ade7953_i2c"
@@ -4,7 +4,7 @@
namespace esphome::ade7953_i2c { namespace esphome::ade7953_i2c {
ESPHOME_LOG_TAG(TAG, "ade7953"); static const char *const TAG = "ade7953";
void AdE7953I2c::dump_config() { void AdE7953I2c::dump_config() {
ESP_LOGCONFIG(TAG, "ADE7953_i2c:"); ESP_LOGCONFIG(TAG, "ADE7953_i2c:");
@@ -1,2 +1 @@
CODEOWNERS = ["@angelnu"] CODEOWNERS = ["@angelnu"]
DOMAIN = "ade7953_spi"
@@ -4,7 +4,7 @@
namespace esphome::ade7953_spi { namespace esphome::ade7953_spi {
ESPHOME_LOG_TAG(TAG, "ade7953"); static const char *const TAG = "ade7953";
// Datasheet requires at least 1.2µs after clearing CONFIG LOCK_BIT before raising CS // Datasheet requires at least 1.2µs after clearing CONFIG LOCK_BIT before raising CS
constexpr uint8_t CONFIG_LOCK_SETTLE_US = 2; constexpr uint8_t CONFIG_LOCK_SETTLE_US = 2;
-1
View File
@@ -5,7 +5,6 @@ from esphome.const import CONF_ID
from esphome.types import ConfigType from esphome.types import ConfigType
DEPENDENCIES = ["i2c"] DEPENDENCIES = ["i2c"]
DOMAIN = "ads1115"
MULTI_CONF = True MULTI_CONF = True
ads1115_ns = cg.esphome_ns.namespace("ads1115") ads1115_ns = cg.esphome_ns.namespace("ads1115")
+1 -1
View File
@@ -4,7 +4,7 @@
namespace esphome::ads1115 { namespace esphome::ads1115 {
ESPHOME_LOG_TAG(TAG, "ads1115"); static const char *const TAG = "ads1115";
static const uint8_t ADS1115_REGISTER_CONVERSION = 0x00; static const uint8_t ADS1115_REGISTER_CONVERSION = 0x00;
static const uint8_t ADS1115_REGISTER_CONFIG = 0x01; static const uint8_t ADS1115_REGISTER_CONFIG = 0x01;
@@ -4,7 +4,7 @@
namespace esphome::ads1115 { namespace esphome::ads1115 {
ESPHOME_LOG_TAG(TAG, "ads1115.sensor"); static const char *const TAG = "ads1115.sensor";
float ADS1115Sensor::sample() { float ADS1115Sensor::sample() {
return this->parent_->request_measurement(this->multiplexer_, this->gain_, this->resolution_, this->samplerate_); return this->parent_->request_measurement(this->multiplexer_, this->gain_, this->resolution_, this->samplerate_);
-1
View File
@@ -6,7 +6,6 @@ from esphome.types import ConfigType
CODEOWNERS = ["@solomondg1"] CODEOWNERS = ["@solomondg1"]
DEPENDENCIES = ["spi"] DEPENDENCIES = ["spi"]
DOMAIN = "ads1118"
MULTI_CONF = True MULTI_CONF = True
CONF_ADS1118_ID = "ads1118_id" CONF_ADS1118_ID = "ads1118_id"
+1 -1
View File
@@ -4,7 +4,7 @@
namespace esphome::ads1118 { namespace esphome::ads1118 {
ESPHOME_LOG_TAG(TAG, "ads1118"); static const char *const TAG = "ads1118";
static const uint8_t ADS1118_DATA_RATE_860_SPS = 0b111; static const uint8_t ADS1118_DATA_RATE_860_SPS = 0b111;
void ADS1118::setup() { void ADS1118::setup() {
@@ -4,7 +4,7 @@
namespace esphome::ads1118 { namespace esphome::ads1118 {
ESPHOME_LOG_TAG(TAG, "ads1118.sensor"); static const char *const TAG = "ads1118.sensor";
void ADS1118Sensor::dump_config() { void ADS1118Sensor::dump_config() {
LOG_SENSOR(" ", "ADS1118 Sensor", this); LOG_SENSOR(" ", "ADS1118 Sensor", this);
-1
View File
@@ -1,2 +1 @@
CODEOWNERS = ["@mak-42"] CODEOWNERS = ["@mak-42"]
DOMAIN = "ags10"
+1 -1
View File
@@ -4,7 +4,7 @@
#include <cinttypes> #include <cinttypes>
namespace esphome::ags10 { namespace esphome::ags10 {
ESPHOME_LOG_TAG(TAG, "ags10"); static const char *const TAG = "ags10";
// Data acquisition. // Data acquisition.
static const uint8_t REG_TVOC = 0x00; static const uint8_t REG_TVOC = 0x00;
-1
View File
@@ -1 +0,0 @@
DOMAIN = "aht10"
+1 -1
View File
@@ -19,7 +19,7 @@
namespace esphome::aht10 { namespace esphome::aht10 {
ESPHOME_LOG_TAG(TAG, "aht10"); static const char *const TAG = "aht10";
static const uint8_t AHT10_INITIALIZE_CMD[] = {0xE1, 0x08, 0x00}; static const uint8_t AHT10_INITIALIZE_CMD[] = {0xE1, 0x08, 0x00};
static const uint8_t AHT20_INITIALIZE_CMD[] = {0xBE, 0x08, 0x00}; static const uint8_t AHT20_INITIALIZE_CMD[] = {0xBE, 0x08, 0x00};
static const uint8_t AHT10_MEASURE_CMD[] = {0xAC, 0x33, 0x00}; static const uint8_t AHT10_MEASURE_CMD[] = {0xAC, 0x33, 0x00};
-1
View File
@@ -1 +0,0 @@
DOMAIN = "aic3204"
+1 -1
View File
@@ -6,7 +6,7 @@
namespace esphome::aic3204 { namespace esphome::aic3204 {
ESPHOME_LOG_TAG(TAG, "aic3204"); static const char *const TAG = "aic3204";
#define ERROR_CHECK(err, msg) \ #define ERROR_CHECK(err, msg) \
if (!(err)) { \ if (!(err)) { \
@@ -6,7 +6,6 @@ from esphome.types import ConfigType
AUTO_LOAD = ["ble_device_base"] AUTO_LOAD = ["ble_device_base"]
CODEOWNERS = ["@jeromelaban"] CODEOWNERS = ["@jeromelaban"]
DOMAIN = "airthings_ble"
airthings_ble_ns = cg.esphome_ns.namespace("airthings_ble") airthings_ble_ns = cg.esphome_ns.namespace("airthings_ble")
AirthingsListener = airthings_ble_ns.class_( AirthingsListener = airthings_ble_ns.class_(
@@ -4,7 +4,7 @@
namespace esphome::airthings_ble { namespace esphome::airthings_ble {
ESPHOME_LOG_TAG(TAG, "airthings_ble"); static const char *const TAG = "airthings_ble";
bool AirthingsListener::parse_device(const ble_device_base::ESPBTDevice &device) { bool AirthingsListener::parse_device(const ble_device_base::ESPBTDevice &device) {
for (auto &it : device.get_manufacturer_datas()) { for (auto &it : device.get_manufacturer_datas()) {
@@ -24,7 +24,6 @@ from esphome.cpp_generator import MockObj
from esphome.types import ConfigType from esphome.types import ConfigType
CODEOWNERS = ["@ncareau", "@jeromelaban"] CODEOWNERS = ["@ncareau", "@jeromelaban"]
DOMAIN = "airthings_wave_base"
DEPENDENCIES = ["ble_client"] DEPENDENCIES = ["ble_client"]
@@ -8,10 +8,7 @@
namespace esphome::airthings_wave_base { namespace esphome::airthings_wave_base {
ESPHOME_LOG_TAG(TAG, "airthings_wave_base"); static const char *const TAG = "airthings_wave_base";
static constexpr uint32_t BATTERY_READ_TIMEOUT_ID = 0;
static constexpr uint32_t RESPONSE_TIMEOUT_ID = 1;
void AirthingsWaveBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, void AirthingsWaveBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) { esp_ble_gattc_cb_param_t *param) {
@@ -184,7 +181,7 @@ void AirthingsWaveBase::read_battery_(uint8_t *raw_value, uint16_t value_len) {
// read the battery again at the configured update interval // read the battery again at the configured update interval
if (this->battery_update_interval_ != this->update_interval_) { if (this->battery_update_interval_ != this->update_interval_) {
this->read_battery_next_update_ = false; this->read_battery_next_update_ = false;
this->set_timeout(BATTERY_READ_TIMEOUT_ID, this->battery_update_interval_, this->set_timeout("battery", this->battery_update_interval_,
[this]() { this->read_battery_next_update_ = true; }); [this]() { this->read_battery_next_update_ = true; });
} }
} }
@@ -207,7 +204,7 @@ void AirthingsWaveBase::response_received_() {
} }
void AirthingsWaveBase::set_response_timeout_() { void AirthingsWaveBase::set_response_timeout_() {
this->set_timeout(RESPONSE_TIMEOUT_ID, 30 * 1000, [this]() { this->set_timeout("response_timeout", 30 * 1000, [this]() {
this->responses_pending_ = 1; this->responses_pending_ = 1;
this->response_received_(); this->response_received_();
}); });
@@ -1,2 +1 @@
CODEOWNERS = ["@ncareau"] CODEOWNERS = ["@ncareau"]
DOMAIN = "airthings_wave_mini"
@@ -4,7 +4,7 @@
namespace esphome::airthings_wave_mini { namespace esphome::airthings_wave_mini {
ESPHOME_LOG_TAG(TAG, "airthings_wave_mini"); static const char *const TAG = "airthings_wave_mini";
void AirthingsWaveMini::read_sensors(uint8_t *raw_value, uint16_t value_len) { void AirthingsWaveMini::read_sensors(uint8_t *raw_value, uint16_t value_len) {
auto *value = (WaveMiniReadings *) raw_value; auto *value = (WaveMiniReadings *) raw_value;
@@ -1,2 +1 @@
CODEOWNERS = ["@jeromelaban", "@precurse"] CODEOWNERS = ["@jeromelaban", "@precurse"]
DOMAIN = "airthings_wave_plus"
@@ -4,7 +4,7 @@
namespace esphome::airthings_wave_plus { namespace esphome::airthings_wave_plus {
ESPHOME_LOG_TAG(TAG, "airthings_wave_plus"); static const char *const TAG = "airthings_wave_plus";
void AirthingsWavePlus::read_sensors(uint8_t *raw_value, uint16_t value_len) { void AirthingsWavePlus::read_sensors(uint8_t *raw_value, uint16_t value_len) {
auto *value = (WavePlusReadings *) raw_value; auto *value = (WavePlusReadings *) raw_value;
@@ -21,7 +21,6 @@ from esphome.core.entity_helpers import (
from esphome.cpp_generator import MockObjClass from esphome.cpp_generator import MockObjClass
CODEOWNERS = ["@grahambrown11", "@hwstar"] CODEOWNERS = ["@grahambrown11", "@hwstar"]
DOMAIN = "alarm_control_panel"
IS_PLATFORM_COMPONENT = True IS_PLATFORM_COMPONENT = True
CONF_ON_TRIGGERED = "on_triggered" CONF_ON_TRIGGERED = "on_triggered"
@@ -10,7 +10,7 @@
namespace esphome::alarm_control_panel { namespace esphome::alarm_control_panel {
ESPHOME_LOG_TAG(TAG, "alarm_control_panel"); static const char *const TAG = "alarm_control_panel";
AlarmControlPanelCall AlarmControlPanel::make_call() { return AlarmControlPanelCall(this); } AlarmControlPanelCall AlarmControlPanel::make_call() { return AlarmControlPanelCall(this); }
@@ -6,7 +6,7 @@
namespace esphome::alarm_control_panel { namespace esphome::alarm_control_panel {
ESPHOME_LOG_TAG(TAG, "alarm_control_panel"); static const char *const TAG = "alarm_control_panel";
AlarmControlPanelCall::AlarmControlPanelCall(AlarmControlPanel *parent) : parent_(parent) {} AlarmControlPanelCall::AlarmControlPanelCall(AlarmControlPanel *parent) : parent_(parent) {}
-1
View File
@@ -1,2 +1 @@
CODEOWNERS = ["@jan-hofmeier"] CODEOWNERS = ["@jan-hofmeier"]
DOMAIN = "alpha3"
+1 -1
View File
@@ -7,7 +7,7 @@
namespace esphome::alpha3 { namespace esphome::alpha3 {
ESPHOME_LOG_TAG(TAG, "alpha3"); static const char *const TAG = "alpha3";
void Alpha3::dump_config() { void Alpha3::dump_config() {
ESP_LOGCONFIG(TAG, "ALPHA3"); ESP_LOGCONFIG(TAG, "ALPHA3");
-1
View File
@@ -1,2 +1 @@
CODEOWNERS = ["@swoboda1337"] CODEOWNERS = ["@swoboda1337"]
DOMAIN = "am2315c"
+1 -1
View File
@@ -26,7 +26,7 @@
namespace esphome::am2315c { namespace esphome::am2315c {
ESPHOME_LOG_TAG(TAG, "am2315c"); static const char *const TAG = "am2315c";
bool AM2315C::reset_register_(uint8_t reg) { bool AM2315C::reset_register_(uint8_t reg) {
// code based on demo code sent by www.aosong.com // code based on demo code sent by www.aosong.com
-1
View File
@@ -1 +0,0 @@
DOMAIN = "am2320"
+1 -1
View File
@@ -10,7 +10,7 @@
namespace esphome::am2320 { namespace esphome::am2320 {
ESPHOME_LOG_TAG(TAG, "am2320"); static const char *const TAG = "am2320";
void AM2320Component::update() { void AM2320Component::update() {
uint8_t data[8]; uint8_t data[8];
-1
View File
@@ -1,2 +1 @@
CODEOWNERS = ["@buxtronix"] CODEOWNERS = ["@buxtronix"]
DOMAIN = "am43"
+1 -1
View File
@@ -5,7 +5,7 @@
namespace esphome::am43 { namespace esphome::am43 {
ESPHOME_LOG_TAG(TAG, "am43_cover"); static const char *const TAG = "am43_cover";
using namespace esphome::cover; using namespace esphome::cover;
@@ -6,7 +6,7 @@
namespace esphome::am43 { namespace esphome::am43 {
ESPHOME_LOG_TAG(TAG, "am43"); static const char *const TAG = "am43";
void Am43::dump_config() { void Am43::dump_config() {
ESP_LOGCONFIG(TAG, "AM43"); ESP_LOGCONFIG(TAG, "AM43");
@@ -1,2 +1 @@
CODEOWNERS = ["@ianchi"] CODEOWNERS = ["@ianchi"]
DOMAIN = "analog_threshold"
@@ -3,7 +3,7 @@
namespace esphome::analog_threshold { namespace esphome::analog_threshold {
ESPHOME_LOG_TAG(TAG, "analog_threshold.binary_sensor"); static const char *const TAG = "analog_threshold.binary_sensor";
void AnalogThresholdBinarySensor::setup() { void AnalogThresholdBinarySensor::setup() {
float sensor_value = this->sensor_->get_state(); float sensor_value = this->sensor_->get_state();
-1
View File
@@ -1 +0,0 @@
DOMAIN = "anova"
+1 -1
View File
@@ -5,7 +5,7 @@
namespace esphome::anova { namespace esphome::anova {
ESPHOME_LOG_TAG(TAG, "anova"); static const char *const TAG = "anova";
using namespace esphome::climate; using namespace esphome::climate;
-3
View File
@@ -1,3 +0,0 @@
"""Support for ScioSense APC1 air quality sensor."""
DOMAIN = "apc1"
-352
View File
@@ -1,352 +0,0 @@
#include "apc1.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include <cinttypes>
#include <cmath>
namespace esphome::apc1 {
ESPHOME_LOG_TAG(TAG, "apc1");
static constexpr uint8_t APC1_MEASUREMENT_OFFSET_AQI = 58;
static constexpr uint8_t APC1_MEASUREMENT_OFFSET_ERROR_CODE = 61;
static constexpr uint8_t APC1_DEVICE_INFO_OFFSET_MODULE = 4;
static constexpr uint8_t APC1_DEVICE_INFO_OFFSET_SERIAL = 10;
static constexpr uint8_t APC1_DEVICE_INFO_OFFSET_FW = 19;
// Give a freshly powered sensor time to boot before the first retry
static constexpr uint32_t APC1_COLD_BOOT_INIT_DELAY_MS = 2000;
// Re-send the init commands at most this often while no frames arrive
static constexpr uint32_t APC1_INIT_RETRY_INTERVAL_MS = 10000;
// Active mode streams a frame every second; this long without one means the sensor stopped
static constexpr uint32_t APC1_NO_DATA_TIMEOUT_MS = 15000;
// Drop a partial frame when the bytes stop mid-way
static constexpr uint32_t APC1_RX_IDLE_TIMEOUT_MS = 200;
// Frames and commands end with the 16-bit sum of every byte before it
static uint16_t apc1_checksum(const uint8_t *data, size_t len) {
uint16_t sum = 0;
for (size_t i = 0; i < len; i++) {
sum += data[i];
}
return sum;
}
void APC1Component::setup() {
if (this->set_pin_ != nullptr) {
this->set_pin_->setup();
this->set_pin_->digital_write(true);
}
if (this->reset_pin_ != nullptr) {
this->reset_pin_->setup();
this->reset_pin_->digital_write(true);
}
// Attempt initial communication immediately for warm restarts
this->initialize_device_();
// Schedule delayed initialization to catch sensors during cold power-on bootup
this->set_timeout(APC1_COLD_BOOT_INIT_DELAY_MS, [this]() { this->initialize_device_(); });
}
void APC1Component::initialize_device_() {
this->last_init_attempt_ = App.get_loop_component_start_time();
if (this->active_mode_ && !this->idle_) {
this->set_active_mode();
}
this->send_command_(APC1Command::APC1_COMMAND_READ_SENSOR_VERSION, 0x0000);
}
void APC1Component::dump_config() {
ESP_LOGCONFIG(TAG, "APC1:");
LOG_PIN(" Set Pin: ", this->set_pin_);
LOG_PIN(" Reset Pin: ", this->reset_pin_);
LOG_SENSOR(" ", "PM1.0", this->pm_1_0_sensor_);
LOG_SENSOR(" ", "PM2.5", this->pm_2_5_sensor_);
LOG_SENSOR(" ", "PM10.0", this->pm_10_0_sensor_);
LOG_SENSOR(" ", "PM1.0 Standard", this->pm_1_0_std_sensor_);
LOG_SENSOR(" ", "PM2.5 Standard", this->pm_2_5_std_sensor_);
LOG_SENSOR(" ", "PM10.0 Standard", this->pm_10_0_std_sensor_);
LOG_SENSOR(" ", "PM >0.3um", this->pm_0_3um_sensor_);
LOG_SENSOR(" ", "PM >0.5um", this->pm_0_5um_sensor_);
LOG_SENSOR(" ", "PM >1.0um", this->pm_1_0um_sensor_);
LOG_SENSOR(" ", "PM >2.5um", this->pm_2_5um_sensor_);
LOG_SENSOR(" ", "PM >5.0um", this->pm_5_0um_sensor_);
LOG_SENSOR(" ", "PM >10.0um", this->pm_10_0um_sensor_);
LOG_SENSOR(" ", "TVOC", this->tvoc_sensor_);
LOG_SENSOR(" ", "eCO2", this->eco2_sensor_);
LOG_SENSOR(" ", "AQI", this->aqi_sensor_);
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
LOG_SENSOR(" ", "Raw Temperature", this->raw_temperature_sensor_);
LOG_SENSOR(" ", "Raw Humidity", this->raw_humidity_sensor_);
LOG_SENSOR(" ", "RS0", this->rs0_sensor_);
LOG_SENSOR(" ", "RS2", this->rs2_sensor_);
LOG_SENSOR(" ", "RS3", this->rs3_sensor_);
LOG_SENSOR(" ", "Error Code", this->error_code_sensor_);
}
void APC1Component::loop() {
const uint32_t now = App.get_loop_component_start_time();
// If in active mode and not idle, retry periodically if no valid data received
if (this->active_mode_ && !this->idle_) {
if (this->last_valid_frame_ == 0) {
if (now - this->last_init_attempt_ >= APC1_INIT_RETRY_INTERVAL_MS) {
ESP_LOGD(TAG, "Waiting for APC1 sensor to respond, sending init commands...");
this->initialize_device_();
}
} else if (now - this->last_valid_frame_ > APC1_NO_DATA_TIMEOUT_MS &&
now - this->last_init_attempt_ >= APC1_INIT_RETRY_INTERVAL_MS) {
ESP_LOGW(TAG, "No data received from APC1 for %" PRIu32 " ms, re-initializing...", now - this->last_valid_frame_);
this->initialize_device_();
}
}
if (this->rx_index_ > 0 && now - this->last_transmission_ > APC1_RX_IDLE_TIMEOUT_MS) {
ESP_LOGV(TAG, "Timed out waiting for data, resetting buffer");
this->rx_index_ = 0;
}
while (this->available()) {
this->last_transmission_ = now;
uint8_t byte;
this->read_byte(&byte);
if (this->rx_index_ == 0) {
if (byte == APC1_HEADER_1) {
this->rx_buffer_[this->rx_index_++] = byte;
}
continue;
}
if (this->rx_index_ == 1) {
if (byte == APC1_HEADER_2) {
this->rx_buffer_[this->rx_index_++] = byte;
} else if (byte != APC1_HEADER_1) {
this->rx_index_ = 0;
}
continue;
}
this->rx_buffer_[this->rx_index_++] = byte;
if (this->rx_index_ < APC1_FRAME_HEADER_SIZE) {
continue;
}
// The payload carries at least the checksum and must fit the buffer; checking it before the sum keeps a
// corrupt length from wrapping the total and walking past the buffer
const uint16_t payload_len = encode_uint16(this->rx_buffer_[2], this->rx_buffer_[3]);
if (payload_len < 2 || payload_len > this->rx_buffer_.size() - APC1_FRAME_HEADER_SIZE) {
ESP_LOGW(TAG, "Invalid frame length: %u", payload_len);
this->rx_index_ = 0;
continue;
}
const uint8_t total_len = payload_len + APC1_FRAME_HEADER_SIZE;
if (this->rx_index_ == total_len) {
this->parse_frame_(total_len);
this->rx_index_ = 0;
}
}
}
void APC1Component::parse_frame_(uint16_t total_len) {
uint16_t expected_checksum = encode_uint16(this->rx_buffer_[total_len - 2], this->rx_buffer_[total_len - 1]);
uint16_t calculated_checksum = apc1_checksum(this->rx_buffer_.data(), total_len - 2);
if (expected_checksum != calculated_checksum) {
ESP_LOGW(TAG, "APC1 checksum mismatch: calculated 0x%04X != expected 0x%04X", calculated_checksum,
expected_checksum);
return;
}
if (total_len == APC1_FRAME_SIZE_MEASUREMENT) {
this->parse_measurement_frame_();
} else if (total_len == APC1_FRAME_SIZE_DEVICE_INFO) {
this->parse_device_info_frame_();
} else if (total_len == APC1_FRAME_SIZE_COMMAND_RESPONSE) {
ESP_LOGD(TAG, "APC1 command response: cmd=0x%02X, data=0x%02X", this->rx_buffer_[4], this->rx_buffer_[5]);
} else {
ESP_LOGD(TAG, "Received frame of unknown length %u", total_len);
}
}
// One message per error bit, bit 0 first
PROGMEM_STRING_TABLE(ErrorStrings, "Fan restart error (too many restarts)", "Fan speed low", "Photodiode fault",
"Fan stopped / startup error", "Laser fault", "VOC sensor fault", "RHT sensor fault");
void APC1Component::parse_measurement_frame_() {
const uint32_t now = App.get_loop_component_start_time();
this->last_valid_frame_ = now;
uint8_t error_code = this->rx_buffer_[APC1_MEASUREMENT_OFFSET_ERROR_CODE];
if (error_code != this->last_error_code_) {
if (error_code != 0) {
for (uint8_t bit = 0; bit < ErrorStrings::COUNT; bit++) {
if (error_code & (1 << bit)) {
ESP_LOGW(TAG, "APC1: %s", LOG_STR_ARG(ErrorStrings::get_log_str(bit, ErrorStrings::LAST_INDEX)));
}
}
this->status_set_warning(LOG_STR("Sensor reported hardware error"));
} else {
ESP_LOGI(TAG, "APC1: Hardware error cleared");
this->status_clear_warning();
}
this->last_error_code_ = error_code;
if (this->error_code_sensor_ != nullptr) {
this->error_code_sensor_->publish_state(error_code);
}
}
uint8_t aqi = this->rx_buffer_[APC1_MEASUREMENT_OFFSET_AQI];
if (aqi == 0) {
if (!this->gas_warming_up_) {
this->gas_warming_up_ = true;
ESP_LOGI(TAG, "APC1: Gas sensor warming up (AQI=0)");
}
} else if (this->gas_warming_up_) {
this->gas_warming_up_ = false;
ESP_LOGI(TAG, "APC1: Gas sensor warm-up complete (AQI=%u)", aqi);
}
// Measurement payload data (bytes 4-57): big-endian sensor readings.
// The values below feed the debug summary, so they are always decoded; every
// other reading is only decoded when its sensor is configured.
const auto u16 = [this](uint8_t offset) {
return encode_uint16(this->rx_buffer_[offset], this->rx_buffer_[offset + 1]);
};
const auto u32 = [this](uint8_t offset) {
return encode_uint32(this->rx_buffer_[offset], this->rx_buffer_[offset + 1], this->rx_buffer_[offset + 2],
this->rx_buffer_[offset + 3]);
};
uint16_t pm_1_0 = u16(10);
uint16_t pm_2_5 = u16(12);
uint16_t pm_10_0 = u16(14);
uint16_t tvoc = u16(28);
uint16_t eco2 = u16(30);
float temp_comp = static_cast<int16_t>(u16(34)) / 10.0f;
float hum_comp = u16(36) / 10.0f;
ESP_LOGD(TAG, "APC1: PM1.0: %u, PM2.5: %u, PM10: %u, TVOC: %u, eCO2: %u, AQI: %u, Temp: %.1f°C, Hum: %.1f%%", pm_1_0,
pm_2_5, pm_10_0, tvoc, eco2, aqi, temp_comp, hum_comp);
if (this->pm_1_0_std_sensor_ != nullptr)
this->pm_1_0_std_sensor_->publish_state(u16(4));
if (this->pm_2_5_std_sensor_ != nullptr)
this->pm_2_5_std_sensor_->publish_state(u16(6));
if (this->pm_10_0_std_sensor_ != nullptr)
this->pm_10_0_std_sensor_->publish_state(u16(8));
if (this->pm_1_0_sensor_ != nullptr)
this->pm_1_0_sensor_->publish_state(pm_1_0);
if (this->pm_2_5_sensor_ != nullptr)
this->pm_2_5_sensor_->publish_state(pm_2_5);
if (this->pm_10_0_sensor_ != nullptr)
this->pm_10_0_sensor_->publish_state(pm_10_0);
if (this->pm_0_3um_sensor_ != nullptr)
this->pm_0_3um_sensor_->publish_state(u16(16));
if (this->pm_0_5um_sensor_ != nullptr)
this->pm_0_5um_sensor_->publish_state(u16(18));
if (this->pm_1_0um_sensor_ != nullptr)
this->pm_1_0um_sensor_->publish_state(u16(20));
if (this->pm_2_5um_sensor_ != nullptr)
this->pm_2_5um_sensor_->publish_state(u16(22));
if (this->pm_5_0um_sensor_ != nullptr)
this->pm_5_0um_sensor_->publish_state(u16(24));
if (this->pm_10_0um_sensor_ != nullptr)
this->pm_10_0um_sensor_->publish_state(u16(26));
if (this->tvoc_sensor_ != nullptr)
this->tvoc_sensor_->publish_state(tvoc);
if (this->eco2_sensor_ != nullptr)
this->eco2_sensor_->publish_state(eco2);
if (this->aqi_sensor_ != nullptr)
this->aqi_sensor_->publish_state(aqi > 0 ? aqi : NAN);
if (this->temperature_sensor_ != nullptr)
this->temperature_sensor_->publish_state(temp_comp);
if (this->humidity_sensor_ != nullptr)
this->humidity_sensor_->publish_state(hum_comp);
if (this->raw_temperature_sensor_ != nullptr) {
this->raw_temperature_sensor_->publish_state(static_cast<int16_t>(u16(38)) / 10.0f);
}
if (this->raw_humidity_sensor_ != nullptr)
this->raw_humidity_sensor_->publish_state(u16(40) / 10.0f);
if (this->rs0_sensor_ != nullptr) {
this->rs0_sensor_->publish_state(u32(42));
}
if (this->rs2_sensor_ != nullptr) {
this->rs2_sensor_->publish_state(u32(50));
}
if (this->rs3_sensor_ != nullptr) {
this->rs3_sensor_->publish_state(u32(54));
}
}
void APC1Component::parse_device_info_frame_() {
const uint8_t *serial = this->rx_buffer_.data() + APC1_DEVICE_INFO_OFFSET_SERIAL;
uint16_t fw_version =
encode_uint16(this->rx_buffer_[APC1_DEVICE_INFO_OFFSET_FW], this->rx_buffer_[APC1_DEVICE_INFO_OFFSET_FW + 1]);
ESP_LOGI(TAG, "APC1: Module: %.6s, Serial: %08" PRIX32 "%08" PRIX32 ", Firmware: 0x%04X (%u)",
reinterpret_cast<const char *>(this->rx_buffer_.data() + APC1_DEVICE_INFO_OFFSET_MODULE),
encode_uint32(serial[0], serial[1], serial[2], serial[3]),
encode_uint32(serial[4], serial[5], serial[6], serial[7]), fw_version, fw_version);
}
void APC1Component::arm_watchdog_() {
this->last_valid_frame_ = App.get_loop_component_start_time();
this->last_init_attempt_ = this->last_valid_frame_;
}
void APC1Component::set_active_mode() {
if (!this->active_mode_) {
this->arm_watchdog_();
}
this->active_mode_ = true;
this->send_command_(APC1Command::APC1_COMMAND_MEASUREMENT_MODE, APC1_MEASUREMENT_MODE_ACTIVE);
}
void APC1Component::set_passive_mode() {
this->active_mode_ = false;
this->send_command_(APC1Command::APC1_COMMAND_MEASUREMENT_MODE, APC1_MEASUREMENT_MODE_PASSIVE);
}
void APC1Component::request_measurement() {
this->send_command_(APC1Command::APC1_COMMAND_REQUEST_MEASUREMENT, 0x0000);
}
void APC1Component::set_idle_mode() {
this->idle_ = true;
this->send_command_(APC1Command::APC1_COMMAND_OPERATION_MODE, APC1_OPERATING_MODE_IDLE);
}
void APC1Component::set_measurement_mode() {
this->idle_ = false;
this->arm_watchdog_();
this->send_command_(APC1Command::APC1_COMMAND_OPERATION_MODE, APC1_OPERATING_MODE_STANDARD);
}
void APC1Component::send_command_(APC1Command cmd, uint16_t data) {
std::array<uint8_t, APC1_COMMAND_FRAME_SIZE> buf{
APC1_HEADER_1,
APC1_HEADER_2,
static_cast<uint8_t>(cmd),
static_cast<uint8_t>((data >> 8) & 0xFF),
static_cast<uint8_t>(data & 0xFF),
0,
0,
};
uint16_t checksum = apc1_checksum(buf.data(), 5);
buf[5] = static_cast<uint8_t>((checksum >> 8) & 0xFF);
buf[6] = static_cast<uint8_t>(checksum & 0xFF);
this->write_array(buf);
}
} // namespace esphome::apc1
-99
View File
@@ -1,99 +0,0 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/uart/uart.h"
#include <array>
namespace esphome::apc1 {
static constexpr uint8_t APC1_HEADER_1 = 0x42;
static constexpr uint8_t APC1_HEADER_2 = 0x4D;
static constexpr uint8_t APC1_FRAME_HEADER_SIZE = 4; // 2 start bytes (0x42, 0x4D) + 2 frame length bytes
static constexpr uint8_t APC1_FRAME_SIZE_MEASUREMENT = 64;
static constexpr uint8_t APC1_FRAME_SIZE_DEVICE_INFO = 23;
static constexpr uint8_t APC1_FRAME_SIZE_COMMAND_RESPONSE = 8;
static constexpr uint8_t APC1_COMMAND_FRAME_SIZE = 7;
enum class APC1Command : uint8_t {
APC1_COMMAND_MEASUREMENT_MODE = 0xE1,
APC1_COMMAND_REQUEST_MEASUREMENT = 0xE2,
APC1_COMMAND_OPERATION_MODE = 0xE4,
APC1_COMMAND_READ_SENSOR_VERSION = 0xE9,
};
static constexpr uint16_t APC1_MEASUREMENT_MODE_PASSIVE = 0x0000;
static constexpr uint16_t APC1_MEASUREMENT_MODE_ACTIVE = 0x0001;
static constexpr uint16_t APC1_OPERATING_MODE_IDLE = 0x0000;
static constexpr uint16_t APC1_OPERATING_MODE_STANDARD = 0x0001;
class APC1Component final : public uart::UARTDevice, public Component {
SUB_SENSOR(pm_1_0)
SUB_SENSOR(pm_2_5)
SUB_SENSOR(pm_10_0)
SUB_SENSOR(pm_1_0_std)
SUB_SENSOR(pm_2_5_std)
SUB_SENSOR(pm_10_0_std)
SUB_SENSOR(pm_0_3um)
SUB_SENSOR(pm_0_5um)
SUB_SENSOR(pm_1_0um)
SUB_SENSOR(pm_2_5um)
SUB_SENSOR(pm_5_0um)
SUB_SENSOR(pm_10_0um)
SUB_SENSOR(tvoc)
SUB_SENSOR(eco2)
SUB_SENSOR(aqi)
SUB_SENSOR(temperature)
SUB_SENSOR(humidity)
SUB_SENSOR(raw_temperature)
SUB_SENSOR(raw_humidity)
SUB_SENSOR(rs0)
SUB_SENSOR(rs2)
SUB_SENSOR(rs3)
SUB_SENSOR(error_code)
public:
void setup() override;
void dump_config() override;
void loop() override;
void set_active_mode();
void set_passive_mode();
void request_measurement();
void set_idle_mode();
void set_measurement_mode();
void set_set_pin(GPIOPin *set_pin) { this->set_pin_ = set_pin; }
void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; }
protected:
void send_command_(APC1Command cmd, uint16_t data);
void initialize_device_();
/// Restarts the no-data watchdog so a mode change gets the same grace period as a cold boot
void arm_watchdog_();
void parse_frame_(uint16_t total_len);
void parse_measurement_frame_();
void parse_device_info_frame_();
GPIOPin *set_pin_{nullptr};
GPIOPin *reset_pin_{nullptr};
uint32_t last_transmission_{0};
uint32_t last_valid_frame_{0};
uint32_t last_init_attempt_{0};
std::array<uint8_t, APC1_FRAME_SIZE_MEASUREMENT> rx_buffer_{};
uint8_t rx_index_{0};
uint8_t last_error_code_{0xFF};
bool active_mode_{true};
bool idle_{false};
bool gas_warming_up_{false};
};
} // namespace esphome::apc1
-222
View File
@@ -1,222 +0,0 @@
from esphome import automation, pins
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
from esphome.components import sensor, uart
from esphome.components.const import CONF_AQI
import esphome.config_validation as cv
from esphome.const import (
CONF_ECO2,
CONF_HUMIDITY,
CONF_ID,
CONF_PM_0_3UM,
CONF_PM_0_5UM,
CONF_PM_1_0,
CONF_PM_1_0_STD,
CONF_PM_1_0UM,
CONF_PM_2_5,
CONF_PM_2_5_STD,
CONF_PM_2_5UM,
CONF_PM_5_0UM,
CONF_PM_10_0,
CONF_PM_10_0_STD,
CONF_PM_10_0UM,
CONF_RESET_PIN,
CONF_TEMPERATURE,
CONF_TVOC,
DEVICE_CLASS_AQI,
DEVICE_CLASS_CARBON_DIOXIDE,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_PM1,
DEVICE_CLASS_PM10,
DEVICE_CLASS_PM25,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
ENTITY_CATEGORY_DIAGNOSTIC,
ICON_CHEMICAL_WEAPON,
ICON_MOLECULE_CO2,
ICON_OMEGA,
ICON_RADIATOR,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_COUNT_DECILITRE,
UNIT_MICROGRAMS_PER_CUBIC_METER,
UNIT_OHM,
UNIT_PARTS_PER_BILLION,
UNIT_PARTS_PER_MILLION,
UNIT_PERCENT,
)
from esphome.types import ConfigType
CODEOWNERS = ["@ademuri"]
DEPENDENCIES = ["uart"]
CONF_SET_PIN = "set_pin"
CONF_RAW_TEMPERATURE = "raw_temperature"
CONF_RAW_HUMIDITY = "raw_humidity"
CONF_RS0 = "rs0"
CONF_RS2 = "rs2"
CONF_RS3 = "rs3"
CONF_ERROR_CODE = "error_code"
apc1_ns = cg.esphome_ns.namespace("apc1")
APC1Component = apc1_ns.class_("APC1Component", uart.UARTDevice, cg.Component)
def _pm_mass_schema(device_class: str) -> cv.Schema:
return sensor.sensor_schema(
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=0,
device_class=device_class,
state_class=STATE_CLASS_MEASUREMENT,
)
_PM_COUNT_SCHEMA = sensor.sensor_schema(
unit_of_measurement=UNIT_COUNT_DECILITRE,
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
)
_TEMPERATURE_SCHEMA = sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
)
_HUMIDITY_SCHEMA = sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
accuracy_decimals=1,
device_class=DEVICE_CLASS_HUMIDITY,
state_class=STATE_CLASS_MEASUREMENT,
)
_RESISTANCE_SCHEMA = sensor.sensor_schema(
unit_of_measurement=UNIT_OHM,
icon=ICON_OMEGA,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
)
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(APC1Component),
cv.Optional(CONF_PM_1_0): _pm_mass_schema(DEVICE_CLASS_PM1),
cv.Optional(CONF_PM_2_5): _pm_mass_schema(DEVICE_CLASS_PM25),
cv.Optional(CONF_PM_10_0): _pm_mass_schema(DEVICE_CLASS_PM10),
cv.Optional(CONF_PM_1_0_STD): _pm_mass_schema(DEVICE_CLASS_PM1),
cv.Optional(CONF_PM_2_5_STD): _pm_mass_schema(DEVICE_CLASS_PM25),
cv.Optional(CONF_PM_10_0_STD): _pm_mass_schema(DEVICE_CLASS_PM10),
cv.Optional(CONF_PM_0_3UM): _PM_COUNT_SCHEMA,
cv.Optional(CONF_PM_0_5UM): _PM_COUNT_SCHEMA,
cv.Optional(CONF_PM_1_0UM): _PM_COUNT_SCHEMA,
cv.Optional(CONF_PM_2_5UM): _PM_COUNT_SCHEMA,
cv.Optional(CONF_PM_5_0UM): _PM_COUNT_SCHEMA,
cv.Optional(CONF_PM_10_0UM): _PM_COUNT_SCHEMA,
cv.Optional(CONF_TVOC): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_BILLION,
icon=ICON_RADIATOR,
accuracy_decimals=0,
device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_ECO2): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_MILLION,
icon=ICON_MOLECULE_CO2,
accuracy_decimals=0,
device_class=DEVICE_CLASS_CARBON_DIOXIDE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_AQI): sensor.sensor_schema(
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=0,
device_class=DEVICE_CLASS_AQI,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_TEMPERATURE): _TEMPERATURE_SCHEMA,
cv.Optional(CONF_HUMIDITY): _HUMIDITY_SCHEMA,
cv.Optional(CONF_RAW_TEMPERATURE): _TEMPERATURE_SCHEMA,
cv.Optional(CONF_RAW_HUMIDITY): _HUMIDITY_SCHEMA,
cv.Optional(CONF_RS0): _RESISTANCE_SCHEMA,
cv.Optional(CONF_RS2): _RESISTANCE_SCHEMA,
cv.Optional(CONF_RS3): _RESISTANCE_SCHEMA,
cv.Optional(CONF_ERROR_CODE): sensor.sensor_schema(
icon="mdi:alert-circle-outline",
accuracy_decimals=0,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
cv.Optional(CONF_SET_PIN): pins.gpio_output_pin_schema,
cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(uart.UART_DEVICE_SCHEMA)
)
def final_validate(config: ConfigType) -> None:
schema = uart.final_validate_device_schema(
"apc1", baud_rate=9600, require_rx=True, require_tx=True
)
schema(config)
FINAL_VALIDATE_SCHEMA = final_validate
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
sensors = sensor.sub_sensors(config)
await sensors(CONF_PM_1_0, var.set_pm_1_0_sensor)
await sensors(CONF_PM_2_5, var.set_pm_2_5_sensor)
await sensors(CONF_PM_10_0, var.set_pm_10_0_sensor)
await sensors(CONF_PM_1_0_STD, var.set_pm_1_0_std_sensor)
await sensors(CONF_PM_2_5_STD, var.set_pm_2_5_std_sensor)
await sensors(CONF_PM_10_0_STD, var.set_pm_10_0_std_sensor)
await sensors(CONF_PM_0_3UM, var.set_pm_0_3um_sensor)
await sensors(CONF_PM_0_5UM, var.set_pm_0_5um_sensor)
await sensors(CONF_PM_1_0UM, var.set_pm_1_0um_sensor)
await sensors(CONF_PM_2_5UM, var.set_pm_2_5um_sensor)
await sensors(CONF_PM_5_0UM, var.set_pm_5_0um_sensor)
await sensors(CONF_PM_10_0UM, var.set_pm_10_0um_sensor)
await sensors(CONF_TVOC, var.set_tvoc_sensor)
await sensors(CONF_ECO2, var.set_eco2_sensor)
await sensors(CONF_AQI, var.set_aqi_sensor)
await sensors(CONF_TEMPERATURE, var.set_temperature_sensor)
await sensors(CONF_HUMIDITY, var.set_humidity_sensor)
await sensors(CONF_RAW_TEMPERATURE, var.set_raw_temperature_sensor)
await sensors(CONF_RAW_HUMIDITY, var.set_raw_humidity_sensor)
await sensors(CONF_RS0, var.set_rs0_sensor)
await sensors(CONF_RS2, var.set_rs2_sensor)
await sensors(CONF_RS3, var.set_rs3_sensor)
await sensors(CONF_ERROR_CODE, var.set_error_code_sensor)
if (set_pin_config := config.get(CONF_SET_PIN)) is not None:
set_pin = await cg.gpio_pin_expression(set_pin_config)
cg.add(var.set_set_pin(set_pin))
if (reset_pin_config := config.get(CONF_RESET_PIN)) is not None:
reset_pin = await cg.gpio_pin_expression(reset_pin_config)
cg.add(var.set_reset_pin(reset_pin))
APC1_ACTION_SCHEMA = maybe_simple_id(
{
cv.GenerateID(): cv.use_id(APC1Component),
}
)
for _name, _call in (
("apc1.set_active_mode", "set_active_mode()"),
("apc1.set_passive_mode", "set_passive_mode()"),
("apc1.request_measurement", "request_measurement()"),
("apc1.set_idle_mode", "set_idle_mode()"),
("apc1.set_measurement_mode", "set_measurement_mode()"),
):
automation.register_apply_action(
_name, APC1_ACTION_SCHEMA, automation.ApplyCall(_call)
)
Loaded 100 of 3444 files, more files were not shown because too many files have changed in this diff. Show more