mirror of
https://github.com/esphome/esphome.git
synced 2026-10-26 16:02:13 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
937b73c8f1 |
No files matched your search
@@ -1,39 +0,0 @@
|
||||
name: Cache Arduino ESP8266
|
||||
description: >
|
||||
Resolve the pinned Arduino core and xtensa toolchain versions and cache the
|
||||
native ESP8266 install (~110 MB framework + toolchain; no ccache store, the
|
||||
seed job saves before any compile runs). Exports
|
||||
ESPHOME_ARDUINO8266_PREFIX to the job so every later step installs into
|
||||
the cached path; the Python venv must already be restored. Mirrors
|
||||
cache-esp-idf: only dev-branch pushes write the shared cache, everything
|
||||
else restores.
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Resolve the native toolchain cache key
|
||||
# Versions are pinned in code, not a hashable file; resolve them so a
|
||||
# bump changes the cache key. Assignment form so errexit catches a
|
||||
# resolver failure.
|
||||
id: version
|
||||
shell: bash
|
||||
run: |
|
||||
# One owner for the install prefix: exported here and referenced by
|
||||
# the cache steps below via env, so the caller's install and the
|
||||
# cached path cannot diverge.
|
||||
echo "ESPHOME_ARDUINO8266_PREFIX=$HOME/.esphome-arduino8266" >> "$GITHUB_ENV"
|
||||
. venv/bin/activate
|
||||
key=$(python -c 'from esphome.components.esp8266 import RECOMMENDED_ARDUINO_FRAMEWORK_VERSION as f; from esphome.arduino8266.framework import TOOLCHAIN_VERSION as t; print(f"{f}-{t}")')
|
||||
[ -n "$key" ] || exit 1
|
||||
echo "key=$key" >> "$GITHUB_OUTPUT"
|
||||
- name: Cache the native toolchain (write on dev)
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: ${{ env.ESPHOME_ARDUINO8266_PREFIX }}
|
||||
key: ${{ runner.os }}-esp8266-native-${{ steps.version.outputs.key }}
|
||||
- name: Restore the native toolchain (off dev)
|
||||
if: github.ref != 'refs/heads/dev'
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: ${{ env.ESPHOME_ARDUINO8266_PREFIX }}
|
||||
key: ${{ runner.os }}-esp8266-native-${{ steps.version.outputs.key }}
|
||||
@@ -195,7 +195,6 @@ jobs:
|
||||
# the default.
|
||||
id:
|
||||
- esp8266-arduino
|
||||
- esp8266-arduino-native
|
||||
- esp32-arduino-platformio
|
||||
- esp32-arduino-esp-idf
|
||||
- esp32-idf-platformio
|
||||
@@ -206,17 +205,6 @@ jobs:
|
||||
- ln882x-arduino
|
||||
- nrf52
|
||||
- host
|
||||
# Strict by default so a new matrix id cannot silently join in the
|
||||
# degrade-quietly mode the knob exists to catch; the knob is inert
|
||||
# where no pch code runs (esp32-*-platformio, nrf52).
|
||||
# Opt-outs: libretiny GCC rejects its own pch until a toolchain bump.
|
||||
include:
|
||||
- id: bk72xx-arduino
|
||||
pch_strict: "0"
|
||||
- id: rtl87xx-arduino
|
||||
pch_strict: "0"
|
||||
- id: ln882x-arduino
|
||||
pch_strict: "0"
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Download image artifact
|
||||
@@ -228,7 +216,6 @@ jobs:
|
||||
- name: Compile ${{ matrix.id }}
|
||||
run: |
|
||||
docker run --rm \
|
||||
-e ESPHOME_PCH_STRICT="${{ matrix.pch_strict || '1' }}" \
|
||||
-v "${{ github.workspace }}/docker/test_configs:/config" \
|
||||
"ghcr.io/esphome/esphome-amd64:${{ needs.check-docker.outputs.tag }}" \
|
||||
compile "${{ matrix.id }}.yaml"
|
||||
+33
-96
@@ -102,8 +102,6 @@ jobs:
|
||||
device-builder: ${{ steps.determine.outputs.device-builder }}
|
||||
esp32-platformio: ${{ steps.determine.outputs.esp32-platformio }}
|
||||
esp32-platformio-components: ${{ steps.determine.outputs.esp32-platformio-components }}
|
||||
esp8266-native: ${{ steps.determine.outputs.esp8266-native }}
|
||||
esp8266-native-components: ${{ steps.determine.outputs.esp8266-native-components }}
|
||||
changed-components: ${{ steps.determine.outputs.changed-components }}
|
||||
changed-components-with-tests: ${{ steps.determine.outputs.changed-components-with-tests }}
|
||||
directly-changed-components-with-tests: ${{ steps.determine.outputs.directly-changed-components-with-tests }}
|
||||
@@ -167,8 +165,6 @@ jobs:
|
||||
echo "device-builder=$(echo "$output" | jq -r '.device_builder')" >> $GITHUB_OUTPUT
|
||||
echo "esp32-platformio=$(echo "$output" | jq -r '.esp32_platformio')" >> $GITHUB_OUTPUT
|
||||
echo "esp32-platformio-components=$(echo "$output" | jq -r '.esp32_platformio_components')" >> $GITHUB_OUTPUT
|
||||
echo "esp8266-native=$(echo "$output" | jq -r '.esp8266_native')" >> $GITHUB_OUTPUT
|
||||
echo "esp8266-native-components=$(echo "$output" | jq -r '.esp8266_native_components')" >> $GITHUB_OUTPUT
|
||||
echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT
|
||||
echo "changed-components-with-tests=$(echo "$output" | jq -c '.changed_components_with_tests')" >> $GITHUB_OUTPUT
|
||||
echo "directly-changed-components-with-tests=$(echo "$output" | jq -c '.directly_changed_components_with_tests')" >> $GITHUB_OUTPUT
|
||||
@@ -187,33 +183,6 @@ jobs:
|
||||
path: .temp/components_graph.json
|
||||
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
|
||||
|
||||
seed-esp8266-native-cache:
|
||||
name: Seed the esp8266 native toolchain cache
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- common
|
||||
# PR-branch cache saves are invisible to other PRs, so dev pushes seed
|
||||
# the shared entry the component matrix, the memory impact jobs and
|
||||
# test-esp8266-native restore. Only dev: the composite action saves
|
||||
# nowhere else, so a beta/release push would download the toolchain and
|
||||
# discard it.
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/dev'
|
||||
timeout-minutes: 15
|
||||
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: Cache the native toolchain
|
||||
uses: ./.github/actions/cache-arduino8266
|
||||
- name: Install the native toolchain
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
python -c "from esphome.arduino8266.framework import check_and_install; from esphome.components.esp8266 import RECOMMENDED_ARDUINO_FRAMEWORK_VERSION; check_and_install(RECOMMENDED_ARDUINO_FRAMEWORK_VERSION)"
|
||||
|
||||
ci-custom:
|
||||
name: Run script/ci-custom
|
||||
runs-on: ubuntu-24.04
|
||||
@@ -400,9 +369,14 @@ jobs:
|
||||
matrix:
|
||||
bucket: ${{ fromJson(needs.determine-jobs.outputs.integration-test-buckets) }}
|
||||
env:
|
||||
# Registry libraries (noise-c, libsodium, ArduinoJson, lvgl) the host builds
|
||||
# download, shared per xdist worker by tests/integration/conftest.py
|
||||
INTEGRATION_LIBRARY_CACHE_PATH: ~/.esphome-integration-tests/pio_components
|
||||
# What the cache steps persist; libdeps is excluded (keyed per xdist
|
||||
# worker and env, it never crosses runs).
|
||||
INTEGRATION_PIO_CACHE_PATH: |
|
||||
~/.esphome-integration-tests/platformio/platforms
|
||||
~/.esphome-integration-tests/platformio/packages
|
||||
~/.esphome-integration-tests/platformio/appstate.json
|
||||
~/.esphome-integration-tests/platformio/.cache
|
||||
~/.esphome-integration-tests/platformio/.esphome.pio.stamp.json
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
@@ -422,13 +396,14 @@ jobs:
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- name: Restore integration library cache
|
||||
# A miss or a changed pin self-heals with a download, so no restore-keys
|
||||
id: library-cache
|
||||
- name: Restore integration PlatformIO cache
|
||||
# Native platform + toolchain installed by shared_platformio_cache in
|
||||
# tests/integration/conftest.py; a miss self-heals, so no restore-keys.
|
||||
id: pio-cache
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: ${{ env.INTEGRATION_LIBRARY_CACHE_PATH }}
|
||||
key: integration-libraries-v1-${{ runner.os }}-${{ hashFiles('esphome/components/json/__init__.py', 'esphome/components/noise/__init__.py', 'esphome/components/lvgl/__init__.py', 'esphome/components/improv_base/__init__.py') }}
|
||||
path: ${{ env.INTEGRATION_PIO_CACHE_PATH }}
|
||||
key: integration-pio-v1-${{ runner.os }}-py${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt', 'tests/integration/fixtures/cache_init.yaml', 'esphome/components/host/__init__.py') }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
@@ -496,16 +471,16 @@ jobs:
|
||||
# A full cron period of margin for the weekly refresh
|
||||
retention-days: 14
|
||||
- name: Print ccache statistics
|
||||
# esphome stores the host build's ccache under the machine-global
|
||||
# cache dir (see get_build_env() in esphome/host/toolchain.py).
|
||||
run: CCACHE_DIR="$HOME/.cache/esphome/host/ccache" ccache -s
|
||||
- name: Save integration library cache
|
||||
# esphome stores the PlatformIO ccache under the machine-global cache
|
||||
# dir (see _ccache_env() in esphome/platformio/toolchain.py).
|
||||
run: CCACHE_DIR="$HOME/.cache/esphome/platformio-ccache" ccache -s
|
||||
- name: Save integration PlatformIO cache
|
||||
# Bucket 0 only; the others would race the same immutable key.
|
||||
if: success() && (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write')) && strategy.job-index == 0 && steps.library-cache.outputs.cache-hit != 'true'
|
||||
if: success() && (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write')) && strategy.job-index == 0 && steps.pio-cache.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: ${{ env.INTEGRATION_LIBRARY_CACHE_PATH }}
|
||||
key: ${{ steps.library-cache.outputs.cache-primary-key }}
|
||||
path: ${{ env.INTEGRATION_PIO_CACHE_PATH }}
|
||||
key: ${{ steps.pio-cache.outputs.cache-primary-key }}
|
||||
|
||||
import-time:
|
||||
name: Check import esphome.__main__ time
|
||||
@@ -1122,11 +1097,6 @@ jobs:
|
||||
uses: ./.github/actions/cache-sdk-nrf
|
||||
with:
|
||||
restore-only: true
|
||||
- name: Cache the native ESP8266 toolchain
|
||||
# Only batches whose test platforms include esp8266; never saves
|
||||
# here, it reuses the install the dev seed job cached.
|
||||
if: matrix.batch.needs_arduino8266
|
||||
uses: ./.github/actions/cache-arduino8266
|
||||
- name: Validate and compile components with intelligent grouping
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
@@ -1267,7 +1237,7 @@ jobs:
|
||||
|
||||
# compile validates config first, so a separate config pass is
|
||||
# redundant for this smoke test. ESP-IDF framework via PlatformIO:
|
||||
python3 script/test_build_components.py -e compile -t esp32-idf -c "$TEST_COMPONENTS" -f --toolchain platformio --fail-on-no-tests
|
||||
python3 script/test_build_components.py -e compile -t esp32-idf -c "$TEST_COMPONENTS" -f --toolchain platformio
|
||||
|
||||
echo ""
|
||||
echo "ESP-IDF-via-PlatformIO build passed! Starting Arduino smoke test..."
|
||||
@@ -1276,42 +1246,6 @@ jobs:
|
||||
# Arduino framework via PlatformIO (only components with an esp32-ard test are built):
|
||||
python3 script/test_build_components.py -e compile -t esp32-ard -c "$TEST_COMPONENTS" -f --toolchain platformio
|
||||
|
||||
test-esp8266-native:
|
||||
name: Test esp8266 components with the native toolchain
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- common
|
||||
- determine-jobs
|
||||
if: github.event_name == 'pull_request' && needs.determine-jobs.outputs.esp8266-native == 'true'
|
||||
env:
|
||||
# Computed by script/determine-jobs.py (ESP8266_NATIVE_TEST_COMPONENTS)
|
||||
TEST_COMPONENTS: ${{ needs.determine-jobs.outputs.esp8266-native-components }}
|
||||
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: Cache the native toolchain
|
||||
uses: ./.github/actions/cache-arduino8266
|
||||
|
||||
- name: Run native toolchain compile test
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
|
||||
echo "Testing components: $TEST_COMPONENTS"
|
||||
echo ""
|
||||
|
||||
# ESP8266 Arduino built directly (no PlatformIO); compile validates
|
||||
# config first, so a separate config pass is redundant. Strict pch:
|
||||
# exercises the native ninja pch (and its probe edge) against real
|
||||
# component configs; the docker matrix smoke-tests both toolchains.
|
||||
ESPHOME_PCH_STRICT=1 python3 script/test_build_components.py -e compile -t esp8266-ard -c "$TEST_COMPONENTS" -f --toolchain arduino --fail-on-no-tests
|
||||
|
||||
device-builder:
|
||||
name: Test downstream esphome/device-builder
|
||||
runs-on: ubuntu-24.04
|
||||
@@ -1481,9 +1415,12 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
|
||||
- name: Cache the native ESP8266 toolchain
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && fromJSON(needs.determine-jobs.outputs.memory_impact).needs_arduino8266
|
||||
uses: ./.github/actions/cache-arduino8266
|
||||
- name: Cache platformio
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Build, compile, and analyze memory
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
|
||||
@@ -1568,9 +1505,11 @@ jobs:
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Cache the native ESP8266 toolchain
|
||||
if: fromJSON(needs.determine-jobs.outputs.memory_impact).needs_arduino8266
|
||||
uses: ./.github/actions/cache-arduino8266
|
||||
- name: Cache platformio
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
|
||||
- name: Build, compile, and analyze memory
|
||||
id: extract
|
||||
run: |
|
||||
@@ -1669,7 +1608,6 @@ jobs:
|
||||
needs:
|
||||
- common
|
||||
- seed-apt-cache
|
||||
- seed-esp8266-native-cache
|
||||
- determine-jobs
|
||||
- ci-custom
|
||||
- pylint
|
||||
@@ -1685,7 +1623,6 @@ jobs:
|
||||
- clang-tidy-esp32-variants
|
||||
- test-build-components-split
|
||||
- test-esp32-platformio
|
||||
- test-esp8266-native
|
||||
- device-builder
|
||||
- memory-impact-target-branch
|
||||
- memory-impact-pr-branch
|
||||
|
||||
@@ -322,25 +322,6 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
var = await switch.new_switch(config)
|
||||
```
|
||||
|
||||
- **Optional child entities of a hub:** bind the config once with `sensor.sub_sensors(config)` (or
|
||||
`sub_binary_sensors`, `sub_text_sensors`, `sub_buttons`, `sub_switches`, `sub_numbers`,
|
||||
`sub_selects` in their domains), adding `parent=hub` for entities that derive from `Parented<T>`,
|
||||
then make one call per key, even when there is only one. A call creates the entity only when its key
|
||||
is configured, passes it to the setter and returns it (or `None`); extra arguments such as
|
||||
`min_value` or `options` go on the call. Always name the setter explicitly on the object that owns
|
||||
it, never with `getattr` and an f-string, and keep that variable short (`var` for the component
|
||||
itself, `hub` for one fetched with `cg.get_variable`) so the calls fit on one line. Loops whose
|
||||
setter also takes an index, such as `set_gate_threshold(x, n)`, stay as they are.
|
||||
```python
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_TEMPERATURE, var.set_temperature_sensor)
|
||||
await sensors(CONF_HUMIDITY, var.set_humidity_sensor)
|
||||
buttons = button.sub_buttons(config, parent=var)
|
||||
await buttons(CONF_RESTART, var.set_restart_button)
|
||||
```
|
||||
|
||||
* **Automations (Triggers, Actions, Conditions):**
|
||||
|
||||
Automations have three building blocks: **Triggers** (fire when something happens), **Actions** (do something), and **Conditions** (check if something is true).
|
||||
@@ -756,9 +737,7 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
|
||||
6. **Avoid `std::deque`:** It allocates in 512-byte blocks regardless of element size, guaranteeing at least 512 bytes of RAM usage immediately. This is a major source of crashes on memory-constrained devices.
|
||||
|
||||
7. **Never use `new (std::nothrow)`:** On ESP-IDF exceptions are disabled, so a failed nothrow allocation aborts instead of returning `nullptr`. Use `RAMAllocator` from `esphome/core/helpers.h`; CI rejects `std::nothrow`.
|
||||
|
||||
8. **Detection:** Look for these patterns in compiler output:
|
||||
7. **Detection:** Look for these patterns in compiler output:
|
||||
- Large code sections with STL symbols (vector, map, set)
|
||||
- `alloc`, `realloc`, `dealloc` in symbol names
|
||||
- `_M_realloc_insert`, `_M_default_append` (vector reallocation)
|
||||
|
||||
@@ -125,7 +125,6 @@ esphome/components/combination/* @Cat-Ion @kahrendt
|
||||
esphome/components/const/* @esphome/core
|
||||
esphome/components/coolix/* @glmnet
|
||||
esphome/components/copy/* @OttoWinter
|
||||
esphome/components/counter/* @clydebarrow
|
||||
esphome/components/cover/* @esphome/core
|
||||
esphome/components/cs5460a/* @balrog-kun
|
||||
esphome/components/cse7761/* @berfenger
|
||||
@@ -423,7 +422,6 @@ esphome/components/pn7150_i2c/* @jesserockz @kbx81
|
||||
esphome/components/pn7160/* @jesserockz @kbx81
|
||||
esphome/components/pn7160_i2c/* @jesserockz @kbx81
|
||||
esphome/components/pn7160_spi/* @jesserockz @kbx81
|
||||
esphome/components/pn71xx/* @jesserockz @kbx81
|
||||
esphome/components/power_supply/* @esphome/core
|
||||
esphome/components/preferences/* @esphome/core
|
||||
esphome/components/provisioning/* @esphome/core
|
||||
@@ -431,7 +429,6 @@ esphome/components/psram/* @esphome/core
|
||||
esphome/components/pulse_meter/* @cstaahl @stevebaxter @TrentHouliston
|
||||
esphome/components/pvvx_mithermometer/* @pasiz
|
||||
esphome/components/pylontech/* @functionpointer
|
||||
esphome/components/pzem6l24/* @nuttytree
|
||||
esphome/components/qmi8658/* @clydebarrow
|
||||
esphome/components/qmp6988/* @andrewpc
|
||||
esphome/components/qr_code/* @wjtje
|
||||
@@ -551,7 +548,6 @@ esphome/components/sx126x/* @swoboda1337
|
||||
esphome/components/sx127x/* @swoboda1337
|
||||
esphome/components/sy6970/* @linkedupbits
|
||||
esphome/components/syslog/* @clydebarrow
|
||||
esphome/components/systa_bus/* @Mat931
|
||||
esphome/components/t6615/* @tylermenezes
|
||||
esphome/components/tas2780/* @remcom
|
||||
esphome/components/tc74/* @sethgirvan
|
||||
@@ -566,7 +562,6 @@ esphome/components/template/datetime/* @rfdarter
|
||||
esphome/components/template/event/* @nohat
|
||||
esphome/components/template/fan/* @ssieb
|
||||
esphome/components/text/* @mauritskorse
|
||||
esphome/components/tfluna/* @candrews
|
||||
esphome/components/thermopro_ble/* @sittner
|
||||
esphome/components/thermostat/* @kbx81
|
||||
esphome/components/time/* @esphome/core
|
||||
@@ -643,7 +638,6 @@ esphome/components/wts01/* @alepee
|
||||
esphome/components/x9c/* @EtienneMD
|
||||
esphome/components/xdb401/* @RT530
|
||||
esphome/components/xgzp68xx/* @gcormier
|
||||
esphome/components/xiaomi_body_scale/* @dckiller51
|
||||
esphome/components/xiaomi_hhccjcy10/* @fariouche
|
||||
esphome/components/xiaomi_lywsd02mmc/* @juanluss31
|
||||
esphome/components/xiaomi_lywsd03mmc/* @ahpohl
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ RUN \
|
||||
-r /requirements.txt
|
||||
|
||||
# Install the ESPHome Device Builder dashboard.
|
||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.17.0
|
||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.15.0
|
||||
|
||||
RUN \
|
||||
platformio settings set enable_telemetry No \
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
esphome:
|
||||
name: docker-test-esp8266-native
|
||||
|
||||
esp8266:
|
||||
board: d1_mini
|
||||
toolchain: arduino
|
||||
|
||||
logger:
|
||||
+47
-71
@@ -16,7 +16,6 @@ from typing import TYPE_CHECKING, Protocol
|
||||
# cause them to be loaded before external components are processed, resulting
|
||||
# in the built-in version being used instead of the external component one.
|
||||
from esphome import const, platform_hooks
|
||||
from esphome.build_helpers.native import analysis_backend, native_backend
|
||||
from esphome.const import (
|
||||
ALLOWED_NAME_CHARS,
|
||||
ARGUMENT_HELP_DEVICE,
|
||||
@@ -818,9 +817,7 @@ def write_cpp_file() -> int:
|
||||
from esphome.build_gen import espidf
|
||||
|
||||
espidf.write_project()
|
||||
elif not CORE.using_native_toolchain:
|
||||
# Other native builds generate their project at compile time;
|
||||
# never write a platformio.ini for them
|
||||
else:
|
||||
from esphome.build_gen import platformio
|
||||
|
||||
platformio.write_project()
|
||||
@@ -862,14 +859,20 @@ def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
|
||||
toolchain.create_factory_bin()
|
||||
toolchain.create_ota_bin()
|
||||
toolchain.create_elf_copy()
|
||||
from esphome.build_helpers.idedata import warn_if_idedata_missing
|
||||
from esphome.build_helpers.idedata import IDEDATA_BEST_EFFORT_ERRORS
|
||||
|
||||
warn_if_idedata_missing(toolchain.get_idedata)
|
||||
elif CORE.using_native_toolchain:
|
||||
raise EsphomeError(
|
||||
f"Toolchain '{CORE.toolchain.value}' resolved but no platform "
|
||||
"backend claimed the build"
|
||||
)
|
||||
try:
|
||||
if toolchain.get_idedata() is None:
|
||||
_LOGGER.warning("No idedata was generated for this build")
|
||||
except IDEDATA_BEST_EFFORT_ERRORS as err:
|
||||
# The firmware already built; an idedata failure must not fail
|
||||
# a successful build.
|
||||
_LOGGER.warning(
|
||||
"Could not generate idedata: %s (IDE, clang-tidy, and "
|
||||
"memory-analysis data will be unavailable for this build)",
|
||||
err,
|
||||
)
|
||||
_LOGGER.debug("Idedata failure detail", exc_info=True)
|
||||
else:
|
||||
from esphome.platformio import toolchain
|
||||
|
||||
@@ -972,15 +975,12 @@ def upload_using_esptool(
|
||||
|
||||
if file is not None:
|
||||
flash_images = [FlashImage(path=file, offset="0x0")]
|
||||
elif (native := native_backend()) is not None:
|
||||
# Every native backend supplies its own 0x0 flash image (bootloader
|
||||
# and partitions included where the target needs them)
|
||||
image = native.get_factory_firmware_path()
|
||||
if not image.is_file():
|
||||
raise EsphomeError(
|
||||
f"{image} does not exist; compile the configuration first"
|
||||
)
|
||||
flash_images = [FlashImage(path=image, offset="0x0")]
|
||||
elif CORE.using_toolchain_esp_idf:
|
||||
from esphome.espidf import toolchain
|
||||
|
||||
flash_images = [
|
||||
FlashImage(path=toolchain.get_factory_firmware_path(), offset="0x0")
|
||||
]
|
||||
else:
|
||||
from esphome.platformio import toolchain
|
||||
|
||||
@@ -1343,12 +1343,8 @@ def _upload_via_native_api(
|
||||
# fall back to a plaintext upload
|
||||
noise_psk = None
|
||||
plaintext_fallback = False
|
||||
allow_plaintext_upload = False
|
||||
if (encryption_conf := ota_conf.get(CONF_ENCRYPTION)) is not None:
|
||||
noise_psk = encryption_conf.get(CONF_KEY)
|
||||
allow_plaintext_upload = bool(
|
||||
encryption_conf.get(espota2.CONF_ALLOW_PLAINTEXT_UPLOAD)
|
||||
)
|
||||
if not noise_psk:
|
||||
raise EsphomeError(
|
||||
"OTA encryption is configured but no key was resolved; "
|
||||
@@ -1399,7 +1395,6 @@ def _upload_via_native_api(
|
||||
ota_type,
|
||||
noise_psk,
|
||||
plaintext_fallback=plaintext_fallback,
|
||||
allow_plaintext_upload=allow_plaintext_upload,
|
||||
)
|
||||
|
||||
|
||||
@@ -1714,12 +1709,26 @@ def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
if exit_code != 0:
|
||||
return exit_code
|
||||
if CORE.is_host:
|
||||
_LOGGER.info("Successfully compiled program to path '%s'", CORE.firmware_bin)
|
||||
_LOGGER.info(
|
||||
"Successfully compiled program to path '%s'", _host_program_path(config)
|
||||
)
|
||||
else:
|
||||
_LOGGER.info("Successfully compiled program.")
|
||||
return 0
|
||||
|
||||
|
||||
def _host_program_path(config: ConfigType) -> str:
|
||||
"""Return the compiled host ELF path."""
|
||||
if CORE.using_toolchain_esp_idf:
|
||||
from esphome.espidf import toolchain
|
||||
|
||||
return str(toolchain.get_elf_path())
|
||||
from esphome.platformio.toolchain import get_idedata
|
||||
|
||||
# Memoized by compile_program's own call; this is a dict lookup
|
||||
return str(get_idedata(config).firmware_elf_path)
|
||||
|
||||
|
||||
def command_upload(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
# Get devices, resolving special identifiers like OTA
|
||||
devices = choose_upload_log_host(
|
||||
@@ -1764,7 +1773,7 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
return exit_code
|
||||
_LOGGER.info("Successfully compiled program.")
|
||||
if CORE.is_host:
|
||||
program_path = str(CORE.firmware_bin)
|
||||
program_path = _host_program_path(config)
|
||||
_LOGGER.info("Running program from path '%s'", program_path)
|
||||
return run_external_process(program_path)
|
||||
|
||||
@@ -1956,12 +1965,12 @@ def command_update_all(args: ArgsProtocol) -> int | None:
|
||||
def command_idedata(args: ArgsProtocol, config: ConfigType) -> int:
|
||||
import json
|
||||
|
||||
native_toolchain = native_backend()
|
||||
if CORE.using_toolchain_esp_idf:
|
||||
# Native ESP-IDF derives idedata from the build's compile_commands.json,
|
||||
# so the configuration must already be compiled.
|
||||
from esphome.espidf import toolchain as espidf_toolchain
|
||||
|
||||
if native_toolchain is not None:
|
||||
# Native toolchains derive idedata from the build's
|
||||
# compile_commands.json, so the configuration must already be compiled.
|
||||
idedata = native_toolchain.get_idedata()
|
||||
idedata = espidf_toolchain.get_idedata()
|
||||
if idedata is None:
|
||||
_LOGGER.error(
|
||||
"No idedata available; compile the configuration first",
|
||||
@@ -2000,22 +2009,6 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
|
||||
from esphome.analyze_memory.cli import MemoryAnalyzerCLI
|
||||
from esphome.analyze_memory.ram_strings import RamStringsAnalyzer
|
||||
|
||||
# Refuse an unsupported toolchain before paying for a full compile
|
||||
analysis_toolchain = analysis_backend()
|
||||
if analysis_toolchain is None and not CORE.using_toolchain_platformio:
|
||||
_LOGGER.error(
|
||||
"analyze-memory is not supported with the '%s' toolchain on %s; "
|
||||
"re-run with --toolchain platformio",
|
||||
CORE.toolchain.value if CORE.toolchain else "unresolved",
|
||||
CORE.target_platform,
|
||||
)
|
||||
return 1
|
||||
if (
|
||||
check_supported := getattr(analysis_toolchain, "check_analysis_supported", None)
|
||||
) is not None:
|
||||
# Raises with the reason; before the compile, not after it
|
||||
check_supported()
|
||||
|
||||
# Always compile to ensure fresh data (fast if no changes - just relinks)
|
||||
exit_code = write_cpp(config)
|
||||
if exit_code != 0:
|
||||
@@ -2027,30 +2020,13 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
|
||||
|
||||
# Get idedata for analysis
|
||||
idedata = None
|
||||
if analysis_toolchain is not None:
|
||||
objdump = analysis_toolchain.get_objdump_path()
|
||||
readelf = analysis_toolchain.get_readelf_path()
|
||||
for tool in (objdump, readelf):
|
||||
if not tool.is_file():
|
||||
# The analyzer would silently fall back to host
|
||||
# binutils, which cannot read the target ELF
|
||||
_LOGGER.error(
|
||||
"%s is missing; the toolchain install may be incomplete "
|
||||
"(recompile, or run 'esphome clean-all' if it persists)",
|
||||
tool,
|
||||
)
|
||||
return 1
|
||||
objdump_path = str(objdump)
|
||||
readelf_path = str(readelf)
|
||||
if CORE.using_toolchain_esp_idf:
|
||||
from esphome.espidf import toolchain
|
||||
|
||||
firmware_elf = analysis_toolchain.get_elf_path()
|
||||
if not firmware_elf.is_file():
|
||||
# The analyzer swallows tool failures, so a missing ELF would
|
||||
# produce an exit-0 zeroed report
|
||||
_LOGGER.error(
|
||||
"%s is missing; compile the configuration first", firmware_elf
|
||||
)
|
||||
return 1
|
||||
objdump_path = str(toolchain.get_objdump_path())
|
||||
readelf_path = str(toolchain.get_readelf_path())
|
||||
|
||||
firmware_elf = toolchain.get_elf_path()
|
||||
else:
|
||||
from esphome.platformio import toolchain
|
||||
|
||||
|
||||
@@ -68,16 +68,12 @@ def idedata_candidates(build_path: Path) -> list[Path]:
|
||||
The candidate idedata JSON paths, most specific first
|
||||
"""
|
||||
name = build_path.name
|
||||
data_dir = build_path.parent.parent / "idedata"
|
||||
# Native backends suffix the cache by toolchain (<name>.arduino.json)
|
||||
suffixed = sorted(data_dir.glob(f"{name}.*.json")) if data_dir.is_dir() else []
|
||||
return [
|
||||
# In .pioenvs for test builds
|
||||
build_path / ".pioenvs" / name / "idedata.json",
|
||||
# Both toolchains cache it in the data dir, which holds this build dir:
|
||||
# <data_dir>/idedata/<name>.json next to <data_dir>/build/<name>
|
||||
build_path.parent.parent / "idedata" / f"{name}.json",
|
||||
*suffixed,
|
||||
# Regular builds, invoked from the config dir or from anywhere
|
||||
Path.cwd() / ".esphome" / "idedata" / f"{name}.json",
|
||||
Path.home() / ".esphome" / "idedata" / f"{name}.json",
|
||||
|
||||
+15
-33
@@ -2,9 +2,7 @@
|
||||
|
||||
Bundled names build straight from the framework tree; everything else goes
|
||||
through ``esphome.platformio.library``. Mirrors ``lib_ldf_mode=off``: each
|
||||
library builds its own archive; all include dirs join one global path. The
|
||||
host build reuses it without a framework tree: nothing is bundled there and
|
||||
every name resolves from the registry.
|
||||
library builds its own archive; all include dirs join one global path.
|
||||
|
||||
Deviations from PlatformIO: flat-layout libraries get the recursive default
|
||||
source filter; ``dot_a_linkage`` is honored; bundled libraries never run a
|
||||
@@ -344,24 +342,13 @@ def _check_unfulfilled_provides(
|
||||
|
||||
|
||||
def resolve_libraries(
|
||||
framework_path: Path | None,
|
||||
*,
|
||||
pio_platform: str,
|
||||
board_mcu: str,
|
||||
cache_key: str,
|
||||
framework: str | None = "arduino",
|
||||
manifest_optional: bool = False,
|
||||
framework_path: Path, *, pio_platform: str, board_mcu: str, cache_key: str
|
||||
) -> list[ArduinoLibrary]:
|
||||
"""Resolve every ``cg.add_library()`` entry into an :class:`ArduinoLibrary`.
|
||||
|
||||
``pio_platform``/``board_mcu`` filter manifests the way PlatformIO would
|
||||
for that core (e.g. ``espressif8266``/``esp8266``); ``cache_key`` keys the
|
||||
shared converter's download cache. ``framework`` is the manifest
|
||||
framework token the compatibility check warns about; None skips it.
|
||||
A None ``framework_path`` means no core-bundled libraries exist (the
|
||||
host build): every name resolves from the registry.
|
||||
``manifest_optional`` accepts libraries without a manifest, built with
|
||||
PlatformIO's default layout.
|
||||
shared converter's download cache.
|
||||
|
||||
The returned list is not topologically sorted, so the caller must link
|
||||
the archives inside one ``--start-group``/``--end-group`` pair (the
|
||||
@@ -372,22 +359,18 @@ def resolve_libraries(
|
||||
# PlatformIO's lib_ignore covers framework-bundled libraries too; the
|
||||
# shared converter only filters the registry/git ones.
|
||||
lib_ignore = lib_ignore_set()
|
||||
bundled_dir_names: frozenset[str] = frozenset()
|
||||
if framework_path is not None:
|
||||
# Exact directory names keep membership case-sensitive everywhere
|
||||
# (an is_dir() probe would match "wire" on macOS/Windows and build
|
||||
# the bundled Wire twice)
|
||||
libraries_dir = framework_path / "libraries"
|
||||
if not libraries_dir.is_dir():
|
||||
# A registry fallback would fail later with a misleading
|
||||
# package-not-found error per bundled name
|
||||
raise EsphomeError(
|
||||
f"{libraries_dir} is missing; the framework install may be "
|
||||
"incomplete (run 'esphome clean-all')"
|
||||
)
|
||||
bundled_dir_names = frozenset(
|
||||
p.name for p in libraries_dir.iterdir() if p.is_dir()
|
||||
# Exact directory names keep membership case-sensitive everywhere
|
||||
# (an is_dir() probe would match "wire" on macOS/Windows and build
|
||||
# the bundled Wire twice)
|
||||
libraries_dir = framework_path / "libraries"
|
||||
if not libraries_dir.is_dir():
|
||||
# A registry fallback would fail later with a misleading
|
||||
# package-not-found error per bundled name
|
||||
raise EsphomeError(
|
||||
f"{libraries_dir} is missing; the framework install may be "
|
||||
"incomplete (run 'esphome clean-all')"
|
||||
)
|
||||
bundled_dir_names = frozenset(p.name for p in libraries_dir.iterdir() if p.is_dir())
|
||||
|
||||
def _provided(name: object) -> bool:
|
||||
return _is_safe_library_name(name) and name in bundled_dir_names
|
||||
@@ -514,13 +497,12 @@ def resolve_libraries(
|
||||
|
||||
backend = LibraryBackend(
|
||||
platform=pio_platform,
|
||||
framework=framework,
|
||||
framework="arduino",
|
||||
emit=_emit,
|
||||
cache_key=cache_key,
|
||||
# The walk must not resolve bundled names from the registry;
|
||||
# _add_bundled_dependencies adds them after emit
|
||||
provides=_provided,
|
||||
manifest_optional=manifest_optional,
|
||||
)
|
||||
if external:
|
||||
convert_libraries(external, backend)
|
||||
|
||||
@@ -17,7 +17,7 @@ import os
|
||||
from pathlib import Path
|
||||
from typing import NamedTuple
|
||||
|
||||
from esphome.build_helpers.ccache import ccache_env
|
||||
from esphome.build_helpers.ccache import ccache_defaults_env
|
||||
from esphome.build_helpers.ninja import find_ninja
|
||||
from esphome.build_helpers.tools_cache import ARDUINO8266_TOOLS_CACHE, tools_cache_path
|
||||
from esphome.core import EsphomeError, Version
|
||||
@@ -143,5 +143,17 @@ def get_build_env(toolchain_path: Path, ccache: str | None) -> dict[str, str]:
|
||||
*filter(None, env.get("PATH", "").split(os.pathsep)),
|
||||
]
|
||||
env["PATH"] = os.pathsep.join(parts)
|
||||
env.update(ccache_env(ccache, ARDUINO8266_TOOLS_CACHE))
|
||||
env.update(ccache_env(ccache))
|
||||
return env
|
||||
|
||||
|
||||
def ccache_env(ccache: str | None) -> dict[str, str]:
|
||||
"""Return ccache settings for the build subprocess (not os.environ).
|
||||
|
||||
``ccache`` is the pre-resolved binary (resolve_ccache_path), or None
|
||||
when disabled. Values the user already set in the environment are
|
||||
respected.
|
||||
"""
|
||||
if ccache is None:
|
||||
return {}
|
||||
return ccache_defaults_env(get_arduino8266_tools_path() / "ccache")
|
||||
@@ -1,212 +0,0 @@
|
||||
"""Native Arduino ESP8266 build driver (the PlatformIO ``run`` equivalent)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from esphome.build_helpers.ccache import resolve_absolute_ccache_path
|
||||
from esphome.build_helpers.native import warn_ignored_platformio_options
|
||||
from esphome.build_helpers.ninja import refresh_compile_commands
|
||||
from esphome.const import (
|
||||
CONF_COMPILE_PROCESS_LIMIT,
|
||||
CONF_ESPHOME,
|
||||
KEY_CORE,
|
||||
KEY_FRAMEWORK_VERSION,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from esphome.arduino8266.framework import InstalledPaths
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# ESP8266 user RAM (matches upload.maximum_ram_size in every board manifest)
|
||||
_MAX_RAM_SIZE = 81920
|
||||
|
||||
|
||||
_RAM_SECTIONS = (".data", ".rodata", ".bss")
|
||||
_FLASH_SECTIONS = (".irom0.text", ".text", ".text1", ".data", ".rodata")
|
||||
|
||||
|
||||
def get_build_dir() -> Path:
|
||||
return CORE.relative_pioenvs_path(CORE.name)
|
||||
|
||||
|
||||
def get_elf_path() -> Path:
|
||||
return get_build_dir() / "firmware.elf"
|
||||
|
||||
|
||||
def _toolchain_tool(name: str) -> Path:
|
||||
# Imported here, not at module scope: the serial upload/logs fast path
|
||||
# resolves this module for its artifact paths alone, and framework
|
||||
# pulls in the whole package-download stack
|
||||
from esphome.arduino8266 import framework
|
||||
|
||||
return framework.toolchain_tool(framework.get_toolchain_path(), name)
|
||||
|
||||
|
||||
def get_factory_firmware_path() -> Path:
|
||||
"""The image to serial-flash at 0x0 (same bytes as firmware.bin: the
|
||||
8266 factory copy exists for artifact-contract parity, not content)."""
|
||||
return get_build_dir() / "firmware.factory.bin"
|
||||
|
||||
|
||||
def get_addr2line_path() -> Path:
|
||||
return _toolchain_tool("addr2line")
|
||||
|
||||
|
||||
def get_objdump_path() -> Path:
|
||||
return _toolchain_tool("objdump")
|
||||
|
||||
|
||||
def get_readelf_path() -> Path:
|
||||
return _toolchain_tool("readelf")
|
||||
|
||||
|
||||
def run_compile(config: ConfigType, verbose: bool) -> int:
|
||||
from esphome.arduino8266 import framework
|
||||
from esphome.build_gen import arduino8266 as build_gen
|
||||
from esphome.core.config import NATIVE_ARDUINO_CONSUMED_PIO_OPTIONS
|
||||
|
||||
warn_ignored_platformio_options(NATIVE_ARDUINO_CONSUMED_PIO_OPTIONS)
|
||||
paths = framework.check_and_install(CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION])
|
||||
# Resolved once: the probe is not free and three consumers need it
|
||||
ccache = resolve_absolute_ccache_path()
|
||||
ninja_changed = build_gen.write_project(paths, ccache)
|
||||
|
||||
build_dir = get_build_dir()
|
||||
env = framework.get_build_env(paths.toolchain, ccache)
|
||||
refresh_compile_commands(paths.ninja, build_dir, env, ninja_changed)
|
||||
|
||||
cmd = [str(paths.ninja)]
|
||||
if verbose:
|
||||
cmd.append("-v")
|
||||
if jobs := config[CONF_ESPHOME].get(CONF_COMPILE_PROCESS_LIMIT):
|
||||
cmd += ["-j", str(jobs)]
|
||||
# Explicit targets: a generator defect that drops them fails loudly
|
||||
# instead of a green no-op run leaving stale artifacts in place
|
||||
targets = ["firmware.factory.bin", "firmware.ota.bin"]
|
||||
cmd += targets
|
||||
|
||||
# cwd, not -C: drops ninja's "Entering directory" banner
|
||||
_LOGGER.debug("Running: %s", " ".join(cmd))
|
||||
rc = subprocess.run(
|
||||
cmd, cwd=build_dir, env=env, check=False, close_fds=False
|
||||
).returncode
|
||||
if rc != 0:
|
||||
return rc
|
||||
|
||||
# ninja already refused missing targets; existence covers a rule that
|
||||
# ran but wrote elsewhere
|
||||
build_dir_artifacts = (
|
||||
get_elf_path(),
|
||||
build_dir / "firmware.bin",
|
||||
get_factory_firmware_path(),
|
||||
build_dir / "firmware.ota.bin",
|
||||
)
|
||||
for artifact in build_dir_artifacts:
|
||||
if not artifact.is_file():
|
||||
_LOGGER.error("Build produced no %s", artifact)
|
||||
return 1
|
||||
|
||||
if not _print_size_summary(build_dir, paths):
|
||||
# Cause already warned; name the consequence for CI harnesses
|
||||
_LOGGER.warning("Firmware size summary unavailable for this build")
|
||||
from esphome.build_helpers.idedata import warn_if_idedata_missing
|
||||
|
||||
warn_if_idedata_missing(lambda: get_idedata(ccache))
|
||||
return 0
|
||||
|
||||
|
||||
def _parse_app_size(build_dir: Path, paths: InstalledPaths) -> int | None:
|
||||
"""Read the app flash budget (irom0_0_seg length) from the linker script."""
|
||||
from esphome.build_gen.arduino8266 import get_flash_ld_path
|
||||
from esphome.components.esp8266.build_surgery import segment_length
|
||||
|
||||
# Warnings, not debug: without the app size the Flash summary line is
|
||||
# dropped and CI's memory-impact extraction loses its flash metric.
|
||||
ld_path = get_flash_ld_path(build_dir, paths)
|
||||
try:
|
||||
ld_text = ld_path.read_text(encoding="utf-8")
|
||||
except (OSError, UnicodeDecodeError) as err:
|
||||
# A corrupt script degrades the same way, never aborts the build
|
||||
_LOGGER.warning("Cannot read linker script for the Flash summary: %s", err)
|
||||
return None
|
||||
if not (app_size := segment_length(ld_text, "irom0_0_seg")):
|
||||
_LOGGER.warning("No usable irom0_0_seg in %s; skipping Flash summary", ld_path)
|
||||
return None
|
||||
return app_size
|
||||
|
||||
|
||||
def _print_size_summary(build_dir: Path, paths: InstalledPaths) -> bool:
|
||||
"""Print the RAM/Flash lines ``ci_memory_impact_extract.py`` parses;
|
||||
False when skipped."""
|
||||
from esphome.arduino8266.framework import toolchain_tool
|
||||
from esphome.build_helpers.size_summary import print_size_line
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[
|
||||
str(toolchain_tool(paths.toolchain, "size")),
|
||||
"-A",
|
||||
"-d",
|
||||
str(build_dir / "firmware.elf"),
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
close_fds=False,
|
||||
)
|
||||
except (OSError, subprocess.CalledProcessError) as err:
|
||||
# The summary is a bonus artifact like idedata; a truncated
|
||||
# toolchain extraction must not discard an already-linked build
|
||||
_LOGGER.warning("Could not summarize firmware size: %s", err)
|
||||
return False
|
||||
# -d prints decimal sizes; anything else trips the missing-sections guard
|
||||
sections = {
|
||||
parts[0]: int(parts[1])
|
||||
for line in result.stdout.splitlines()
|
||||
if (parts := line.split())[:1] and parts[0].startswith(".") and len(parts) >= 2
|
||||
if parts[1].isdigit()
|
||||
}
|
||||
if missing := set(_RAM_SECTIONS + _FLASH_SECTIONS) - set(sections):
|
||||
# A defaulted 0 would print a confidently wrong total for CI's metric
|
||||
_LOGGER.warning(
|
||||
"Size output is missing section(s) %s; skipping the size summary",
|
||||
", ".join(sorted(missing)),
|
||||
)
|
||||
return False
|
||||
# Resolve the flash budget before printing: a RAM line without its
|
||||
# Flash line would skew CI's memory-impact extraction
|
||||
app_size = _parse_app_size(build_dir, paths)
|
||||
if not app_size:
|
||||
return False
|
||||
ram = sum(sections[s] for s in _RAM_SECTIONS)
|
||||
flash = sum(sections[s] for s in _FLASH_SECTIONS)
|
||||
print_size_line("RAM", ram, _MAX_RAM_SIZE)
|
||||
print_size_line("Flash", flash, app_size)
|
||||
return True
|
||||
|
||||
|
||||
def get_idedata(ccache: str | None = None) -> dict | None:
|
||||
"""Derive idedata from the build's compile_commands.json (same
|
||||
contract as ``espidf.toolchain.get_idedata``)."""
|
||||
from esphome.build_helpers.idedata import load_or_build_idedata
|
||||
|
||||
# A disabled ccache resolves to None without spawning anything, so
|
||||
# re-resolving here costs nothing when the caller has no answer
|
||||
launcher = ccache or resolve_absolute_ccache_path()
|
||||
return load_or_build_idedata(
|
||||
get_build_dir() / "compile_commands.json",
|
||||
get_elf_path(),
|
||||
# Suffixed so a platformio->arduino->platformio round trip on one
|
||||
# config never serves the other toolchain's cache shape
|
||||
CORE.relative_internal_path("idedata", f"{CORE.name}.arduino.json"),
|
||||
# The compile DB's commands carry the same ccache prefix the ninja
|
||||
# rules were generated with
|
||||
launcher=str(launcher) if launcher else None,
|
||||
)
|
||||
+18
-35
@@ -239,9 +239,9 @@ class ApplyCall:
|
||||
|
||||
Each arg is ``(conf_key, type_)`` or ``(conf_key, type_, const_fn)``. A ``conf_key`` may be a
|
||||
path into nested sections. A plain ``str`` ``type_`` is raw C++ type text and may use
|
||||
``{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
|
||||
has no keys, and a partial set is a config error.
|
||||
``{parent}``. ``const_fn(config, value)`` renders a constant's argument text; a lambda 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.
|
||||
"""
|
||||
|
||||
target: str
|
||||
@@ -280,8 +280,8 @@ class ApplyField:
|
||||
Double a literal brace in a template. ``conf_key`` may be a path into nested sections.
|
||||
``type_`` may be a C++ type string using ``{parent}`` when the type is only known per
|
||||
instance. ``const_fn(config, value)`` renders a constant's argument text when ``cg.safe_exp``
|
||||
is not the right spelling (unit conversion belongs in the validator); a lambda or an id
|
||||
bypasses it, so the target must also take a plain ``type_``. An absent key emits nothing.
|
||||
is not the right spelling (unit conversion belongs in the validator); a lambda bypasses it,
|
||||
so the target must also take a plain ``type_``. An absent key emits nothing.
|
||||
"""
|
||||
|
||||
conf_key: str | tuple[str, ...]
|
||||
@@ -345,28 +345,14 @@ async def _apply_parent(config: ConfigType, id_key: str = CONF_ID) -> str:
|
||||
|
||||
|
||||
def _apply_lambda_args(args: TemplateArgsType) -> TemplateArgsType:
|
||||
# The generated function's parameters; a std::string arg is never copied.
|
||||
# Must match ApplyAction::ApplyFn and ApplyCondition::CheckFn exactly for the function
|
||||
# pointer conversion.
|
||||
return [
|
||||
(cg.RawExpression(f"const std::remove_cvref_t<{cg.safe_exp(t)}> &"), arg)
|
||||
for t, arg in args
|
||||
]
|
||||
|
||||
|
||||
def _apply_function(
|
||||
id_: ID,
|
||||
return_type: SafeExpType,
|
||||
template_arg: cg.TemplateArguments,
|
||||
lambda_args: TemplateArgsType,
|
||||
statements: list[str],
|
||||
) -> MockObj:
|
||||
"""Emit the generated function and declare ``id_`` as the ``ApplyAction`` or
|
||||
``ApplyCondition`` templated on it, so ``play()`` calls it directly."""
|
||||
fn = cg.static_function(
|
||||
f"esphome__{id_.id}__fn", return_type, lambda_args, statements
|
||||
)
|
||||
return cg.new_Pvariable(id_, cg.TemplateArguments(fn, *template_arg))
|
||||
|
||||
|
||||
async def _render_values(
|
||||
name: str,
|
||||
target: str,
|
||||
@@ -394,9 +380,6 @@ async def _render_values(
|
||||
expr = call_lambda(inner)
|
||||
bare = compare and isinstance(expr, cg.RawExpression)
|
||||
exprs.append(f"({expr})" if bare else str(expr))
|
||||
elif isinstance(value, ID):
|
||||
# Qualified like the parent, so a trigger arg named like the id cannot shadow it.
|
||||
exprs.append(f"::{await cg.get_variable(value)}")
|
||||
elif const_fn is not None:
|
||||
exprs.append(const_fn(config, value))
|
||||
else:
|
||||
@@ -417,9 +400,8 @@ def register_apply_action(
|
||||
) -> None:
|
||||
"""Register an action that only forwards config values to its parent, with no C++ class.
|
||||
|
||||
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.
|
||||
A constant that is an id (``cv.use_id`` under ``cv.templatable``) is the object it names.
|
||||
Generates one stateless function for ``ApplyAction<Ts...>``: the parent (read from
|
||||
``id_key``) and constants are baked in, lambdas are called inline with the trigger args.
|
||||
With ``call`` every statement targets the call object ``auto apply_call = parent->call()``,
|
||||
and ``apply_call.perform()`` is appended.
|
||||
"""
|
||||
@@ -463,9 +445,10 @@ def register_apply_action(
|
||||
*statements,
|
||||
"apply_call.perform();",
|
||||
]
|
||||
return _apply_function(
|
||||
action_id, cg.void, template_arg, lambda_args, statements
|
||||
apply_lambda = LambdaExpression(
|
||||
["\n".join(statements)], lambda_args, capture="", return_type=cg.void
|
||||
)
|
||||
return cg.new_Pvariable(action_id, template_arg, apply_lambda)
|
||||
|
||||
register_action(name, ApplyAction, schema, synchronous=True)(builder)
|
||||
|
||||
@@ -479,7 +462,7 @@ def register_apply_condition(
|
||||
``ApplyCall`` such as ``ApplyCall("state == {}", ((CONF_STATE, cg.bool_),))`` compares
|
||||
against config values, all of which must be present. Write ``== false`` to negate.
|
||||
String constants are plain literals, so compare a ``std::string`` or ``StringRef`` member.
|
||||
Generates one static predicate and an ``ApplyCondition`` templated on it.
|
||||
Generates one stateless function for ``ApplyCondition<Ts...>``.
|
||||
"""
|
||||
call = check if isinstance(check, ApplyCall) else ApplyCall(check)
|
||||
members = call.members
|
||||
@@ -505,13 +488,13 @@ def register_apply_condition(
|
||||
lambda_args,
|
||||
compare=True,
|
||||
)
|
||||
return _apply_function(
|
||||
condition_id,
|
||||
cg.bool_,
|
||||
template_arg,
|
||||
lambda_args,
|
||||
check_lambda = LambdaExpression(
|
||||
[f"return {parent}->{call.target.format(*exprs)};"],
|
||||
lambda_args,
|
||||
capture="",
|
||||
return_type=cg.bool_,
|
||||
)
|
||||
return cg.new_Pvariable(condition_id, template_arg, check_lambda)
|
||||
|
||||
register_condition(name, ApplyCondition, schema)(builder)
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ from the build flags with the same precedence as the PlatformIO builder.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Collection
|
||||
from dataclasses import dataclass
|
||||
import hashlib
|
||||
import logging
|
||||
@@ -22,26 +23,15 @@ from pathlib import Path
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import TYPE_CHECKING, NamedTuple
|
||||
|
||||
from esphome.arduino8266.framework import toolchain_tool
|
||||
from esphome.build_helpers.idedata import is_joined_include
|
||||
from esphome.build_helpers.ninja import (
|
||||
escape as _e,
|
||||
quote_path as _q,
|
||||
shell_token as _shell_token,
|
||||
)
|
||||
from esphome.build_helpers.ninja_gen import (
|
||||
ar_rule_lines,
|
||||
collect_sources,
|
||||
compile_edges,
|
||||
compile_rule_lines,
|
||||
library_edges,
|
||||
pch_edges,
|
||||
pch_rule_lines,
|
||||
tool_lines,
|
||||
)
|
||||
from esphome.build_helpers.pch import PCH_CORE_HEADER
|
||||
from esphome.components.esp8266 import build_surgery
|
||||
from esphome.components.esp8266.boards import (
|
||||
BOARDS,
|
||||
@@ -56,9 +46,12 @@ from esphome.components.esp8266.const import (
|
||||
KEY_SCANF_FLOAT,
|
||||
)
|
||||
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,
|
||||
strip_win_long_path_prefix,
|
||||
)
|
||||
from esphome.helpers import mkdir_p, write_file_if_changed
|
||||
from esphome.platformio.library import lex_build_flags
|
||||
from esphome.platformio.library import SOURCE_KIND_FOR_SUFFIX, lex_build_flags
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from esphome.arduino8266.framework import InstalledPaths
|
||||
@@ -650,6 +643,14 @@ _PLAIN_LINKER_PREFIXES = ("-T", "-Xlinker")
|
||||
_PLAIN_DRIVER_LINK_PREFIXES = ("-fuse-ld=", "--specs=", "-specs=")
|
||||
|
||||
|
||||
def _collect_sources(root: Path, exclude: Collection[str] = frozenset()) -> list[Path]:
|
||||
return sorted(
|
||||
p
|
||||
for p in root.rglob("*")
|
||||
if p.suffix in SOURCE_KIND_FOR_SUFFIX and p.name not in exclude
|
||||
)
|
||||
|
||||
|
||||
def _stat_sig(path: Path) -> str:
|
||||
"""Size and mtime cache-stamp signature for one input file.
|
||||
|
||||
@@ -867,6 +868,34 @@ def _generate_testing_flash_ld(
|
||||
)
|
||||
|
||||
|
||||
def _ninja_compile_edges(
|
||||
lines: list[str],
|
||||
sources: list[Path],
|
||||
root: Path,
|
||||
group: str,
|
||||
flags: str = "",
|
||||
) -> list[str]:
|
||||
"""Emit compile edges for ``sources``; return the object paths."""
|
||||
objects = []
|
||||
for src in sources:
|
||||
rel = src.relative_to(root).as_posix()
|
||||
obj = f"obj/{group}/{rel}.o"
|
||||
escaped_obj = _e(obj)
|
||||
lines.append(
|
||||
f"build {escaped_obj}: {SOURCE_KIND_FOR_SUFFIX[src.suffix]} {_e(src)}"
|
||||
)
|
||||
if flags:
|
||||
lines.append(f" flags = {flags}")
|
||||
# Escaped once here: the returned paths only ever appear in build
|
||||
# statements (archive/link inputs), which use ninja escaping.
|
||||
objects.append(escaped_obj)
|
||||
return objects
|
||||
|
||||
|
||||
def _common_parent(paths: list[Path]) -> Path:
|
||||
return Path(os.path.commonpath([str(p.parent) for p in paths]))
|
||||
|
||||
|
||||
class _FlagSets(NamedTuple):
|
||||
cflags: list[str]
|
||||
cxxflags: list[str]
|
||||
@@ -1053,15 +1082,48 @@ def write_project(paths: InstalledPaths, ccache: str | None) -> bool:
|
||||
+ ["stdc++-exc" if config.exceptions else "stdc++", "m", "c", "gcc"]
|
||||
)
|
||||
|
||||
build_tool = Path(__file__).parent / "build_tool.py"
|
||||
|
||||
# $in/$out stay unquoted: ninja escapes its built-in path variables
|
||||
# itself; only literal paths need _q().
|
||||
lines = [
|
||||
*tool_lines(
|
||||
(toolchain_tool(paths.toolchain, "gcc"),),
|
||||
(toolchain_tool(paths.toolchain, "g++"),),
|
||||
ccache,
|
||||
),
|
||||
*compile_rule_lines(),
|
||||
*ar_rule_lines(toolchain_tool(paths.toolchain, "ar")),
|
||||
*pch_rule_lines(),
|
||||
"# Auto-generated by ESPHome",
|
||||
"ninja_required_version = 1.5",
|
||||
f"cc = {_q(toolchain_tool(paths.toolchain, 'gcc'))}",
|
||||
f"cxx = {_q(toolchain_tool(paths.toolchain, 'g++'))}",
|
||||
# The NSIS launcher starts Python with a \\?\ extended-length path
|
||||
# that cmd.exe cannot spawn; same strip every other emitted binary
|
||||
# path gets
|
||||
f"python = {_q(strip_win_long_path_prefix(sys.executable))}",
|
||||
f"buildtool = {_q(build_tool)}",
|
||||
f"ccache = {_q(ccache) if ccache else ''}",
|
||||
"",
|
||||
# Rule names match SOURCE_KIND_FOR_SUFFIX values (c, cxx, asm, aspp)
|
||||
"rule c",
|
||||
" command = $ccache $cc -MMD -MF $out.d $cflags $flags -c $in -o $out",
|
||||
" depfile = $out.d",
|
||||
" deps = gcc",
|
||||
" description = CC $out",
|
||||
"rule cxx",
|
||||
" command = $ccache $cxx -MMD -MF $out.d $cxxflags $flags -c $in -o $out",
|
||||
" depfile = $out.d",
|
||||
" deps = gcc",
|
||||
" description = CXX $out",
|
||||
"rule aspp",
|
||||
" command = $ccache $cc -MMD -MF $out.d -x assembler-with-cpp $asflags $flags -c $in -o $out",
|
||||
" depfile = $out.d",
|
||||
" deps = gcc",
|
||||
" description = AS $out",
|
||||
# Plain assembler, as SCons's ASCOM: no preprocessor, so no
|
||||
# depfile and no $flags (defines/includes) either
|
||||
"rule asm",
|
||||
" command = $ccache $cc -x assembler $asflags -c $in -o $out",
|
||||
" description = AS $out",
|
||||
"rule ar",
|
||||
f" command = $python $buildtool ar {_q(toolchain_tool(paths.toolchain, 'ar'))} $out $out.rsp",
|
||||
" rspfile = $out.rsp",
|
||||
" rspfile_content = $in_newline",
|
||||
" description = AR $out",
|
||||
"rule link",
|
||||
" command = $cxx -o $out $linkflags @$out.rsp $libdirflags -Wl,--start-group $archives $libflags -Wl,--end-group",
|
||||
" rspfile = $out.rsp",
|
||||
@@ -1070,8 +1132,7 @@ def write_project(paths: InstalledPaths, ccache: str | None) -> bool:
|
||||
"rule elf2bin",
|
||||
# --flash_size deliberately stays board-derived, as under
|
||||
# PlatformIO (which reads upload.maximum_size, not the ldscript).
|
||||
# -W: the framework's own elf2bin.py trips SyntaxWarning on 3.12+.
|
||||
f" command = $python -W ignore::SyntaxWarning {_q(framework / 'tools' / 'elf2bin.py')} --eboot {_q(framework / 'bootloaders' / 'eboot' / 'eboot.elf')} --app $in --flash_mode {flash_mode} --flash_freq {_FLASH_FREQ_MHZ} --flash_size {_flash_size_str(BOARDS[board][KEY_FLASH_SIZE])} --path {_q(toolchain_bin)} --out $out",
|
||||
f" command = $python {_q(framework / 'tools' / 'elf2bin.py')} --eboot {_q(framework / 'bootloaders' / 'eboot' / 'eboot.elf')} --app $in --flash_mode {flash_mode} --flash_freq {_FLASH_FREQ_MHZ} --flash_size {_flash_size_str(BOARDS[board][KEY_FLASH_SIZE])} --path {_q(toolchain_bin)} --out $out",
|
||||
" description = BIN $out",
|
||||
"rule copy",
|
||||
" command = $python $buildtool copy $in $out",
|
||||
@@ -1091,15 +1152,16 @@ def write_project(paths: InstalledPaths, ccache: str | None) -> bool:
|
||||
core_exclude |= _CORE_EXCLUDE_WAVEFORM
|
||||
|
||||
archives = []
|
||||
direct_objs: list[str] = []
|
||||
# variant_dir existence was already enforced with the include dirs
|
||||
variant_sources = collect_sources(variant_dir)
|
||||
variant_sources = _collect_sources(variant_dir)
|
||||
if variant_sources:
|
||||
objs = compile_edges(lines, variant_sources, variant_dir, "variant")
|
||||
objs = _ninja_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"
|
||||
core_objs = _ninja_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
|
||||
@@ -1110,14 +1172,37 @@ def write_project(paths: InstalledPaths, ccache: str | None) -> bool:
|
||||
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
|
||||
for lib in libraries:
|
||||
if not lib.sources:
|
||||
# Header-only libraries are legitimate; the log makes an empty
|
||||
# srcFilter or broken tree traceable before link errors do.
|
||||
_LOGGER.debug(
|
||||
"Library %s has no source files; contributing includes only",
|
||||
lib.name,
|
||||
)
|
||||
continue
|
||||
lib_root = _common_parent(lib.sources)
|
||||
objs = _ninja_compile_edges(
|
||||
lines,
|
||||
lib.sources,
|
||||
lib_root,
|
||||
f"lib/{lib.name}",
|
||||
flags=" ".join(_shell_token(f) for f in lib.flags),
|
||||
)
|
||||
if not lib.lib_archive:
|
||||
# libArchive: false / dot_a_linkage=false: hand the objects to
|
||||
# the linker directly so unreferenced-but-required symbols
|
||||
# (exception handlers, weak overrides) survive
|
||||
direct_objs.extend(objs)
|
||||
continue
|
||||
archive = f"lib{lib.name}.a"
|
||||
lines.append(f"build {_e(archive)}: ar {' '.join(objs)}")
|
||||
archives.append(archive)
|
||||
|
||||
# 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_includes: list[str] = []
|
||||
src_parts: list[str] = []
|
||||
src_it = iter(
|
||||
lex_build_flags(_pio_option("build_src_flags", ""), "build_src_flags")
|
||||
)
|
||||
@@ -1128,34 +1213,15 @@ def write_project(paths: InstalledPaths, ccache: str | None) -> bool:
|
||||
raise EsphomeError(
|
||||
"build_src_flags has a trailing '-include' with no header"
|
||||
)
|
||||
src_includes.append(header)
|
||||
elif is_joined_include(tok):
|
||||
# Left in src_other it would precede the pch include and
|
||||
# silently defeat the .gch
|
||||
src_includes.append(tok[len("-include") :])
|
||||
src_parts.append(f"-include {_q(src_dir / header)}")
|
||||
else:
|
||||
src_other.append(_shell_token(tok))
|
||||
include_flags = [f"-include {_q(src_dir / h)}" for h in src_includes]
|
||||
src_parts.append(_shell_token(tok))
|
||||
src_extra = " ".join(src_parts)
|
||||
# One shared variable instead of repeating the flags line on every src
|
||||
# edge (hundreds of edges in a real project)
|
||||
lines.append(f"srcflags = {' '.join(src_other + include_flags)}")
|
||||
src_cxx_override = pch_edges(
|
||||
lines,
|
||||
build_dir,
|
||||
src_dir,
|
||||
(*src_includes, PCH_CORE_HEADER),
|
||||
flag_sets.cxxflags,
|
||||
src_other,
|
||||
ccache,
|
||||
(str(paths.framework), str(paths.toolchain)),
|
||||
)
|
||||
src_objs = compile_edges(
|
||||
lines,
|
||||
collect_sources(src_dir),
|
||||
src_dir,
|
||||
"src",
|
||||
flags="$srcflags",
|
||||
cxx_override=src_cxx_override,
|
||||
lines.append(f"srcflags = {src_extra}")
|
||||
src_objs = _ninja_compile_edges(
|
||||
lines, _collect_sources(src_dir), src_dir, "src", flags="$srcflags"
|
||||
)
|
||||
|
||||
ld_deps = [f"ld/{_COMMON_LD_NAME}"]
|
||||
|
||||
@@ -6,7 +6,6 @@ started esphome and must not depend on the package being importable.
|
||||
Subcommands:
|
||||
ar <ar-binary> <archive> <rspfile> remove stale archive, then ``ar rcs``
|
||||
copy <src> <dst> copy a file
|
||||
touch <path> create/update a stamp file
|
||||
|
||||
The ar rspfile carries one object path per line (the generating rule must
|
||||
use ``$in_newline``, never ``$in``).
|
||||
@@ -84,18 +83,9 @@ def _run_copy(src: str, dst: str) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def _run_touch(path: str) -> int:
|
||||
try:
|
||||
Path(path).touch()
|
||||
except OSError as err:
|
||||
print(f"touch: {path} failed: {err}", file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
# mode -> (handler, expected operand count); surplus argv means a
|
||||
# mis-specified ninja rule and must error, not silently drop operands
|
||||
_MODES = {"ar": (_run_ar, 3), "copy": (_run_copy, 2), "touch": (_run_touch, 1)}
|
||||
_MODES = {"ar": (_run_ar, 3), "copy": (_run_copy, 2)}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
|
||||
@@ -4,14 +4,6 @@ import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from esphome.build_helpers import pch
|
||||
from esphome.build_helpers.pch import (
|
||||
PCH_DEFAULT_HEADERS,
|
||||
PCH_HEADER_NAME,
|
||||
mark_pch_emitted,
|
||||
pch_enabled,
|
||||
pch_header_text,
|
||||
)
|
||||
from esphome.components.esp32 import (
|
||||
get_esp32_variant,
|
||||
get_excluded_builtin_components,
|
||||
@@ -290,74 +282,9 @@ idf_component_register(
|
||||
target_link_options(${{COMPONENT_LIB}} PUBLIC
|
||||
{link_opts_str}
|
||||
)
|
||||
{_pch_cmake()}"""
|
||||
|
||||
|
||||
def _pch_cmake() -> str:
|
||||
"""The src component's precompiled-header block (C++ TUs only).
|
||||
|
||||
The -include stays relative (resolved from the compiler cwd, the build
|
||||
dir); an absolute path would poison ccache keys.
|
||||
"""
|
||||
if not pch_enabled():
|
||||
return ""
|
||||
# Strict inverts: a per-process consumer rejection reds the build.
|
||||
# Baked at generation: a knob flip takes effect when the CMakeLists is
|
||||
# rewritten (every esphome compile); a hand-run idf.py keeps the old one
|
||||
escalation = pch.pch_consumer_escalation()
|
||||
return f"""
|
||||
# ESPHome precompiled header (see esphome/build_helpers/pch.py).
|
||||
# OBJECT_DEPENDS is on the header, not the .gch: pch-baked headers drop
|
||||
# out of TU depfiles, and prepare_pch() touches the header on rebuild.
|
||||
target_compile_options(${{COMPONENT_LIB}} PRIVATE
|
||||
"$<$<COMPILE_LANGUAGE:CXX>:-Winvalid-pch>"
|
||||
"$<$<COMPILE_LANGUAGE:CXX>:{escalation}>"
|
||||
"$<$<COMPILE_LANGUAGE:CXX>:-include>"
|
||||
"$<$<COMPILE_LANGUAGE:CXX>:{PCH_HEADER_NAME}>"
|
||||
)
|
||||
set_source_files_properties(${{app_sources}} PROPERTIES
|
||||
OBJECT_DEPENDS "${{CMAKE_BINARY_DIR}}/{PCH_HEADER_NAME}")
|
||||
"""
|
||||
|
||||
|
||||
def discard_pch() -> None:
|
||||
"""Drop the pch sidecars in the IDF build dir."""
|
||||
pch.discard_pch(CORE.relative_build_path("build"))
|
||||
|
||||
|
||||
def prepare_pch() -> None:
|
||||
"""Build the .gch right before ninja, after every reconfigure, so the
|
||||
compile_commands.json flags and the sdkconfig are the settled ones."""
|
||||
if not pch_enabled():
|
||||
# Self-cleaning escape hatch: drop any previously built .gch
|
||||
pch.discard_pch(CORE.relative_build_path("build"))
|
||||
pch.pch_disabled_degraded()
|
||||
return
|
||||
sdkconfig_path = CORE.relative_build_path(f"sdkconfig.{CORE.name}")
|
||||
try:
|
||||
sdkconfig = sdkconfig_path.read_text(encoding="utf-8")
|
||||
except OSError as err:
|
||||
# Fail closed: the sdkconfig is the .sum's only config identity for
|
||||
# sdkconfig.h-only options; a stand-in marker would collide
|
||||
_LOGGER.warning(
|
||||
"Could not read %s; compiling without the pch: %s", sdkconfig_path, err
|
||||
)
|
||||
pch.discard_pch(CORE.relative_build_path("build"))
|
||||
pch.pch_degraded(f"sdkconfig unreadable: {err}")
|
||||
return
|
||||
pch.prepare_pch(
|
||||
CORE.relative_build_path("build"),
|
||||
PCH_DEFAULT_HEADERS,
|
||||
(
|
||||
str(idf_version()),
|
||||
CORE.cpp_standard or "",
|
||||
sdkconfig,
|
||||
*get_project_compile_flags(),
|
||||
*get_project_cxx_compile_flags(),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def write_project(
|
||||
minimal: bool = False, builtin_components: list[str] | None = None
|
||||
) -> None:
|
||||
@@ -377,14 +304,6 @@ def write_project(
|
||||
get_component_cmakelists(),
|
||||
)
|
||||
|
||||
if pch_enabled():
|
||||
write_file_if_changed(
|
||||
CORE.relative_build_path("build", PCH_HEADER_NAME),
|
||||
pch_header_text(PCH_DEFAULT_HEADERS),
|
||||
)
|
||||
# Consumers carry the -include; gate the ccache relaxation on it
|
||||
mark_pch_emitted()
|
||||
|
||||
# Snapshot the exclusion set so has_outdated_files() can trigger a
|
||||
# discovery reconfigure when it changes. Excluded components never
|
||||
# register in project_description.json, so re-including one (e.g. a
|
||||
|
||||
@@ -1,296 +0,0 @@
|
||||
"""Native ninja build generator for the host platform.
|
||||
|
||||
Emits ``build.ninja`` under ``.pioenvs/<name>/``: every source in the
|
||||
generated ``src/`` tree plus the resolved registry libraries compiles with
|
||||
the machine's compiler and links into ``program``, the name PlatformIO's
|
||||
native platform produced. Build flags route the way SCons's ``ParseFlags``
|
||||
did under PlatformIO: ``-D``/``-I``/``-std=``/``-W`` shapes reach the
|
||||
compile lines only, ``-l``/``-L``/``-Wl,`` the link line only, everything
|
||||
else both.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from esphome.build_helpers.ninja import escape as _e, quote_path as _q, shell_token
|
||||
from esphome.build_helpers.ninja_gen import (
|
||||
PATH_ARG_FLAGS,
|
||||
Flag,
|
||||
anchor_path_flag,
|
||||
ar_rule_lines,
|
||||
collect_sources,
|
||||
compile_edges,
|
||||
compile_rule_lines,
|
||||
library_edges,
|
||||
pch_edges,
|
||||
pch_rule_lines,
|
||||
tool_lines,
|
||||
)
|
||||
from esphome.build_helpers.pch import PCH_CORE_HEADER
|
||||
from esphome.core import CORE, EsphomeError
|
||||
from esphome.framework_helpers import get_project_cxx_compile_flags
|
||||
from esphome.helpers import mkdir_p, write_file_if_changed
|
||||
from esphome.host.toolchain import PROGRAM_NAME, HostCompilers, find_tool, get_build_dir
|
||||
from esphome.platformio.library import lex_build_flags
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from esphome.arduino.library import ArduinoLibrary
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# The PlatformIO platform the host built under; registry manifests declare
|
||||
# compatibility against it, as lib_compat_mode=strict checked before
|
||||
PIO_PLATFORM = "native"
|
||||
# Namespaces the shared library download cache (pio_components/host/)
|
||||
LIBRARY_CACHE_KEY = "host"
|
||||
|
||||
# Flag shapes that only the compiler understands; dropped from the link line
|
||||
_COMPILE_ONLY_PREFIXES = ("-D", "-U", "-I", "-std=", "-W", *PATH_ARG_FLAGS)
|
||||
# Flag shapes that only the linker consumes; inert on a -c compile line
|
||||
_LINK_ONLY_PREFIXES = ("-l", "-L", "-Wl,")
|
||||
# Link-only flags whose argument is the next token
|
||||
_LINK_ONLY_ARG_FLAGS = ("-framework", "-Xlinker", "-z")
|
||||
|
||||
|
||||
def parse_flags(entries: Iterable[str], owner: str) -> list[Flag]:
|
||||
"""Lex build flag entries into flags, each with its argument.
|
||||
|
||||
Entries are a set, so their order is not the user's: a flag and the
|
||||
argument it takes as the next token must share one entry.
|
||||
"""
|
||||
flags: list[Flag] = []
|
||||
for entry in entries:
|
||||
it = iter(lex_build_flags(entry, owner))
|
||||
for tok in it:
|
||||
if tok not in PATH_ARG_FLAGS and tok not in _LINK_ONLY_ARG_FLAGS:
|
||||
flags.append((tok,))
|
||||
continue
|
||||
arg = next(it, None)
|
||||
# A path never starts with "-"; that is the next flag
|
||||
if arg is None or (tok in PATH_ARG_FLAGS and arg.startswith("-")):
|
||||
raise EsphomeError(
|
||||
f"{owner} build flags have '{tok}' with no argument; write "
|
||||
f"the flag and its argument as one entry"
|
||||
)
|
||||
flags.append((tok, arg))
|
||||
return flags
|
||||
|
||||
|
||||
def split_flags(flags: list[Flag]) -> tuple[list[Flag], list[Flag]]:
|
||||
"""Route build flags to the compile and link lines."""
|
||||
compile_flags: list[Flag] = []
|
||||
link_flags: list[Flag] = []
|
||||
for flag in flags:
|
||||
name = flag[0]
|
||||
if len(flag) > 1:
|
||||
(compile_flags if name in PATH_ARG_FLAGS else link_flags).append(flag)
|
||||
elif name.startswith(_LINK_ONLY_PREFIXES):
|
||||
# Checked before the compile prefixes: -Wl, would match -W
|
||||
link_flags.append(flag)
|
||||
elif name.startswith(_COMPILE_ONLY_PREFIXES):
|
||||
compile_flags.append(flag)
|
||||
else:
|
||||
# -g, -O, -f*, -m*, -pthread, --coverage: both lines, as SCons
|
||||
compile_flags.append(flag)
|
||||
link_flags.append(flag)
|
||||
return compile_flags, link_flags
|
||||
|
||||
|
||||
def _is_std(flag: Flag) -> bool:
|
||||
return flag[0].startswith("-std=")
|
||||
|
||||
|
||||
def _is_cxx_std(flag: Flag) -> bool:
|
||||
return _is_std(flag) and "++" in flag[0]
|
||||
|
||||
|
||||
def _anchored_flags(entries: Iterable[str], owner: str) -> list[Flag]:
|
||||
build_path = Path(CORE.build_path)
|
||||
return [
|
||||
anchor_path_flag(flag, build_path)
|
||||
for flag in parse_flags(sorted(entries), owner)
|
||||
]
|
||||
|
||||
|
||||
def _flag_lists() -> tuple[list[str], list[str], list[str]]:
|
||||
"""The C, C++, and link flag lists (raw tokens), build_unflags applied.
|
||||
|
||||
``cg.set_cpp_standard`` wins over any ``-std=`` in the build flags for
|
||||
C++ compiles, as PlatformIO's unflag of every other standard did; C
|
||||
compiles never see a C++ standard.
|
||||
"""
|
||||
# The funnel warns and drops empty glued arguments (-D "") itself
|
||||
compile_flags, link_flags = split_flags(
|
||||
_anchored_flags(CORE.build_flags, "esphome")
|
||||
)
|
||||
cflags = [f for f in compile_flags if not _is_cxx_std(f)]
|
||||
cxx_std = CORE.cpp_standard
|
||||
cxxflags = [f for f in compile_flags if not (cxx_std and _is_std(f))]
|
||||
if cxx_std:
|
||||
cxxflags.insert(0, (f"-std={cxx_std}",))
|
||||
cxxflags += [(tok,) for tok in get_project_cxx_compile_flags()]
|
||||
|
||||
# A flag is removed whole, with its argument, as PlatformIO did
|
||||
unflags = set(_anchored_flags(CORE.build_unflags, "esphome build_unflags"))
|
||||
# An unflag that hits nothing (a typo, or -DUSE_FOO against
|
||||
# -DUSE_FOO=1) must be visible, since the user believes the flag is
|
||||
# gone while it still drives the build
|
||||
if unmatched := sorted(unflags - set(cflags) - set(cxxflags) - set(link_flags)):
|
||||
_LOGGER.warning(
|
||||
"build_unflags entries matched no build flag: %s",
|
||||
", ".join(" ".join(flag) for flag in unmatched),
|
||||
)
|
||||
|
||||
def keep(flags: list[Flag]) -> list[str]:
|
||||
return [tok for flag in flags if flag not in unflags for tok in flag]
|
||||
|
||||
return keep(cflags), keep(cxxflags), keep(link_flags)
|
||||
|
||||
|
||||
def _resolve_host_libraries() -> list[ArduinoLibrary]:
|
||||
"""Every ``cg.add_library()`` entry, fetched from the registry.
|
||||
|
||||
The host has no framework, so nothing is bundled and no framework
|
||||
compatibility check applies; the platform check keeps the strict
|
||||
manifest gate PlatformIO's native platform enforced. Manifest-less
|
||||
libraries (a bare git checkout) build with PlatformIO's default
|
||||
layout, as they did under its native platform.
|
||||
"""
|
||||
if not CORE.platformio_libraries:
|
||||
return []
|
||||
from esphome.arduino.library import resolve_libraries
|
||||
|
||||
return resolve_libraries(
|
||||
None,
|
||||
pio_platform=PIO_PLATFORM,
|
||||
board_mcu="host",
|
||||
cache_key=LIBRARY_CACHE_KEY,
|
||||
framework=None,
|
||||
manifest_optional=True,
|
||||
)
|
||||
|
||||
|
||||
def _file_macro_maps(build_dir: Path) -> list[str]:
|
||||
"""Flags that keep ``__FILE__`` relative to the build path.
|
||||
|
||||
PlatformIO compiled ``src/x.cpp`` from the build path, and tools name
|
||||
things after that spelling (CodSpeed's benchmark ids). Here a source
|
||||
reaches the compiler by its absolute path, or relative to the build
|
||||
directory when ccache rewrites it.
|
||||
"""
|
||||
build_path = Path(CORE.build_path)
|
||||
prefixes = (build_path, Path(os.path.relpath(build_path, build_dir)))
|
||||
return [
|
||||
shell_token(f"-fmacro-prefix-map={prefix}{os.sep}=", force=True)
|
||||
for prefix in prefixes
|
||||
]
|
||||
|
||||
|
||||
def write_project(compilers: HostCompilers, ccache: str | None) -> bool:
|
||||
"""Write the ninja build for the current configuration.
|
||||
|
||||
``ccache`` is the caller's already-resolved binary (None when disabled).
|
||||
Returns True when ``build.ninja`` changed, so the caller can skip work
|
||||
derived purely from it (the compile database) on unchanged builds.
|
||||
"""
|
||||
build_dir = get_build_dir()
|
||||
mkdir_p(build_dir)
|
||||
src_dir = CORE.relative_src_path()
|
||||
if not src_dir.is_dir():
|
||||
# Generated project state, not install state: clean-all would not help
|
||||
raise EsphomeError(f"Generated source directory {src_dir} is missing")
|
||||
|
||||
cflags, cxxflags, link_flags = _flag_lists()
|
||||
libraries = _resolve_host_libraries()
|
||||
|
||||
include_dirs = [src_dir]
|
||||
for lib in libraries:
|
||||
include_dirs += lib.include_dirs
|
||||
includes = [f"-I{_q(d)}" for d in include_dirs]
|
||||
includes += _file_macro_maps(build_dir)
|
||||
|
||||
# SCons's link line: $LINKFLAGS $SOURCES $_LIBDIRFLAGS $_LIBFLAGS, so
|
||||
# -L and -l trail the objects while every other link token leads
|
||||
lib_dirs = [Path(t[2:]) for t in link_flags if t.startswith("-L")]
|
||||
libs = [t for t in link_flags if t.startswith("-l")]
|
||||
linkflags = [shell_token(t) for t in link_flags if not t.startswith(("-L", "-l"))]
|
||||
for lib in libraries:
|
||||
lib_dirs += lib.link_dirs
|
||||
libs += [f"-l{name}" for name in lib.link_libs]
|
||||
linkflags += [shell_token(f) for f in lib.link_flags]
|
||||
|
||||
# PlatformIO's ASPPCOM passes only -D/-I user flags to assembly
|
||||
asflags = [t for t in cflags if t.startswith(("-D", "-I"))]
|
||||
|
||||
lines = [
|
||||
*tool_lines(compilers.cc, compilers.cxx, ccache),
|
||||
*compile_rule_lines(),
|
||||
*pch_rule_lines(),
|
||||
"rule link",
|
||||
" command = $cxx -o $out $linkflags @$out.rsp $archives $libdirflags $libflags",
|
||||
" rspfile = $out.rsp",
|
||||
" rspfile_content = $in_newline",
|
||||
" description = LINK $out",
|
||||
]
|
||||
if any(lib.sources and lib.lib_archive for lib in libraries):
|
||||
# Resolved only when an archive is built, so a system without
|
||||
# binutils still links a library-free configuration
|
||||
lines += ar_rule_lines(find_tool("AR", ("ar",)))
|
||||
lines += [
|
||||
"",
|
||||
f"cflags = {' '.join([*map(shell_token, cflags), *includes])}",
|
||||
f"cxxflags = {' '.join([*map(shell_token, cxxflags), *includes])}",
|
||||
f"asflags = {' '.join([*map(shell_token, asflags), *includes])}",
|
||||
f"linkflags = {' '.join(linkflags)}",
|
||||
f"libdirflags = {' '.join(f'-L{_q(d)}' for d in lib_dirs)}",
|
||||
f"libflags = {' '.join(shell_token(lib) for lib in libs)}",
|
||||
"",
|
||||
]
|
||||
|
||||
archives, direct_objs = library_edges(lines, libraries)
|
||||
|
||||
from esphome.components.host import HOST_PCH_PREFIX
|
||||
|
||||
# Host has no framework force-includes; the per-TU cost is the STL
|
||||
# closure behind the core headers, so those are what is precompiled
|
||||
src_cxx_override = pch_edges(
|
||||
lines,
|
||||
build_dir,
|
||||
src_dir,
|
||||
(HOST_PCH_PREFIX, PCH_CORE_HEADER),
|
||||
cxxflags,
|
||||
(),
|
||||
ccache,
|
||||
compilers.cxx,
|
||||
)
|
||||
src_objs = compile_edges(
|
||||
lines,
|
||||
collect_sources(src_dir),
|
||||
src_dir,
|
||||
"src",
|
||||
cxx_override=src_cxx_override,
|
||||
)
|
||||
if not src_objs:
|
||||
raise EsphomeError(f"No source files found under {src_dir}")
|
||||
|
||||
# Archives are not topologically sorted; GNU ld needs the group to
|
||||
# resolve references between them. ld64 loads archives iteratively and
|
||||
# rejects the option, so macOS lists them bare.
|
||||
archive_tokens = [shell_token(a) for a in archives]
|
||||
if archive_tokens and sys.platform != "darwin":
|
||||
archive_tokens = ["-Wl,--start-group", *archive_tokens, "-Wl,--end-group"]
|
||||
lines.append(
|
||||
f"build {PROGRAM_NAME}: link {' '.join(src_objs + direct_objs)} | "
|
||||
f"{' '.join(_e(a) for a in archives)}"
|
||||
)
|
||||
lines.append(f" archives = {' '.join(archive_tokens)}")
|
||||
lines.append(f"default {PROGRAM_NAME}")
|
||||
lines.append("")
|
||||
|
||||
return write_file_if_changed(build_dir / "build.ninja", "\n".join(lines))
|
||||
@@ -21,13 +21,12 @@ def _ccache_runs(ccache: str) -> bool:
|
||||
)
|
||||
|
||||
|
||||
def parse_enable_env(name: str, strict: bool = False) -> bool | None:
|
||||
def parse_enable_env(name: str) -> bool | None:
|
||||
"""Strictly parse an on/off environment knob; None when unset or invalid.
|
||||
|
||||
``bool(str)`` truthiness would flip ``no``/``off`` to enabled, so only
|
||||
1/true/yes/on and 0/false/no/off count; anything else warns and reads
|
||||
as unset so the caller's default policy applies — or raises when
|
||||
``strict`` (a typo must not silently disable a CI gate).
|
||||
as unset so the caller's default policy applies.
|
||||
"""
|
||||
raw = os.environ.get(name)
|
||||
if raw is None:
|
||||
@@ -40,10 +39,6 @@ def parse_enable_env(name: str, strict: bool = False) -> bool | None:
|
||||
return True
|
||||
if lowered in FALSY_ENV_STRINGS:
|
||||
return False
|
||||
if strict:
|
||||
from esphome.core import EsphomeError
|
||||
|
||||
raise EsphomeError(f"Unrecognized {name}={raw!r}; use 1 or 0")
|
||||
_LOGGER.warning("Ignoring unrecognized %s=%r; use 1 or 0", name, raw)
|
||||
return None
|
||||
|
||||
@@ -73,15 +68,6 @@ def resolve_ccache_path() -> str | None:
|
||||
return ccache
|
||||
|
||||
|
||||
def resolve_absolute_ccache_path() -> str | None:
|
||||
"""``resolve_ccache_path`` for the ninja backends, which run their
|
||||
commands from the build directory, where a relative path is lost."""
|
||||
from esphome.build_helpers.ninja import absolute_tool
|
||||
|
||||
ccache = resolve_ccache_path()
|
||||
return absolute_tool(ccache) if ccache else None
|
||||
|
||||
|
||||
def ccache_defaults_env(cache_dir: Path) -> dict[str, str]:
|
||||
"""Default ``CCACHE_*`` values for a build subprocess (not os.environ).
|
||||
|
||||
@@ -98,39 +84,9 @@ def ccache_defaults_env(cache_dir: Path) -> dict[str, str]:
|
||||
"CORE.build_path must be set before constructing the build environment"
|
||||
)
|
||||
defaults = {
|
||||
# ccache expands $VAR in its settings; $$ is a literal $
|
||||
"CCACHE_DIR": str(cache_dir).replace("$", "$$"),
|
||||
"CCACHE_DIR": str(cache_dir),
|
||||
"CCACHE_NOHASHDIR": "true",
|
||||
"CCACHE_DEPEND": "1",
|
||||
"CCACHE_BASEDIR": str(Path(CORE.build_path).resolve()).replace("$", "$$"),
|
||||
"CCACHE_BASEDIR": str(Path(CORE.build_path).resolve()),
|
||||
}
|
||||
return {k: v for k, v in defaults.items() if k not in os.environ}
|
||||
|
||||
|
||||
def effective_ccache_basedir() -> str:
|
||||
"""The prefix ccache rewrites out of hashed paths: a user CCACHE_BASEDIR
|
||||
wins, else the resolved build path (matching ccache_defaults_env)."""
|
||||
from esphome.core import CORE
|
||||
|
||||
raw = os.environ.get("CCACHE_BASEDIR")
|
||||
if raw is not None and Path(raw).is_absolute() and len(Path(raw).parts) > 1:
|
||||
return raw
|
||||
# Unset or degenerate ("", "/", relative): fall back to the build path
|
||||
return str(Path(CORE.build_path).resolve())
|
||||
|
||||
|
||||
def ccache_env(ccache: str | None, tools_cache: tuple[str, str]) -> dict[str, str]:
|
||||
"""The ccache settings for a build subprocess (not os.environ).
|
||||
|
||||
``ccache`` is the pre-resolved binary (resolve_ccache_path), or None when
|
||||
disabled; ``tools_cache`` is the backend's tools cache spec, which holds
|
||||
its ccache dir.
|
||||
"""
|
||||
if ccache is None:
|
||||
return {}
|
||||
from esphome.build_helpers.pch import ccache_pch_env
|
||||
from esphome.build_helpers.tools_cache import tools_cache_path
|
||||
|
||||
env = ccache_defaults_env(tools_cache_path(*tools_cache) / "ccache")
|
||||
env.update(ccache_pch_env())
|
||||
return env
|
||||
@@ -11,7 +11,6 @@ consumers (IDE integration, clang-tidy) expect:
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
@@ -22,8 +21,6 @@ import subprocess
|
||||
from esphome.core import EsphomeError
|
||||
from esphome.helpers import write_file
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Everything idedata generation may raise after a successful link; idedata
|
||||
# is a bonus artifact, so consumers warn instead of failing the build
|
||||
IDEDATA_BEST_EFFORT_ERRORS = (
|
||||
@@ -34,27 +31,13 @@ IDEDATA_BEST_EFFORT_ERRORS = (
|
||||
ValueError,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
def warn_if_idedata_missing(get_idedata: Callable[[], dict | None]) -> None:
|
||||
"""Run an idedata generator, downgrading any failure to a warning:
|
||||
the firmware already built."""
|
||||
try:
|
||||
if get_idedata() is None:
|
||||
_LOGGER.warning("No idedata was generated for this build")
|
||||
except IDEDATA_BEST_EFFORT_ERRORS as err:
|
||||
_LOGGER.warning(
|
||||
"Could not generate idedata: %s (IDE, clang-tidy, and "
|
||||
"memory-analysis data will be unavailable for this build)",
|
||||
err,
|
||||
)
|
||||
_LOGGER.debug("Idedata failure detail", exc_info=True)
|
||||
|
||||
|
||||
# C++ translation-unit suffixes.
|
||||
CXX_SOURCE_SUFFIXES = (".cpp", ".cc", ".cxx")
|
||||
# C++ translation-unit suffixes used to identify ESPHome source files.
|
||||
_CXX_SUFFIXES = (".cpp", ".cc")
|
||||
# Suffixes of input/output files that appear bare on the command line (and so
|
||||
# must not be mistaken for compiler flags).
|
||||
_INPUT_FILE_SUFFIXES = (*CXX_SOURCE_SUFFIXES, ".c", ".o", ".S", ".s")
|
||||
_INPUT_FILE_SUFFIXES = (*_CXX_SUFFIXES, ".c", ".o", ".S", ".s")
|
||||
# Path marker identifying an ESPHome source translation unit.
|
||||
_ESPHOME_SRC_MARKER = "/src/esphome/"
|
||||
|
||||
@@ -63,11 +46,11 @@ def _is_esphome_src(file: str) -> bool:
|
||||
"""Whether ``file`` is an ESPHome C++ translation unit; normalized to
|
||||
``/`` first since Windows compile DBs use backslashes."""
|
||||
return _ESPHOME_SRC_MARKER in file.replace("\\", "/") and file.endswith(
|
||||
CXX_SOURCE_SUFFIXES
|
||||
_CXX_SUFFIXES
|
||||
)
|
||||
|
||||
|
||||
def split_command(command: str) -> list[str]:
|
||||
def _split_command(command: str) -> list[str]:
|
||||
r"""Tokenize a compile_commands.json / response-file command string.
|
||||
|
||||
On Windows, tokenize per Windows ``argv`` rules via ``CommandLineToArgvW``.
|
||||
@@ -103,7 +86,7 @@ def split_command(command: str) -> list[str]:
|
||||
ctypes.windll.kernel32.LocalFree(argv)
|
||||
|
||||
|
||||
def expand_response_files(tokens: list[str], directory: Path) -> list[str]:
|
||||
def _expand_response_files(tokens: list[str], directory: Path) -> list[str]:
|
||||
"""Inline any ``@response-file`` arguments (paths relative to ``directory``).
|
||||
|
||||
GCC response files embed flags that must be expanded so GCC-only flags
|
||||
@@ -118,8 +101,8 @@ def expand_response_files(tokens: list[str], directory: Path) -> list[str]:
|
||||
rf = directory / rf
|
||||
try:
|
||||
out.extend(
|
||||
expand_response_files(
|
||||
split_command(rf.read_text(encoding="utf-8")), directory
|
||||
_expand_response_files(
|
||||
_split_command(rf.read_text(encoding="utf-8")), directory
|
||||
)
|
||||
)
|
||||
continue
|
||||
@@ -138,7 +121,7 @@ def _pick_entry(entries: list[dict]) -> dict:
|
||||
if _is_esphome_src(entry["file"]):
|
||||
return entry
|
||||
for entry in entries:
|
||||
if entry["file"].endswith(CXX_SOURCE_SUFFIXES):
|
||||
if entry["file"].endswith(_CXX_SUFFIXES):
|
||||
return entry
|
||||
raise ValueError("no C++ translation unit found in compile_commands.json")
|
||||
|
||||
@@ -148,25 +131,16 @@ def _pick_entry(entries: list[dict]) -> dict:
|
||||
_LAUNCHER_STEMS = frozenset({"ccache", "sccache", "distcc", "icecc", "buildcache"})
|
||||
|
||||
|
||||
def is_launcher(token: str) -> bool:
|
||||
def _is_launcher(token: str) -> bool:
|
||||
return Path(token).stem.lower() in _LAUNCHER_STEMS
|
||||
|
||||
|
||||
def is_joined_include(tok: str) -> bool:
|
||||
"""The joined ``-includefoo.h`` spelling; excludes clang's -include-pch."""
|
||||
return (
|
||||
tok.startswith("-include")
|
||||
and tok != "-include"
|
||||
and not tok.startswith("-include-")
|
||||
)
|
||||
|
||||
|
||||
def parse_entry(
|
||||
entry: dict, launcher: str | None = None
|
||||
) -> tuple[str, list[str], list[str], list[str]]:
|
||||
"""Parse one compile_commands entry -> (cxx_path, defines, includes, cxx_flags)."""
|
||||
directory = Path(entry["directory"])
|
||||
tokens = expand_response_files(split_command(entry["command"]), directory)
|
||||
tokens = _expand_response_files(_split_command(entry["command"]), directory)
|
||||
|
||||
def _include(raw: str) -> str:
|
||||
# Resolve against the entry's ``directory`` so cached idedata works
|
||||
@@ -182,7 +156,7 @@ def parse_entry(
|
||||
if not tokens:
|
||||
# An empty command, or one that was only the launcher; fail by name
|
||||
raise ValueError(f"empty compile command for {entry.get('file')}")
|
||||
if is_launcher(tokens[0]) and len(tokens) > 1 and not tokens[1].startswith("-"):
|
||||
if _is_launcher(tokens[0]) and len(tokens) > 1 and not tokens[1].startswith("-"):
|
||||
# Stale DB built with a launcher this run no longer configures; the
|
||||
# real compiler is the next token
|
||||
_LOGGER.warning("Stripping unconfigured launcher %s", tokens[0])
|
||||
@@ -195,23 +169,11 @@ def parse_entry(
|
||||
defines: list[str] = []
|
||||
includes: list[str] = []
|
||||
cxx_flags: list[str] = []
|
||||
unresolved_force_includes: list[str] = []
|
||||
|
||||
it = iter(tokens[1:])
|
||||
for tok in it:
|
||||
if tok in ("-c", "-o"):
|
||||
next(it, None) # drop the flag and its argument (input/output)
|
||||
elif tok == "-include" or is_joined_include(tok):
|
||||
# Re-anchor only names next to the compile (the pch); a name
|
||||
# meant for the -I chain must stay untouched
|
||||
raw = next(it, "") if tok == "-include" else tok[len("-include") :]
|
||||
if not raw:
|
||||
_LOGGER.warning("Dropping -include with no argument")
|
||||
elif Path(resolved := _include(raw)).is_file():
|
||||
cxx_flags.extend(("-include", resolved))
|
||||
else:
|
||||
unresolved_force_includes.append(raw)
|
||||
cxx_flags.extend(("-include", raw))
|
||||
elif tok.startswith("-D"):
|
||||
# ``.strip()`` handles tokens like ``-D CONFIGURED=1`` (a single
|
||||
# quoted arg with a space after -D) that some flags arrive as.
|
||||
@@ -230,14 +192,6 @@ def parse_entry(
|
||||
pass # input/output files
|
||||
else:
|
||||
cxx_flags.append(tok)
|
||||
for raw in unresolved_force_includes:
|
||||
# A deleted build artifact would otherwise surface only downstream
|
||||
if not any((Path(inc) / raw).is_file() for inc in includes):
|
||||
_LOGGER.warning(
|
||||
"-include %s found neither next to the compile nor on the "
|
||||
"include path; cached idedata may not resolve it",
|
||||
raw,
|
||||
)
|
||||
return cxx_path, defines, includes, cxx_flags
|
||||
|
||||
|
||||
@@ -302,7 +256,7 @@ def _cache_usable(cached: object) -> bool:
|
||||
if not isinstance(cached, dict) or "cc_path" not in cached:
|
||||
return False
|
||||
cxx_path = cached.get("cxx_path")
|
||||
if not isinstance(cxx_path, str) or is_launcher(cxx_path):
|
||||
if not isinstance(cxx_path, str) or _is_launcher(cxx_path):
|
||||
return False
|
||||
includes = cached.get("includes")
|
||||
return isinstance(includes, dict) and isinstance(includes.get("build"), list)
|
||||
@@ -350,7 +304,7 @@ def load_or_build_idedata(
|
||||
def reject_launcher_compiler(cxx_path: str) -> None:
|
||||
"""Reject a compile DB naming a launcher (ccache) as the compiler; it
|
||||
must never be probed, cached, or consumed."""
|
||||
if is_launcher(cxx_path):
|
||||
if _is_launcher(cxx_path):
|
||||
raise EsphomeError(
|
||||
f"compile_commands.json names the launcher {cxx_path} as the "
|
||||
"compiler; the compile database is unusable"
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
"""Resolution of the native (non-PlatformIO) build backend for a config.
|
||||
|
||||
Kept deliberately light: the serial upload and logs fast path resolves a
|
||||
backend for its artifact paths alone, so importing this must not pull in a
|
||||
platform component package or the backend itself.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Collection
|
||||
import importlib
|
||||
import logging
|
||||
from types import ModuleType
|
||||
|
||||
from esphome.const import Toolchain
|
||||
from esphome.core import CORE, EsphomeError
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Native build backend per (target platform, toolchain)
|
||||
NATIVE_TOOLCHAIN_MODULES = {
|
||||
("esp32", Toolchain.ESP_IDF): "esphome.espidf.toolchain",
|
||||
("esp8266", Toolchain.ARDUINO): "esphome.arduino8266.toolchain",
|
||||
("host", Toolchain.HOST): "esphome.host.toolchain",
|
||||
}
|
||||
|
||||
|
||||
def native_backend() -> ModuleType | None:
|
||||
"""The native build backend module for the resolved toolchain."""
|
||||
if not CORE.using_native_toolchain:
|
||||
return None
|
||||
key = (CORE.target_platform, CORE.toolchain)
|
||||
if (module_path := NATIVE_TOOLCHAIN_MODULES.get(key)) is None:
|
||||
# Degrading to the PlatformIO path would build with the wrong backend
|
||||
raise EsphomeError(
|
||||
f"Toolchain '{CORE.toolchain.value}' has no native build backend "
|
||||
f"module for platform {CORE.target_platform}"
|
||||
)
|
||||
return importlib.import_module(module_path)
|
||||
|
||||
|
||||
# Binutils and the linked image for memory analysis, for toolchains that build
|
||||
# without PlatformIO but have no native build backend (which supplies them)
|
||||
ANALYSIS_TOOLCHAIN_MODULES = {
|
||||
("nrf52", Toolchain.SDK_NRF): "esphome.components.nrf52.toolchain",
|
||||
}
|
||||
|
||||
|
||||
def analysis_backend() -> ModuleType | None:
|
||||
"""The module giving objdump, readelf and the ELF of a non-PlatformIO build.
|
||||
|
||||
None means PlatformIO's idedata supplies them (or nothing can).
|
||||
"""
|
||||
if (native := native_backend()) is not None:
|
||||
return native
|
||||
module_path = ANALYSIS_TOOLCHAIN_MODULES.get((CORE.target_platform, CORE.toolchain))
|
||||
return importlib.import_module(module_path) if module_path else None
|
||||
|
||||
|
||||
def warn_ignored_platformio_options(consumed: Collection[str]) -> None:
|
||||
"""Warn for component-added platformio options a native build drops.
|
||||
|
||||
User-supplied keys were already routed or warned about by
|
||||
``core/config.py``; what survives into ``CORE.platformio_options`` came
|
||||
from ``cg.add_platformio_option`` calls in components.
|
||||
"""
|
||||
for key in sorted(CORE.platformio_options or {}):
|
||||
if key not in consumed:
|
||||
_LOGGER.warning(
|
||||
"platformio_options->%s is ignored when building with the "
|
||||
"native '%s' toolchain",
|
||||
key,
|
||||
CORE.toolchain.value,
|
||||
)
|
||||
@@ -2,17 +2,14 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
from esphome.core import EsphomeError
|
||||
from esphome.framework_helpers import strip_win_long_path_prefix, tool_version_runs
|
||||
from esphome.helpers import write_file_if_changed
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -26,26 +23,11 @@ def _ninja_runs(binary: str) -> bool:
|
||||
)
|
||||
|
||||
|
||||
# Compile rule names the generators emit; ninja's compdb tool is asked for
|
||||
# exactly these, so a renamed rule fails the build instead of stranding idedata
|
||||
COMPILE_RULES = ("c", "cxx", "aspp", "asm")
|
||||
|
||||
|
||||
def absolute_tool(tool: str | Path) -> str:
|
||||
"""A tool path that still resolves from the build directory.
|
||||
|
||||
``shutil.which`` returns a relative path for a relative PATH entry, and
|
||||
ninja runs the commands from ``.pioenvs/<name>``. Symlinks are kept:
|
||||
ccache's compiler links depend on the name they are called by.
|
||||
"""
|
||||
return strip_win_long_path_prefix(str(Path(tool).absolute()))
|
||||
|
||||
|
||||
def find_ninja() -> Path:
|
||||
"""Locate the ninja binary: a runnable PATH hit first, else the ninja
|
||||
PyPI wheel."""
|
||||
if binary := shutil.which("ninja"):
|
||||
binary = absolute_tool(binary)
|
||||
binary = strip_win_long_path_prefix(binary)
|
||||
if _ninja_runs(binary):
|
||||
return Path(binary)
|
||||
import_error: ImportError | None = None
|
||||
@@ -67,18 +49,8 @@ def find_ninja() -> Path:
|
||||
|
||||
|
||||
def escape(value: Path | str) -> str:
|
||||
"""Escape a path or token for a ninja file.
|
||||
|
||||
ninja has no escape for ``|`` or a line break in a path, so those fail
|
||||
here by name instead of producing a build file ninja misreads.
|
||||
"""
|
||||
text = str(value)
|
||||
if bad := next((c for c in "|\r\n" if c in text), None):
|
||||
raise EsphomeError(
|
||||
f"Path {text!r} contains {bad!r}, which a ninja build file cannot "
|
||||
"express; rename the file or directory"
|
||||
)
|
||||
return text.replace("$", "$$").replace(":", "$:").replace(" ", "$ ")
|
||||
"""Escape a path or token for a ninja file."""
|
||||
return str(value).replace("$", "$$").replace(":", "$:").replace(" ", "$ ")
|
||||
|
||||
|
||||
def quote_arg(tok: str) -> str:
|
||||
@@ -118,65 +90,3 @@ def shell_token(tok: str, force: bool = False) -> str:
|
||||
def quote_path(value: Path | str) -> str:
|
||||
"""Force-quote a path for the ninja command line (shell/CreateProcess)."""
|
||||
return shell_token(str(value), force=True)
|
||||
|
||||
|
||||
def refresh_compile_commands(
|
||||
ninja_path: Path, build_dir: Path, env: dict[str, str], ninja_changed: bool
|
||||
) -> None:
|
||||
"""Regenerate the compile DB (a pure function of build.ninja) when stale.
|
||||
|
||||
Freshness rides a stamp: the DB itself is written through
|
||||
write_file_if_changed (its mtime feeds the idedata cache), so a
|
||||
regeneration with identical content would stay "stale" forever. An
|
||||
interrupted previous run may have rewritten the manifest without
|
||||
regenerating the DB, hence the mtime comparison.
|
||||
"""
|
||||
compdb = build_dir / "compile_commands.json"
|
||||
compdb_stamp = build_dir / ".compile_commands.stamp"
|
||||
ninja_file = build_dir / "build.ninja"
|
||||
if (
|
||||
ninja_changed
|
||||
or not compdb.is_file()
|
||||
or not compdb_stamp.is_file()
|
||||
or compdb_stamp.stat().st_mtime < ninja_file.stat().st_mtime
|
||||
):
|
||||
write_compile_commands(ninja_path, build_dir, env)
|
||||
compdb_stamp.touch()
|
||||
|
||||
|
||||
def write_compile_commands(
|
||||
ninja_path: Path, build_dir: Path, env: dict[str, str]
|
||||
) -> None:
|
||||
compdb = build_dir / "compile_commands.json"
|
||||
result = subprocess.run(
|
||||
[str(ninja_path), "-C", str(build_dir), "-t", "compdb", *COMPILE_RULES],
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
close_fds=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
# Drop any stale database so consumers (IDE integration, clang-tidy,
|
||||
# the memory analyzer) can't silently read outdated data
|
||||
compdb.unlink(missing_ok=True)
|
||||
raise EsphomeError(f"Could not generate compile_commands.json: {result.stderr}")
|
||||
try:
|
||||
entries = json.loads(result.stdout)
|
||||
except ValueError as err:
|
||||
compdb.unlink(missing_ok=True)
|
||||
raise EsphomeError(
|
||||
f"ninja produced an unparsable compile database: {err} "
|
||||
f"(output starts {result.stdout[:120]!r})"
|
||||
) from err
|
||||
if not entries:
|
||||
# compdb exits 0 with [] for unknown rule names; a renamed compile
|
||||
# rule must fail the build, not silently strand every consumer
|
||||
compdb.unlink(missing_ok=True)
|
||||
raise EsphomeError(
|
||||
"ninja produced an empty compile database; the generator's rule "
|
||||
"names no longer match"
|
||||
)
|
||||
# write_file_if_changed keeps the mtime stable on no-op builds so the
|
||||
# idedata cache stays valid
|
||||
write_file_if_changed(compdb, result.stdout)
|
||||
@@ -1,338 +0,0 @@
|
||||
"""The parts of a ``build.ninja`` every native backend emits the same way.
|
||||
|
||||
Rule names match ``SOURCE_KIND_FOR_SUFFIX`` values (c, cxx, asm, aspp) and
|
||||
``ninja.COMPILE_RULES``, which the compile database is asked for.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Collection, Sequence
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from esphome.build_helpers.ccache import effective_ccache_basedir
|
||||
from esphome.build_helpers.ninja import (
|
||||
escape as _e,
|
||||
quote_path as _q,
|
||||
shell_token as _shell_token,
|
||||
)
|
||||
from esphome.build_helpers.pch import (
|
||||
PCH_HEADER_NAME,
|
||||
mark_pch_emitted,
|
||||
pch_checksum,
|
||||
pch_consumer_escalation,
|
||||
pch_degraded,
|
||||
pch_disabled_degraded,
|
||||
pch_enabled,
|
||||
pch_header_text,
|
||||
pch_probe_args,
|
||||
pch_strict,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.framework_helpers import strip_win_long_path_prefix
|
||||
from esphome.helpers import write_file_if_changed
|
||||
from esphome.platformio.library import SOURCE_KIND_FOR_SUFFIX
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from esphome.arduino.library import ArduinoLibrary
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_BUILD_TOOL = Path(__file__).parent.parent / "build_gen" / "build_tool.py"
|
||||
|
||||
|
||||
def collect_sources(root: Path, exclude: Collection[str] = frozenset()) -> list[Path]:
|
||||
return sorted(
|
||||
p
|
||||
for p in root.rglob("*")
|
||||
if p.suffix in SOURCE_KIND_FOR_SUFFIX and p.name not in exclude
|
||||
)
|
||||
|
||||
|
||||
def common_parent(paths: list[Path]) -> Path:
|
||||
return Path(os.path.commonpath([str(p.parent) for p in paths]))
|
||||
|
||||
|
||||
def _command(words: Sequence[Path | str]) -> str:
|
||||
program, *args = words
|
||||
return " ".join([_q(program), *(_shell_token(str(a)) for a in args)])
|
||||
|
||||
|
||||
def tool_lines(
|
||||
cc: Sequence[Path | str], cxx: Sequence[Path | str], ccache: str | None
|
||||
) -> list[str]:
|
||||
"""The file header: the compilers and the helper tools as variables.
|
||||
|
||||
A compiler is its program followed by any arguments it always takes.
|
||||
"""
|
||||
return [
|
||||
"# Auto-generated by ESPHome",
|
||||
"ninja_required_version = 1.5",
|
||||
f"cc = {_command(cc)}",
|
||||
f"cxx = {_command(cxx)}",
|
||||
# The NSIS launcher starts Python with a \\?\ extended-length path
|
||||
# that cmd.exe cannot spawn; same strip every other emitted binary
|
||||
# path gets
|
||||
f"python = {_q(strip_win_long_path_prefix(sys.executable))}",
|
||||
f"buildtool = {_q(_BUILD_TOOL)}",
|
||||
f"ccache = {_q(ccache) if ccache else ''}",
|
||||
"",
|
||||
]
|
||||
|
||||
|
||||
def compile_rule_lines() -> list[str]:
|
||||
"""The compile rules; ``$own_includes`` is empty unless an edge sets it."""
|
||||
return [
|
||||
"rule c",
|
||||
" command = $ccache $cc -MMD -MF $out.d $own_includes $cflags $flags -c $in -o $out",
|
||||
" depfile = $out.d",
|
||||
" deps = gcc",
|
||||
" description = CC $out",
|
||||
"rule cxx",
|
||||
" command = $ccache $cxx -MMD -MF $out.d $own_includes $cxxflags $flags -c $in -o $out",
|
||||
" depfile = $out.d",
|
||||
" deps = gcc",
|
||||
" description = CXX $out",
|
||||
"rule aspp",
|
||||
" command = $ccache $cc -MMD -MF $out.d -x assembler-with-cpp $own_includes $asflags $flags -c $in -o $out",
|
||||
" depfile = $out.d",
|
||||
" deps = gcc",
|
||||
" description = AS $out",
|
||||
# Plain assembler, as SCons's ASCOM: no preprocessor, so no
|
||||
# depfile and no $flags (defines/includes) either
|
||||
"rule asm",
|
||||
" command = $ccache $cc -x assembler $asflags -c $in -o $out",
|
||||
" description = AS $out",
|
||||
]
|
||||
|
||||
|
||||
def pch_rule_lines() -> list[str]:
|
||||
"""The precompiled header rule, for a generator that emits one."""
|
||||
return [
|
||||
# No $ccache: the .gch is compiled once per build dir and ccache
|
||||
# cannot cache it usefully (its bytes embed build-dir paths)
|
||||
"rule pch",
|
||||
" command = $cxx -MMD -MF $out.d -x c++-header $cxxflags $flags -c $in -o $out",
|
||||
" depfile = $out.d",
|
||||
" deps = gcc",
|
||||
" description = PCH $out",
|
||||
]
|
||||
|
||||
|
||||
def pch_edges(
|
||||
lines: list[str],
|
||||
build_dir: Path,
|
||||
src_dir: Path,
|
||||
headers: Sequence[str],
|
||||
cxxflags: Sequence[str],
|
||||
src_flags: Sequence[str],
|
||||
ccache: str | None,
|
||||
identity: Sequence[str],
|
||||
) -> tuple[str, str] | None:
|
||||
"""Emit the precompiled header for the C++ src edges.
|
||||
|
||||
``headers`` are folded into one prefix header, ``src_flags`` are the
|
||||
flags every src edge carries, and ``identity`` names what the compile
|
||||
depends on beyond the headers and flags (versioned install paths).
|
||||
Returns the ``cxx_override`` for ``compile_edges``, or None when the
|
||||
build goes without a precompiled header.
|
||||
"""
|
||||
if not pch_enabled():
|
||||
pch_disabled_degraded()
|
||||
return None
|
||||
if any(tok.startswith("-include") for tok in cxxflags):
|
||||
# $cxxflags expands first, so a user -include there means GCC would
|
||||
# never load the .gch
|
||||
_LOGGER.warning(
|
||||
"A -include in build_flags prevents the precompiled header from "
|
||||
"loading; compiling without it"
|
||||
)
|
||||
pch_degraded("a user -include precedes the pch")
|
||||
return None
|
||||
pch_header = build_dir / PCH_HEADER_NAME
|
||||
pch_text = pch_header_text(headers)
|
||||
checksum = None
|
||||
try:
|
||||
if ccache:
|
||||
# The .sum exists only for CCACHE_PCH_EXTSUM; ninja's depfile
|
||||
# handles staleness. Strip resolved and raw build paths
|
||||
# (symlinks) so identical configs share cache entries
|
||||
flags_id = (
|
||||
" ".join(cxxflags)
|
||||
.replace(effective_ccache_basedir(), "")
|
||||
.replace(str(CORE.build_path), "")
|
||||
)
|
||||
# The header text covers include order
|
||||
checksum = pch_checksum(src_dir, headers, (pch_text, *identity, flags_id))
|
||||
except (OSError, UnicodeError) as err:
|
||||
# Identity unknown: a stale cache entry must never be served
|
||||
_LOGGER.warning(
|
||||
"Could not establish the pch identity; compiling without it: %s", err
|
||||
)
|
||||
pch_degraded(f"identity unknown: {err}")
|
||||
return None
|
||||
_LOGGER.info(
|
||||
"Compiling with a precompiled header (set ESPHOME_PCH_ENABLE=0 to disable)"
|
||||
)
|
||||
write_file_if_changed(pch_header, pch_text)
|
||||
sum_path = build_dir / f"{PCH_HEADER_NAME}.gch.sum"
|
||||
if checksum is not None:
|
||||
# Generate-time stamp: a hand-run ninja can rebuild the .gch
|
||||
# while this .sum lags
|
||||
write_file_if_changed(sum_path, checksum + "\n")
|
||||
else:
|
||||
# A stale .sum from an earlier ccache run must not survive
|
||||
sum_path.unlink(missing_ok=True)
|
||||
gch = _e(f"{PCH_HEADER_NAME}.gch")
|
||||
lines.append(f"build {gch}: pch {_e(pch_header)}")
|
||||
if src_flags:
|
||||
lines.append(f" flags = {' '.join(src_flags)}")
|
||||
# Relative -include: absolute would break cross-device ccache.
|
||||
# -Wno-error keeps a rejected .gch a warning under user -Werror;
|
||||
# strict inverts it so any consumer rejection reds the build
|
||||
# (rejection is per-process, so the probe alone cannot prove
|
||||
# the consumers)
|
||||
cxx_parts = [
|
||||
*src_flags,
|
||||
f"-Winvalid-pch {pch_consumer_escalation()} -include {PCH_HEADER_NAME}",
|
||||
]
|
||||
lines.append(f"srccxxflags = {' '.join(cxx_parts)}")
|
||||
pch_dep = gch
|
||||
if pch_strict():
|
||||
# Consumers wait on the probe stamp, so an unloadable .gch
|
||||
# reds the build here instead of warning ~100 times
|
||||
probe = " ".join(pch_probe_args(PCH_HEADER_NAME, source=os.devnull))
|
||||
lines.append("rule pchprobe")
|
||||
# $out only expands in rule text, hence the inline stamp
|
||||
lines.append(
|
||||
f" command = $cxx $cxxflags $flags {probe}"
|
||||
" && $python $buildtool touch $out"
|
||||
)
|
||||
lines.append(" description = PCHPROBE $out")
|
||||
# Runs when the .gch is (re)built; strict consumer -Werror
|
||||
# covers a cached .gch this process cannot load
|
||||
lines.append(f"build esphome_pch.probe: pchprobe {gch}")
|
||||
if src_flags:
|
||||
lines.append(f" flags = {' '.join(src_flags)}")
|
||||
pch_dep = f"{gch} esphome_pch.probe"
|
||||
mark_pch_emitted()
|
||||
return ("$srccxxflags", pch_dep)
|
||||
|
||||
|
||||
def ar_rule_lines(ar: Path | str) -> list[str]:
|
||||
return [
|
||||
"rule ar",
|
||||
f" command = $python $buildtool ar {_q(ar)} $out $out.rsp",
|
||||
" rspfile = $out.rsp",
|
||||
" rspfile_content = $in_newline",
|
||||
" description = AR $out",
|
||||
]
|
||||
|
||||
|
||||
def compile_edges(
|
||||
lines: list[str],
|
||||
sources: list[Path],
|
||||
root: Path,
|
||||
group: str,
|
||||
flags: str = "",
|
||||
own_includes: str = "",
|
||||
cxx_override: tuple[str, str] | None = None,
|
||||
) -> list[str]:
|
||||
"""Emit compile edges for ``sources``; return the object paths.
|
||||
|
||||
``cxx_override`` is a (flags, implicit-dep) pair applied to C++ edges
|
||||
only, replacing ``flags`` (used for the precompiled header).
|
||||
"""
|
||||
objects = []
|
||||
for src in sources:
|
||||
rel = src.relative_to(root).as_posix()
|
||||
obj = f"obj/{group}/{rel}.o"
|
||||
escaped_obj = _e(obj)
|
||||
kind = SOURCE_KIND_FOR_SUFFIX[src.suffix]
|
||||
override = cxx_override if kind == "cxx" else None
|
||||
implicit = f" | {override[1]}" if override else ""
|
||||
lines.append(f"build {escaped_obj}: {kind} {_e(src)}{implicit}")
|
||||
if own_includes:
|
||||
lines.append(f" own_includes = {own_includes}")
|
||||
if edge_flags := override[0] if override else flags:
|
||||
lines.append(f" flags = {edge_flags}")
|
||||
# Escaped once here: the returned paths only ever appear in build
|
||||
# statements (archive/link inputs), which use ninja escaping
|
||||
objects.append(escaped_obj)
|
||||
return objects
|
||||
|
||||
|
||||
def library_edges(
|
||||
lines: list[str], libraries: list[ArduinoLibrary]
|
||||
) -> tuple[list[str], list[str]]:
|
||||
"""Emit every library's compile and archive edges.
|
||||
|
||||
Returns the archive names and the objects that link directly. A
|
||||
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
|
||||
header of the same name would shadow them.
|
||||
"""
|
||||
archives: list[str] = []
|
||||
direct_objs: list[str] = []
|
||||
for lib in libraries:
|
||||
if not lib.sources:
|
||||
# Header-only libraries are legitimate; the log makes an empty
|
||||
# srcFilter or broken tree traceable before link errors do
|
||||
_LOGGER.debug(
|
||||
"Library %s has no source files; contributing includes only",
|
||||
lib.name,
|
||||
)
|
||||
continue
|
||||
objs = compile_edges(
|
||||
lines,
|
||||
lib.sources,
|
||||
common_parent(lib.sources),
|
||||
f"lib/{lib.name}",
|
||||
flags=" ".join(_shell_token(f) for f in lib.flags),
|
||||
own_includes=" ".join(f"-I{_q(d)}" for d in lib.include_dirs),
|
||||
)
|
||||
if not lib.lib_archive:
|
||||
# libArchive: false / dot_a_linkage=false: hand the objects to
|
||||
# the linker directly so unreferenced-but-required symbols
|
||||
# (exception handlers, weak overrides) survive
|
||||
direct_objs.extend(objs)
|
||||
continue
|
||||
archive = f"lib{lib.name}.a"
|
||||
lines.append(f"build {_e(archive)}: ar {' '.join(objs)}")
|
||||
archives.append(archive)
|
||||
return archives, direct_objs
|
||||
|
||||
|
||||
# One build flag: the flag, plus its argument when that is a separate token
|
||||
Flag = tuple[str, ...]
|
||||
|
||||
# Flags whose path operand is the next token; gcc also takes it glued on
|
||||
PATH_ARG_FLAGS = ("-include", "-imacros", "-isystem", "-iquote", "-idirafter")
|
||||
# Flags whose path operand is glued on
|
||||
PATH_PREFIXES = ("-I", "-L", *PATH_ARG_FLAGS)
|
||||
|
||||
|
||||
def _anchor(path: str, base: Path) -> str:
|
||||
if not path or Path(path).is_absolute():
|
||||
return path
|
||||
return str(base / path)
|
||||
|
||||
|
||||
def anchor_path_flag(flag: Flag, base: Path) -> Flag:
|
||||
"""Anchor a flag's relative path operand at ``base``.
|
||||
|
||||
PlatformIO ran the compiler from the build path; ninja runs it from
|
||||
``.pioenvs/<name>``, where a relative operand would point elsewhere.
|
||||
"""
|
||||
name, *args = flag
|
||||
if args:
|
||||
if name in PATH_ARG_FLAGS:
|
||||
return (name, _anchor(args[0], base))
|
||||
return flag
|
||||
for prefix in PATH_PREFIXES:
|
||||
if name.startswith(prefix):
|
||||
return (prefix + _anchor(name[len(prefix) :], base),)
|
||||
return flag
|
||||
@@ -1,549 +0,0 @@
|
||||
"""Shared precompiled-header policy for the build backends.
|
||||
|
||||
The prefix either mirrors the TUs' own force-includes (ESP8266) or is a
|
||||
curated core-header set (ESP-IDF). ``esphome: includes:`` sources receive
|
||||
it too; Arduino.h visibility there is intended (esphome#8693).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable
|
||||
from contextlib import suppress
|
||||
from dataclasses import dataclass
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
import posixpath
|
||||
import re
|
||||
import stat
|
||||
import subprocess
|
||||
|
||||
from esphome.build_helpers.ccache import effective_ccache_basedir, parse_enable_env
|
||||
from esphome.build_helpers.idedata import (
|
||||
CXX_SOURCE_SUFFIXES,
|
||||
expand_response_files,
|
||||
is_launcher,
|
||||
split_command,
|
||||
)
|
||||
|
||||
_DOMAIN = "pch"
|
||||
|
||||
|
||||
@dataclass
|
||||
class _PCHData:
|
||||
emitted: bool = False
|
||||
|
||||
|
||||
def _pch_data() -> _PCHData:
|
||||
from esphome.core import CORE
|
||||
|
||||
if _DOMAIN not in CORE.data:
|
||||
CORE.data[_DOMAIN] = _PCHData()
|
||||
return CORE.data[_DOMAIN]
|
||||
|
||||
|
||||
def mark_pch_emitted() -> None:
|
||||
"""Record that this build's consumers reference the pch."""
|
||||
_pch_data().emitted = True
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# The header and its .gch/.sum sidecars live in the build directory.
|
||||
PCH_HEADER_NAME = "esphome_pch.h"
|
||||
|
||||
# Every artifact the pch machinery can leave behind, for cleanup.
|
||||
PCH_ARTIFACT_NAMES = (
|
||||
PCH_HEADER_NAME,
|
||||
f"{PCH_HEADER_NAME}.gch",
|
||||
f"{PCH_HEADER_NAME}.gch.sum",
|
||||
f"{PCH_HEADER_NAME}.gch.failed",
|
||||
)
|
||||
|
||||
# The core defines header every backend anchors its prefix on.
|
||||
PCH_CORE_HEADER = "esphome/core/defines.h"
|
||||
|
||||
# Prefix-header contents for backends that inject a curated set (rather
|
||||
# than mirroring the TUs' own force-includes), defines.h first so USE_*
|
||||
# macros exist for the rest. Deliberately hard-coded: frequency-derived
|
||||
# sets measured no better and kept selecting headers that cannot compile
|
||||
# standalone (X-macro, platform-variant). Every entry must be safe to
|
||||
# include first in an empty TU. Caveat: application.h/automation.h become
|
||||
# ambiently visible, so a TU missing those #includes still builds on such
|
||||
# backends; ESPHOME_PCH_ENABLE=0 restores the strict view.
|
||||
PCH_DEFAULT_HEADERS = (
|
||||
PCH_CORE_HEADER,
|
||||
"esphome/core/component.h",
|
||||
"esphome/core/helpers.h",
|
||||
"esphome/core/log.h",
|
||||
"esphome/core/application.h",
|
||||
"esphome/core/automation.h",
|
||||
)
|
||||
|
||||
# ccache cannot hash through a .gch; CCACHE_PCH_EXTSUM makes it hash the
|
||||
# .sum sidecar instead of the .gch bytes, which are not reproducible.
|
||||
# Keep in sync with the literals in platformio/pch.py.script.
|
||||
_CCACHE_PCH_ENV = {
|
||||
"CCACHE_SLOPPINESS": "pch_defines,time_macros",
|
||||
"CCACHE_PCH_EXTSUM": "true",
|
||||
}
|
||||
|
||||
# Both include forms: an angle include resolving under src/ must enter the
|
||||
# digest too; ones that do not resolve simply end the walk
|
||||
# Compiler failures that clear on their own must not latch the .failed marker
|
||||
_TRANSIENT_ERRORS = ("No space left", "Cannot allocate", "Resource temporarily")
|
||||
|
||||
_INCLUDE_RE = re.compile(rb'^\s*#\s*include\s+["<]([^">]+)[">]', re.MULTILINE)
|
||||
|
||||
|
||||
def pch_enabled() -> bool:
|
||||
"""Precompiled-header knob: default on, ``ESPHOME_PCH_ENABLE=0`` opts out."""
|
||||
return parse_enable_env("ESPHOME_PCH_ENABLE") is not False
|
||||
|
||||
|
||||
def pch_strict() -> bool:
|
||||
"""CI knob: ``ESPHOME_PCH_STRICT=1`` turns pch degrade paths fatal.
|
||||
|
||||
A set-but-unrecognized value raises: a typo must not silently turn
|
||||
the gate into a no-op that proves nothing.
|
||||
"""
|
||||
return parse_enable_env("ESPHOME_PCH_STRICT", strict=True) is True
|
||||
|
||||
|
||||
def pch_degraded(reason: str) -> None:
|
||||
"""Every degrade path funnels through here; strict mode raises."""
|
||||
if pch_strict():
|
||||
from esphome.core import EsphomeError
|
||||
|
||||
raise EsphomeError(f"ESPHOME_PCH_STRICT: {reason}")
|
||||
|
||||
|
||||
def pch_disabled_degraded() -> None:
|
||||
"""Strict CI must not read "no pch at all" as success."""
|
||||
pch_degraded("pch disabled by ESPHOME_PCH_ENABLE")
|
||||
|
||||
|
||||
def pch_probe_tail(source: str = "-") -> list[str]:
|
||||
"""The syntax-only compile shared by the probe and its baseline."""
|
||||
return ["-fsyntax-only", "-x", "c++", source]
|
||||
|
||||
|
||||
def pch_probe_args(header: str, source: str = "-") -> list[str]:
|
||||
"""Flags that load-check a built .gch via a syntax-only compile.
|
||||
|
||||
Rejection must be a nonzero exit (never just a wording match), so the
|
||||
invalid-pch class is always escalated. ``source`` defaults to stdin
|
||||
(host independent); the ninja probe edge passes a real file.
|
||||
"""
|
||||
return [
|
||||
"-Winvalid-pch",
|
||||
"-Werror=invalid-pch",
|
||||
"-include",
|
||||
header,
|
||||
*pch_probe_tail(source),
|
||||
]
|
||||
|
||||
|
||||
def pch_consumer_escalation() -> str:
|
||||
"""Consumer-side invalid-pch flag: strict reds the build on rejection
|
||||
(per-process, so the probe alone cannot prove the consumers)."""
|
||||
return "-Werror=invalid-pch" if pch_strict() else "-Wno-error=invalid-pch"
|
||||
|
||||
|
||||
def ccache_pch_env() -> dict[str, str]:
|
||||
"""Settings ccache needs to cache compiles that consume the .gch;
|
||||
empty unless this build actually emitted one. User-set values win.
|
||||
Native backends export these process-wide; only time_macros affects
|
||||
non-pch TUs."""
|
||||
if not (pch_enabled() and _pch_data().emitted):
|
||||
return {}
|
||||
extsum = os.environ.get("CCACHE_PCH_EXTSUM")
|
||||
if extsum is not None and extsum.strip().lower() not in ("1", "true", "yes", "on"):
|
||||
# ccache then hashes the non-reproducible .gch bytes: permanent misses
|
||||
_LOGGER.warning("CCACHE_PCH_EXTSUM=%s disables pch caching", extsum)
|
||||
env = {k: v for k, v in _CCACHE_PCH_ENV.items() if k not in os.environ}
|
||||
user_sloppiness = os.environ.get("CCACHE_SLOPPINESS")
|
||||
if user_sloppiness is not None and (
|
||||
missing := [
|
||||
t
|
||||
for t in ("pch_defines", "time_macros")
|
||||
if t not in {tok.strip() for tok in user_sloppiness.split(",")}
|
||||
]
|
||||
):
|
||||
# Without these ccache declines every pch-consuming compile
|
||||
env["CCACHE_SLOPPINESS"] = ",".join((user_sloppiness, *missing))
|
||||
_LOGGER.warning(
|
||||
"Adding %s to CCACHE_SLOPPINESS so ccache can cache compiles "
|
||||
"that use the precompiled header",
|
||||
",".join(missing),
|
||||
)
|
||||
return env
|
||||
|
||||
|
||||
def pch_extra_scripts() -> list[str]:
|
||||
"""The extra_scripts entries a PlatformIO platform registers for the
|
||||
pch; empty when disabled (the script itself has no enable check)."""
|
||||
if not pch_enabled():
|
||||
pch_disabled_degraded()
|
||||
return []
|
||||
return ["post:pch.py"]
|
||||
|
||||
|
||||
def pch_header_text(include_headers: Iterable[str]) -> str:
|
||||
"""The prefix-header source: exactly these includes, in order."""
|
||||
return "".join(f'#include "{name}"\n' for name in include_headers)
|
||||
|
||||
|
||||
def _resolves(path: Path) -> bool:
|
||||
"""False when missing; other stat failures propagate (identity unknown,
|
||||
unlike is_file(), which would silently drop the header)."""
|
||||
try:
|
||||
return stat.S_ISREG(path.stat().st_mode)
|
||||
except (FileNotFoundError, NotADirectoryError):
|
||||
return False
|
||||
|
||||
|
||||
def _include_closure(src_dir: Path, roots: Iterable[str]) -> dict[str, bytes]:
|
||||
"""Include closure of ``roots``: src-relative name -> contents.
|
||||
|
||||
Resolution mirrors the compiler (includer's dir, then src root); names
|
||||
outside ``src_dir`` end the walk and are versioned by the caller. No
|
||||
#ifdef evaluation: over-approximating is the safe direction.
|
||||
"""
|
||||
seen: dict[str, bytes] = {}
|
||||
stack: list[tuple[str, str]] = [(name, "") for name in roots]
|
||||
while stack:
|
||||
name, from_dir = stack.pop()
|
||||
for candidate in (f"{from_dir}/{name}" if from_dir else name, name):
|
||||
rel = posixpath.normpath(candidate)
|
||||
if not rel.startswith("..") and _resolves(src_dir / rel):
|
||||
break
|
||||
else:
|
||||
continue
|
||||
if rel in seen:
|
||||
continue
|
||||
try:
|
||||
data = (src_dir / rel).read_bytes()
|
||||
except OSError as err:
|
||||
# A marker would truncate the transitive walk; fail closed
|
||||
_LOGGER.warning("Could not read %s for the pch checksum: %s", rel, err)
|
||||
raise
|
||||
seen[rel] = data
|
||||
parent = posixpath.dirname(rel)
|
||||
stack.extend(
|
||||
# surrogateescape: a non-UTF-8 name just fails to resolve
|
||||
(inc.decode(errors="surrogateescape"), parent)
|
||||
for inc in _INCLUDE_RE.findall(data)
|
||||
)
|
||||
return seen
|
||||
|
||||
|
||||
def pch_checksum(
|
||||
src_dir: Path, include_headers: Iterable[str], extra: Iterable[str]
|
||||
) -> str:
|
||||
"""Digest standing in for the .gch in ccache's hash: the include closure
|
||||
of the prefix header plus caller-supplied identity strings (versioned
|
||||
install paths, flags). Raises OSError when a header's identity cannot
|
||||
be established at all; callers must then compile without a pch."""
|
||||
digest = hashlib.sha256()
|
||||
closure = _include_closure(src_dir, include_headers)
|
||||
for name in sorted(closure):
|
||||
digest.update(name.encode(errors="surrogateescape"))
|
||||
digest.update(closure[name])
|
||||
digest.update(b"\0")
|
||||
for item in extra:
|
||||
digest.update(item.encode(errors="surrogateescape"))
|
||||
digest.update(b"\0")
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
# Tokens dropped when retargeting a TU's flags at the prefix header
|
||||
# (the pch compile must not touch depfiles)
|
||||
_PCH_STRIP_FLAGS_WITH_ARG = frozenset({"-o", "-c", "-MT", "-MF", "-MQ"})
|
||||
_PCH_STRIP_FLAGS = frozenset({"-MD", "-MMD", "-MP", "-MM", "-M"})
|
||||
|
||||
|
||||
def pch_compile_command(
|
||||
build_dir: Path, header: Path, gch: Path
|
||||
) -> tuple[list[str], Path] | None:
|
||||
"""The exact src C++ flags from compile_commands.json retargeted at the
|
||||
header, with the directory they resolve against (relative -I paths must
|
||||
be expanded and executed from the same root); None (logged) when no
|
||||
configured C++ TU is available yet."""
|
||||
from esphome.core import CORE
|
||||
|
||||
try:
|
||||
entries = json.loads(
|
||||
(build_dir / "compile_commands.json").read_text(encoding="utf-8")
|
||||
)
|
||||
except (OSError, json.JSONDecodeError) as err:
|
||||
# Configure already succeeded, so an unusable DB is a real anomaly
|
||||
_LOGGER.warning("No usable compile database, skipping pch: %s", err)
|
||||
return None
|
||||
if not isinstance(entries, list):
|
||||
_LOGGER.warning("Malformed compile database, skipping pch")
|
||||
return None
|
||||
# CMake may spell paths through a symlink differently than CORE does
|
||||
# (macOS /tmp vs /private/tmp), so compare resolved paths
|
||||
src_root = Path(CORE.relative_src_path()).resolve()
|
||||
entry = next(
|
||||
(
|
||||
e
|
||||
for e in entries
|
||||
if isinstance(e, dict)
|
||||
and isinstance(e.get("file"), str)
|
||||
and e["file"].endswith(CXX_SOURCE_SUFFIXES)
|
||||
and Path(e["file"]).resolve().is_relative_to(src_root)
|
||||
),
|
||||
None,
|
||||
)
|
||||
if entry is None:
|
||||
_LOGGER.warning("No src C++ entry in the compile database, skipping pch")
|
||||
return None
|
||||
directory = entry.get("directory")
|
||||
cmd_dir = Path(directory) if isinstance(directory, str) and directory else build_dir
|
||||
command = entry.get("command")
|
||||
tokens = expand_response_files(
|
||||
split_command(command if isinstance(command, str) else ""), cmd_dir
|
||||
)
|
||||
# A DB recorded with ccache enabled prefixes the compiler with the
|
||||
# launcher; the .gch must be compiled directly
|
||||
if tokens and is_launcher(tokens[0]):
|
||||
tokens = tokens[1:]
|
||||
if not tokens:
|
||||
# "arguments"-style or empty entries must skip, not spawn "-x ..."
|
||||
_LOGGER.warning("Compile database entry has no usable command, skipping pch")
|
||||
return None
|
||||
args: list[str] = []
|
||||
arg_it = iter(tokens)
|
||||
for tok in arg_it:
|
||||
if tok in _PCH_STRIP_FLAGS_WITH_ARG:
|
||||
next(arg_it, None)
|
||||
continue
|
||||
if tok in _PCH_STRIP_FLAGS:
|
||||
continue
|
||||
if tok == "-include":
|
||||
# Drop only the injected prefix; user force-includes must reach
|
||||
# the .gch compile or GCC rejects it over the macro mismatch
|
||||
inc = next(arg_it, "")
|
||||
if not inc.endswith(PCH_HEADER_NAME):
|
||||
args.extend(("-include", inc))
|
||||
continue
|
||||
args.append(tok)
|
||||
return [*args, "-x", "c++-header", "-c", str(header), "-o", str(gch)], cmd_dir
|
||||
|
||||
|
||||
def _log_pch_in_use() -> None:
|
||||
# The only place a user can discover the knob; emitted only once a
|
||||
# .gch is actually fresh or being built
|
||||
_LOGGER.info(
|
||||
"Compiling with a precompiled header (set ESPHOME_PCH_ENABLE=0 to disable)"
|
||||
)
|
||||
|
||||
|
||||
def _read_stamp(path: Path) -> str:
|
||||
"""A corrupt sidecar must read as stale, not kill the pch forever."""
|
||||
try:
|
||||
return path.read_text(encoding="utf-8").strip()
|
||||
except (OSError, UnicodeDecodeError):
|
||||
return ""
|
||||
|
||||
|
||||
def discard_pch(build_dir: Path) -> None:
|
||||
"""Remove the pch sidecars so a stale .gch is never consumed.
|
||||
|
||||
Bumps the header only when a .gch was actually removed: TUs compiled
|
||||
against it have incomplete depfiles, while a repeat failure with no
|
||||
.gch must not force a full rebuild every build. A .gch that survives
|
||||
an unlink failure would be consumed silently (wrong output, not a
|
||||
slow build), so that raises.
|
||||
"""
|
||||
header = build_dir / PCH_HEADER_NAME
|
||||
gch = Path(f"{header}.gch")
|
||||
had_gch = gch.is_file()
|
||||
errors = []
|
||||
for sidecar in (gch, Path(f"{gch}.sum")):
|
||||
try:
|
||||
sidecar.unlink(missing_ok=True)
|
||||
except OSError as err:
|
||||
if sidecar.is_file():
|
||||
from esphome.core import EsphomeError
|
||||
|
||||
raise EsphomeError(
|
||||
f"Could not discard the stale precompiled header: {err}"
|
||||
) from err
|
||||
errors.append(err)
|
||||
for err in errors:
|
||||
_LOGGER.warning("Could not discard the pch sidecars: %s", err)
|
||||
if had_gch and header.is_file():
|
||||
with suppress(OSError):
|
||||
os.utime(header)
|
||||
|
||||
|
||||
def prepare_pch(
|
||||
build_dir: Path, include_headers: tuple[str, ...], extra: Iterable[str]
|
||||
) -> None:
|
||||
"""Compile ``build_dir``'s .gch from compile_commands.json flags and
|
||||
write its ccache .sum.
|
||||
|
||||
The .sum doubles as the freshness stamp and folds in the compile
|
||||
command, so a flag-only change rebuilds the .gch; ``extra`` carries
|
||||
backend identity (framework version, sdkconfig, ...). A failed
|
||||
compile falls back to the plain header include.
|
||||
"""
|
||||
from esphome.core import CORE
|
||||
|
||||
header = build_dir / PCH_HEADER_NAME
|
||||
gch = Path(f"{header}.gch")
|
||||
sum_path = Path(f"{gch}.sum")
|
||||
cmd_and_dir = pch_compile_command(build_dir, header, gch)
|
||||
if cmd_and_dir is None:
|
||||
# Freshness cannot be validated; a leftover .gch must not be consumed
|
||||
discard_pch(build_dir)
|
||||
pch_degraded("no usable compile command")
|
||||
return
|
||||
cmd, cmd_dir = cmd_and_dir
|
||||
# Strip like ccache's rewriting (user CCACHE_BASEDIR wins); the raw
|
||||
# build path covers unresolved (symlinked) spellings
|
||||
cmd_id = (
|
||||
" ".join(cmd)
|
||||
.replace(effective_ccache_basedir(), "")
|
||||
.replace(str(CORE.build_path), "")
|
||||
)
|
||||
try:
|
||||
checksum = pch_checksum(
|
||||
CORE.relative_src_path(),
|
||||
include_headers,
|
||||
(
|
||||
# The closure is sorted, so root order only enters via the text
|
||||
pch_header_text(include_headers),
|
||||
*extra,
|
||||
cmd_id,
|
||||
),
|
||||
)
|
||||
except (OSError, UnicodeError) as err:
|
||||
# Identity unknown: a stale cache entry must never be served
|
||||
_LOGGER.warning(
|
||||
"Could not establish the pch identity; compiling without it: %s", err
|
||||
)
|
||||
discard_pch(build_dir)
|
||||
pch_degraded(f"identity unknown: {err}")
|
||||
return
|
||||
failed_marker = Path(f"{gch}.failed")
|
||||
|
||||
def _run(
|
||||
run_cmd: list[str], what: str, stdin: str | None = None
|
||||
) -> subprocess.CompletedProcess | None:
|
||||
"""Spawn one pch tool step; environmental failures discard and
|
||||
degrade (None): spawn/IO/timeout errors and signal kills never
|
||||
latch the marker."""
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
run_cmd,
|
||||
cwd=cmd_dir,
|
||||
# C locale keeps diagnostics matchable by _TRANSIENT_ERRORS
|
||||
env={**os.environ, "LC_ALL": "C"},
|
||||
input=stdin,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
timeout=300,
|
||||
)
|
||||
except (OSError, subprocess.SubprocessError) as err:
|
||||
_LOGGER.warning("Precompiled header %s did not run: %s", what, err)
|
||||
discard_pch(build_dir)
|
||||
pch_degraded(f"{what} did not run: {err}")
|
||||
return None
|
||||
if proc.returncode < 0:
|
||||
# Killed by a signal (OOM, ^C): environmental, do not latch
|
||||
_LOGGER.warning(
|
||||
"Precompiled header %s was killed (signal %d); retrying next build",
|
||||
what,
|
||||
-proc.returncode,
|
||||
)
|
||||
discard_pch(build_dir)
|
||||
pch_degraded(f"{what} killed by signal {-proc.returncode}")
|
||||
return None
|
||||
return proc
|
||||
|
||||
def _fail(error: str, reason: str, latch: bool) -> None:
|
||||
"""Discard and degrade; deterministic failures latch when asked."""
|
||||
_LOGGER.warning(
|
||||
"Precompiled header failed; compiling without it: %s", error[:400]
|
||||
)
|
||||
# Latching paths keep the full compiler output recoverable
|
||||
_LOGGER.debug("Full pch output: %s", error)
|
||||
discard_pch(build_dir)
|
||||
if latch and not any(m in error for m in _TRANSIENT_ERRORS):
|
||||
# Skip retries until a header/flag/backend-identity/command change
|
||||
failed_marker.write_text(checksum + "\n", encoding="utf-8")
|
||||
os.utime(header)
|
||||
pch_degraded(f"{reason}: {error[:200]}")
|
||||
|
||||
def _probe(latch: bool = True) -> None:
|
||||
"""Load-check the built .gch: some toolchains build one they then
|
||||
refuse to load (per-process ASLR). Dep flags are already stripped
|
||||
from cmd, so no -MF is needed; cmd ends with the fixed
|
||||
"-x c++-header -c -o" tail. A cached-header rejection may not
|
||||
reproduce (per-process), so that caller passes latch=False."""
|
||||
if cmd[-6:-4] != ["-x", "c++-header"]:
|
||||
# The slice below depends on pch_compile_command's fixed tail
|
||||
_LOGGER.warning("Unexpected pch command shape: %s", cmd[-6:])
|
||||
discard_pch(build_dir)
|
||||
pch_degraded("unexpected pch command shape")
|
||||
return
|
||||
base = cmd[:-6]
|
||||
probe = _run([*base, *pch_probe_args(str(header))], "probe", stdin="")
|
||||
if probe is None:
|
||||
return
|
||||
if probe.returncode != 0:
|
||||
# Disambiguate: only blame the pch when the same compile passes
|
||||
# without it; a failing baseline is its own (latchable) problem
|
||||
baseline = _run([*base, *pch_probe_tail()], "probe baseline", stdin="")
|
||||
if baseline is None:
|
||||
return
|
||||
if baseline.returncode == 0:
|
||||
error = probe.stderr.strip() or f"exit code {probe.returncode}"
|
||||
_fail(error, "toolchain cannot load the pch", latch=latch)
|
||||
else:
|
||||
error = baseline.stderr.strip() or f"exit code {baseline.returncode}"
|
||||
_fail(error, "probe cannot run at all", latch=latch)
|
||||
|
||||
if gch.is_file() and _read_stamp(sum_path) == checksum:
|
||||
_log_pch_in_use()
|
||||
if pch_strict():
|
||||
# Rejection is per-process, so a cached .gch must re-prove
|
||||
# loadability for the strict gate (CI-only cost); no latch,
|
||||
# since the rejection may not reproduce either
|
||||
_probe(latch=False)
|
||||
return
|
||||
if _read_stamp(failed_marker) == checksum:
|
||||
_LOGGER.info(
|
||||
"Precompiled header disabled after an earlier failure; delete %s to retry",
|
||||
failed_marker,
|
||||
)
|
||||
pch_degraded("earlier failure latched")
|
||||
return
|
||||
_log_pch_in_use()
|
||||
result = _run(cmd, "compile")
|
||||
if result is None:
|
||||
return
|
||||
error = None
|
||||
if result.returncode != 0:
|
||||
error = result.stderr.strip() or f"exit code {result.returncode}"
|
||||
elif not gch.is_file():
|
||||
error = "compiler produced no .gch"
|
||||
if error is not None:
|
||||
_fail(error, "compile failed", latch=True)
|
||||
return
|
||||
_probe()
|
||||
if not gch.is_file():
|
||||
# The probe discarded a rejected or unrunnable .gch
|
||||
return
|
||||
failed_marker.unlink(missing_ok=True)
|
||||
sum_path.write_text(checksum + "\n", encoding="utf-8")
|
||||
# Consumers depend on the header (depfiles cannot see through a .gch);
|
||||
# bump it so users of the previous .gch recompile
|
||||
os.utime(header)
|
||||
@@ -33,11 +33,4 @@ def tools_cache_path(env_var: str, subdir: str) -> Path:
|
||||
IDF_TOOLS_CACHE = ("ESPHOME_ESP_IDF_PREFIX", "idf")
|
||||
SDK_NRF_TOOLS_CACHE = ("ESPHOME_SDK_NRF_PREFIX", "sdk-nrf")
|
||||
ARDUINO8266_TOOLS_CACHE = ("ESPHOME_ARDUINO8266_PREFIX", "arduino8266")
|
||||
# The host backend installs nothing; the entry only holds its ccache dir
|
||||
HOST_TOOLS_CACHE = ("ESPHOME_HOST_PREFIX", "host")
|
||||
TOOLS_CACHE_SPECS = (
|
||||
IDF_TOOLS_CACHE,
|
||||
SDK_NRF_TOOLS_CACHE,
|
||||
ARDUINO8266_TOOLS_CACHE,
|
||||
HOST_TOOLS_CACHE,
|
||||
)
|
||||
TOOLS_CACHE_SPECS = (IDF_TOOLS_CACHE, SDK_NRF_TOOLS_CACHE, ARDUINO8266_TOOLS_CACHE)
|
||||
@@ -42,7 +42,6 @@ from esphome.cpp_generator import ( # noqa: F401
|
||||
set_cpp_standard,
|
||||
statement,
|
||||
static_const_array,
|
||||
static_function,
|
||||
templatable,
|
||||
variable,
|
||||
with_local_variable,
|
||||
|
||||
@@ -106,20 +106,6 @@ bool AGS10Component::set_zero_point_with_factory_defaults() { return this->set_z
|
||||
|
||||
bool AGS10Component::set_zero_point_with_current_resistance() { return this->set_zero_point_with(ZP_CURRENT); }
|
||||
|
||||
void AGS10Component::set_zero_point(AGS10SetZeroPointActionMode mode, uint16_t value) {
|
||||
switch (mode) {
|
||||
case FACTORY_DEFAULT:
|
||||
this->set_zero_point_with_factory_defaults();
|
||||
break;
|
||||
case CURRENT_VALUE:
|
||||
this->set_zero_point_with_current_resistance();
|
||||
break;
|
||||
case CUSTOM_VALUE:
|
||||
this->set_zero_point_with(value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool AGS10Component::set_zero_point_with(uint16_t value) {
|
||||
std::array<uint8_t, 5> data{0x00, 0x0C, (uint8_t) ((value >> 8) & 0xFF), (uint8_t) (value & 0xFF), 0};
|
||||
data[4] = crc8(data.data(), 4, 0xFF, 0x31, true);
|
||||
|
||||
@@ -2,19 +2,11 @@
|
||||
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
namespace esphome::ags10 {
|
||||
|
||||
enum AGS10SetZeroPointActionMode {
|
||||
// Zero-point reset.
|
||||
FACTORY_DEFAULT,
|
||||
// Zero-point calibration with current resistance.
|
||||
CURRENT_VALUE,
|
||||
// Zero-point calibration with custom resistance.
|
||||
CUSTOM_VALUE,
|
||||
};
|
||||
|
||||
class AGS10Component final : public PollingComponent, public i2c::I2CDevice {
|
||||
public:
|
||||
/**
|
||||
@@ -55,11 +47,6 @@ class AGS10Component final : public PollingComponent, public i2c::I2CDevice {
|
||||
*/
|
||||
bool set_zero_point_with_current_resistance();
|
||||
|
||||
/**
|
||||
* Sets zero-point by mode; the value is only used for CUSTOM_VALUE.
|
||||
*/
|
||||
void set_zero_point(AGS10SetZeroPointActionMode mode, uint16_t value);
|
||||
|
||||
/**
|
||||
* Sets zero-point with the value.
|
||||
*/
|
||||
@@ -113,4 +100,39 @@ class AGS10Component final : public PollingComponent, public i2c::I2CDevice {
|
||||
template<size_t N> optional<std::array<uint8_t, N>> read_and_check_(uint8_t a_register);
|
||||
};
|
||||
|
||||
template<typename... Ts> class AGS10NewI2cAddressAction final : public Action<Ts...>, public Parented<AGS10Component> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(uint8_t, new_address)
|
||||
|
||||
void play(const Ts &...x) override { this->parent_->new_i2c_address(this->new_address_.value(x...)); }
|
||||
};
|
||||
|
||||
enum AGS10SetZeroPointActionMode {
|
||||
// Zero-point reset.
|
||||
FACTORY_DEFAULT,
|
||||
// Zero-point calibration with current resistance.
|
||||
CURRENT_VALUE,
|
||||
// Zero-point calibration with custom resistance.
|
||||
CUSTOM_VALUE,
|
||||
};
|
||||
|
||||
template<typename... Ts> class AGS10SetZeroPointAction final : public Action<Ts...>, public Parented<AGS10Component> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(uint16_t, value)
|
||||
TEMPLATABLE_VALUE(AGS10SetZeroPointActionMode, mode)
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
switch (this->mode_.value(x...)) {
|
||||
case FACTORY_DEFAULT:
|
||||
this->parent_->set_zero_point_with_factory_defaults();
|
||||
break;
|
||||
case CURRENT_VALUE:
|
||||
this->parent_->set_zero_point_with_current_resistance();
|
||||
break;
|
||||
case CUSTOM_VALUE:
|
||||
this->parent_->set_zero_point_with(this->value_.value(x...));
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
} // namespace esphome::ags10
|
||||
@@ -17,6 +17,8 @@ from esphome.const import (
|
||||
UNIT_OHM,
|
||||
UNIT_PARTS_PER_BILLION,
|
||||
)
|
||||
from esphome.core import ID
|
||||
from esphome.cpp_generator import MockObj, TemplateArgsType
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CONF_RESISTANCE = "resistance"
|
||||
@@ -26,6 +28,12 @@ DEPENDENCIES = ["i2c"]
|
||||
ags10_ns = cg.esphome_ns.namespace("ags10")
|
||||
AGS10Component = ags10_ns.class_("AGS10Component", cg.PollingComponent, i2c.I2CDevice)
|
||||
|
||||
# Actions
|
||||
AGS10NewI2cAddressAction = ags10_ns.class_(
|
||||
"AGS10NewI2cAddressAction", automation.Action
|
||||
)
|
||||
AGS10SetZeroPointAction = ags10_ns.class_("AGS10SetZeroPointAction", automation.Action)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
@@ -83,11 +91,24 @@ AGS10_NEW_I2C_ADDRESS_SCHEMA = cv.maybe_simple_value(
|
||||
)
|
||||
|
||||
|
||||
automation.register_apply_action(
|
||||
@automation.register_action(
|
||||
"ags10.new_i2c_address",
|
||||
AGS10NewI2cAddressAction,
|
||||
AGS10_NEW_I2C_ADDRESS_SCHEMA,
|
||||
automation.ApplyField(CONF_ADDRESS, "new_i2c_address", cg.uint8),
|
||||
synchronous=True,
|
||||
)
|
||||
async def ags10newi2caddress_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
address = await cg.templatable(config[CONF_ADDRESS], args, cg.uint8)
|
||||
cg.add(var.set_new_address(address))
|
||||
return var
|
||||
|
||||
|
||||
AGS10SetZeroPointActionMode = ags10_ns.enum("AGS10SetZeroPointActionMode")
|
||||
AGS10_SET_ZERO_POINT_ACTION_MODE = {
|
||||
@@ -107,11 +128,24 @@ AGS10_SET_ZERO_POINT_SCHEMA = cv.Schema(
|
||||
)
|
||||
|
||||
|
||||
automation.register_apply_action(
|
||||
@automation.register_action(
|
||||
"ags10.set_zero_point",
|
||||
AGS10SetZeroPointAction,
|
||||
AGS10_SET_ZERO_POINT_SCHEMA,
|
||||
automation.ApplyCall(
|
||||
"set_zero_point({}, {})",
|
||||
((CONF_MODE, AGS10SetZeroPointActionMode), (CONF_VALUE, cg.uint16)),
|
||||
),
|
||||
synchronous=True,
|
||||
)
|
||||
async def ags10setzeropoint_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
mode = await cg.templatable(
|
||||
config.get(CONF_MODE), args, AGS10SetZeroPointActionMode
|
||||
)
|
||||
cg.add(var.set_mode(mode))
|
||||
value = await cg.templatable(config[CONF_VALUE], args, cg.uint16)
|
||||
cg.add(var.set_value(value))
|
||||
return var
|
||||
@@ -4,6 +4,8 @@ from esphome.components import i2c
|
||||
from esphome.components.audio_dac import AudioDac
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_MODE
|
||||
from esphome.core import ID
|
||||
from esphome.cpp_generator import MockObj, TemplateArgsType
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@kbx81"]
|
||||
@@ -12,6 +14,7 @@ DEPENDENCIES = ["i2c"]
|
||||
aic3204_ns = cg.esphome_ns.namespace("aic3204")
|
||||
AIC3204 = aic3204_ns.class_("AIC3204", AudioDac, cg.Component, i2c.I2CDevice)
|
||||
|
||||
SetAutoMuteAction = aic3204_ns.class_("SetAutoMuteAction", automation.Action)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
@@ -33,11 +36,25 @@ SET_AUTO_MUTE_ACTION_SCHEMA = cv.maybe_simple_value(
|
||||
)
|
||||
|
||||
|
||||
automation.register_apply_action(
|
||||
@automation.register_action(
|
||||
"aic3204.set_auto_mute_mode",
|
||||
SetAutoMuteAction,
|
||||
SET_AUTO_MUTE_ACTION_SCHEMA,
|
||||
automation.ApplyField(CONF_MODE, "set_auto_mute_mode", cg.uint8),
|
||||
synchronous=True,
|
||||
)
|
||||
async def aic3204_set_volume_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
template_ = await cg.templatable(config.get(CONF_MODE), args, cg.uint8)
|
||||
cg.add(var.set_auto_mute_mode(template_))
|
||||
|
||||
return var
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "aic3204.h"
|
||||
|
||||
namespace esphome::aic3204 {
|
||||
|
||||
template<typename... Ts> class SetAutoMuteAction final : public Action<Ts...> {
|
||||
public:
|
||||
explicit SetAutoMuteAction(AIC3204 *aic3204) : aic3204_(aic3204) {}
|
||||
|
||||
TEMPLATABLE_VALUE(uint8_t, auto_mute_mode)
|
||||
|
||||
void play(const Ts &...x) override { this->aic3204_->set_auto_mute_mode(this->auto_mute_mode_.value(x...)); }
|
||||
|
||||
protected:
|
||||
AIC3204 *aic3204_;
|
||||
};
|
||||
|
||||
} // namespace esphome::aic3204
|
||||
@@ -85,11 +85,20 @@ async def wave_base_to_code(var: MockObj, config: ConfigType) -> None:
|
||||
|
||||
await ble_client.register_ble_node(var, config)
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_HUMIDITY, var.set_humidity)
|
||||
await sensors(CONF_TEMPERATURE, var.set_temperature)
|
||||
await sensors(CONF_PRESSURE, var.set_pressure)
|
||||
await sensors(CONF_TVOC, var.set_tvoc)
|
||||
await sensors(CONF_BATTERY_VOLTAGE, var.set_battery_voltage)
|
||||
if config_humidity := config.get(CONF_HUMIDITY):
|
||||
sens = await sensor.new_sensor(config_humidity)
|
||||
cg.add(var.set_humidity(sens))
|
||||
if config_temperature := config.get(CONF_TEMPERATURE):
|
||||
sens = await sensor.new_sensor(config_temperature)
|
||||
cg.add(var.set_temperature(sens))
|
||||
if config_pressure := config.get(CONF_PRESSURE):
|
||||
sens = await sensor.new_sensor(config_pressure)
|
||||
cg.add(var.set_pressure(sens))
|
||||
if config_tvoc := config.get(CONF_TVOC):
|
||||
sens = await sensor.new_sensor(config_tvoc)
|
||||
cg.add(var.set_tvoc(sens))
|
||||
if config_battery_voltage := config.get(CONF_BATTERY_VOLTAGE):
|
||||
sens = await sensor.new_sensor(config_battery_voltage)
|
||||
cg.add(var.set_battery_voltage(sens))
|
||||
if config_battery_update_interval := config.get(CONF_BATTERY_UPDATE_INTERVAL):
|
||||
cg.add(var.set_battery_update_interval(config_battery_update_interval))
|
||||
@@ -87,9 +87,16 @@ async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await airthings_wave_base.wave_base_to_code(var, config)
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_RADON, var.set_radon)
|
||||
await sensors(CONF_RADON_LONG_TERM, var.set_radon_long_term)
|
||||
await sensors(CONF_CO2, var.set_co2)
|
||||
await sensors(CONF_ILLUMINANCE, var.set_illuminance)
|
||||
if config_radon := config.get(CONF_RADON):
|
||||
sens = await sensor.new_sensor(config_radon)
|
||||
cg.add(var.set_radon(sens))
|
||||
if config_radon_long_term := config.get(CONF_RADON_LONG_TERM):
|
||||
sens = await sensor.new_sensor(config_radon_long_term)
|
||||
cg.add(var.set_radon_long_term(sens))
|
||||
if config_co2 := config.get(CONF_CO2):
|
||||
sens = await sensor.new_sensor(config_co2)
|
||||
cg.add(var.set_co2(sens))
|
||||
if config_illuminance := config.get(CONF_ILLUMINANCE):
|
||||
sens = await sensor.new_sensor(config_illuminance)
|
||||
cg.add(var.set_illuminance(sens))
|
||||
cg.add(var.set_device_type(config[CONF_DEVICE_TYPE]))
|
||||
@@ -41,6 +41,14 @@ StateAnyForwarder = alarm_control_panel_ns.class_("StateAnyForwarder")
|
||||
StateEnterForwarder = alarm_control_panel_ns.class_("StateEnterForwarder")
|
||||
AlarmControlPanelState = alarm_control_panel_ns.enum("AlarmControlPanelState")
|
||||
|
||||
PendingAction = alarm_control_panel_ns.class_("PendingAction", automation.Action)
|
||||
TriggeredAction = alarm_control_panel_ns.class_("TriggeredAction", automation.Action)
|
||||
ChimeAction = alarm_control_panel_ns.class_("ChimeAction", automation.Action)
|
||||
ReadyAction = alarm_control_panel_ns.class_("ReadyAction", automation.Action)
|
||||
|
||||
AlarmControlPanelCondition = alarm_control_panel_ns.class_(
|
||||
"AlarmControlPanelCondition", automation.Condition
|
||||
)
|
||||
|
||||
_ALARM_CONTROL_PANEL_SCHEMA = (
|
||||
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
|
||||
@@ -200,22 +208,65 @@ for _name, _arm in (
|
||||
)
|
||||
|
||||
|
||||
for _name, _call in (
|
||||
("alarm_control_panel.pending", "pending()"),
|
||||
("alarm_control_panel.triggered", "triggered()"),
|
||||
@automation.register_action(
|
||||
"alarm_control_panel.pending",
|
||||
PendingAction,
|
||||
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def alarm_action_pending_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"alarm_control_panel.triggered",
|
||||
TriggeredAction,
|
||||
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def alarm_action_trigger_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"alarm_control_panel.chime",
|
||||
ChimeAction,
|
||||
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def alarm_action_chime_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"alarm_control_panel.ready",
|
||||
ReadyAction,
|
||||
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
@automation.register_condition(
|
||||
"alarm_control_panel.ready",
|
||||
AlarmControlPanelCondition,
|
||||
ALARM_CONTROL_PANEL_CONDITION_SCHEMA,
|
||||
)
|
||||
async def alarm_action_ready_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
|
||||
@automation.register_condition(
|
||||
"alarm_control_panel.is_armed",
|
||||
AlarmControlPanelCondition,
|
||||
ALARM_CONTROL_PANEL_CONDITION_SCHEMA,
|
||||
)
|
||||
async def alarm_control_panel_is_armed_to_code(
|
||||
config, condition_id, template_arg, args
|
||||
):
|
||||
automation.register_apply_action(
|
||||
_name,
|
||||
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
|
||||
automation.ApplyCall(_call),
|
||||
call="make_call",
|
||||
)
|
||||
|
||||
|
||||
for _name in ("alarm_control_panel.ready", "alarm_control_panel.is_armed"):
|
||||
automation.register_apply_condition(
|
||||
_name, ALARM_CONTROL_PANEL_CONDITION_SCHEMA, "is_armed_pending_or_triggered()"
|
||||
)
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(condition_id, template_arg, paren)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.CORE)
|
||||
|
||||
@@ -130,12 +130,6 @@ class AlarmControlPanel : public EntityBase {
|
||||
|
||||
// is the state one of the armed states
|
||||
bool is_state_armed(AlarmControlPanelState state);
|
||||
/// Armed, pending (entry delay) or triggered; not ARMING (exit delay). Backs the is_armed and ready
|
||||
/// conditions.
|
||||
bool is_armed_pending_or_triggered() {
|
||||
auto state = this->get_state();
|
||||
return this->is_state_armed(state) || state == ACP_STATE_PENDING || state == ACP_STATE_TRIGGERED;
|
||||
}
|
||||
|
||||
protected:
|
||||
friend AlarmControlPanelCall;
|
||||
|
||||
@@ -27,4 +27,36 @@ static_assert(std::is_trivially_copyable_v<StateAnyForwarder>);
|
||||
static_assert(sizeof(StateEnterForwarder<ACP_STATE_TRIGGERED>) <= sizeof(void *));
|
||||
static_assert(std::is_trivially_copyable_v<StateEnterForwarder<ACP_STATE_TRIGGERED>>);
|
||||
|
||||
template<typename... Ts> class PendingAction final : public Action<Ts...> {
|
||||
public:
|
||||
explicit PendingAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {}
|
||||
|
||||
void play(const Ts &...x) override { this->alarm_control_panel_->make_call().pending().perform(); }
|
||||
|
||||
protected:
|
||||
AlarmControlPanel *alarm_control_panel_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class TriggeredAction final : public Action<Ts...> {
|
||||
public:
|
||||
explicit TriggeredAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {}
|
||||
|
||||
void play(const Ts &...x) override { this->alarm_control_panel_->make_call().triggered().perform(); }
|
||||
|
||||
protected:
|
||||
AlarmControlPanel *alarm_control_panel_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class AlarmControlPanelCondition final : public Condition<Ts...> {
|
||||
public:
|
||||
AlarmControlPanelCondition(AlarmControlPanel *parent) : parent_(parent) {}
|
||||
bool check(const Ts &...x) override {
|
||||
return this->parent_->is_state_armed(this->parent_->get_state()) ||
|
||||
this->parent_->get_state() == ACP_STATE_PENDING || this->parent_->get_state() == ACP_STATE_TRIGGERED;
|
||||
}
|
||||
|
||||
protected:
|
||||
AlarmControlPanel *parent_;
|
||||
};
|
||||
|
||||
} // namespace esphome::alarm_control_panel
|
||||
@@ -1,6 +1,8 @@
|
||||
#pragma once
|
||||
#include "esphome/components/image/image.h"
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
|
||||
namespace esphome::animation {
|
||||
|
||||
class Animation final : public image::Image {
|
||||
@@ -33,4 +35,32 @@ class Animation final : public image::Image {
|
||||
int loop_current_iteration_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class AnimationNextFrameAction final : public Action<Ts...> {
|
||||
public:
|
||||
AnimationNextFrameAction(Animation *parent) : parent_(parent) {}
|
||||
void play(const Ts &...x) override { this->parent_->next_frame(); }
|
||||
|
||||
protected:
|
||||
Animation *parent_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class AnimationPrevFrameAction final : public Action<Ts...> {
|
||||
public:
|
||||
AnimationPrevFrameAction(Animation *parent) : parent_(parent) {}
|
||||
void play(const Ts &...x) override { this->parent_->prev_frame(); }
|
||||
|
||||
protected:
|
||||
Animation *parent_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class AnimationSetFrameAction final : public Action<Ts...> {
|
||||
public:
|
||||
AnimationSetFrameAction(Animation *parent) : parent_(parent) {}
|
||||
TEMPLATABLE_VALUE(uint16_t, frame)
|
||||
void play(const Ts &...x) override { this->parent_->set_frame(this->frame_.value(x...)); }
|
||||
|
||||
protected:
|
||||
Animation *parent_;
|
||||
};
|
||||
|
||||
} // namespace esphome::animation
|
||||
@@ -6,6 +6,8 @@ from esphome.components.file.image import image_schema, write_image
|
||||
from esphome.components.image import Image_, validate_settings
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_REPEAT
|
||||
from esphome.core import ID
|
||||
from esphome.cpp_generator import MockObj, TemplateArgsType
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@syndlex"]
|
||||
@@ -24,6 +26,17 @@ animation_ns = cg.esphome_ns.namespace("animation")
|
||||
|
||||
Animation_ = animation_ns.class_("Animation", Image_)
|
||||
|
||||
# Actions
|
||||
NextFrameAction = animation_ns.class_(
|
||||
"AnimationNextFrameAction", automation.Action, cg.Parented.template(Animation_)
|
||||
)
|
||||
PrevFrameAction = animation_ns.class_(
|
||||
"AnimationPrevFrameAction", automation.Action, cg.Parented.template(Animation_)
|
||||
)
|
||||
SetFrameAction = animation_ns.class_(
|
||||
"AnimationSetFrameAction", automation.Action, cg.Parented.template(Animation_)
|
||||
)
|
||||
|
||||
ANIMATION_SCHEMA = image_schema(Animation_).extend(
|
||||
{
|
||||
cv.Optional(CONF_LOOP): cv.All(
|
||||
@@ -59,17 +72,28 @@ SET_FRAME_SCHEMA = cv.Schema(
|
||||
)
|
||||
|
||||
|
||||
automation.register_apply_action(
|
||||
"animation.next_frame", NEXT_FRAME_SCHEMA, automation.ApplyCall("next_frame()")
|
||||
@automation.register_action(
|
||||
"animation.next_frame", NextFrameAction, NEXT_FRAME_SCHEMA, synchronous=True
|
||||
)
|
||||
automation.register_apply_action(
|
||||
"animation.prev_frame", PREV_FRAME_SCHEMA, automation.ApplyCall("prev_frame()")
|
||||
@automation.register_action(
|
||||
"animation.prev_frame", PrevFrameAction, PREV_FRAME_SCHEMA, synchronous=True
|
||||
)
|
||||
automation.register_apply_action(
|
||||
"animation.set_frame",
|
||||
SET_FRAME_SCHEMA,
|
||||
automation.ApplyField(CONF_FRAME, "set_frame", cg.uint16),
|
||||
@automation.register_action(
|
||||
"animation.set_frame", SetFrameAction, SET_FRAME_SCHEMA, synchronous=True
|
||||
)
|
||||
async def animation_action_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
if (frame := config.get(CONF_FRAME)) is not None:
|
||||
template_ = await cg.templatable(frame, args, cg.uint16)
|
||||
cg.add(var.set_frame(template_))
|
||||
return var
|
||||
|
||||
|
||||
async def setup_animation(config: ConfigType) -> None:
|
||||
|
||||
@@ -803,9 +803,6 @@ message SwitchStateResponse {
|
||||
fixed32 key = 1 [(force) = true];
|
||||
bool state = 2;
|
||||
uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
|
||||
// If the switch does not have a valid state yet.
|
||||
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
|
||||
bool missing_state = 4;
|
||||
}
|
||||
message SwitchCommandRequest {
|
||||
option (id) = 33;
|
||||
@@ -1247,9 +1244,6 @@ message ClimateStateResponse {
|
||||
float current_humidity = 14;
|
||||
float target_humidity = 15;
|
||||
uint32 device_id = 16 [(field_ifdef) = "USE_DEVICES"];
|
||||
// If the climate device does not have a valid state yet.
|
||||
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
|
||||
bool missing_state = 17;
|
||||
}
|
||||
message ClimateCommandRequest {
|
||||
option (id) = 48;
|
||||
@@ -1336,9 +1330,6 @@ message WaterHeaterStateResponse {
|
||||
uint32 state = 6;
|
||||
float target_temperature_low = 7;
|
||||
float target_temperature_high = 8;
|
||||
// If the water heater does not have a valid state yet.
|
||||
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
|
||||
bool missing_state = 9;
|
||||
}
|
||||
|
||||
// Bitmask for WaterHeaterCommandRequest.has_fields
|
||||
|
||||
@@ -709,7 +709,6 @@ uint16_t APIConnection::try_send_switch_state(EntityBase *entity, APIConnection
|
||||
auto *a_switch = static_cast<switch_::Switch *>(entity);
|
||||
SwitchStateResponse resp;
|
||||
resp.state = a_switch->state;
|
||||
resp.missing_state = !a_switch->has_state();
|
||||
return fill_and_encode_entity_state(a_switch, resp, conn, remaining_size);
|
||||
}
|
||||
|
||||
@@ -755,7 +754,6 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection
|
||||
auto traits = climate->get_traits();
|
||||
resp.mode = static_cast<enums::ClimateMode>(climate->mode);
|
||||
resp.action = static_cast<enums::ClimateAction>(climate->action);
|
||||
resp.missing_state = !climate->has_state();
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE))
|
||||
resp.current_temperature = climate->current_temperature;
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
|
||||
@@ -1449,7 +1447,6 @@ uint16_t APIConnection::try_send_water_heater_state(EntityBase *entity, APIConne
|
||||
auto *wh = static_cast<water_heater::WaterHeater *>(entity);
|
||||
WaterHeaterStateResponse resp;
|
||||
resp.mode = static_cast<enums::WaterHeaterMode>(wh->get_mode());
|
||||
resp.missing_state = !wh->has_state();
|
||||
resp.current_temperature = wh->get_current_temperature();
|
||||
resp.target_temperature = wh->get_target_temperature();
|
||||
resp.target_temperature_low = wh->get_target_temperature_low();
|
||||
|
||||
@@ -938,7 +938,6 @@ uint8_t *SwitchStateResponse::encode_msg(const void *self, ProtoWriteBuffer &buf
|
||||
#ifdef USE_DEVICES
|
||||
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, msg.device_id);
|
||||
#endif
|
||||
pos = ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 4, msg.missing_state);
|
||||
return pos;
|
||||
}
|
||||
uint32_t SwitchStateResponse::calc_size_msg(const void *self) {
|
||||
@@ -949,7 +948,6 @@ uint32_t SwitchStateResponse::calc_size_msg(const void *self) {
|
||||
#ifdef USE_DEVICES
|
||||
size += ProtoSize::calc_uint32(1, msg.device_id);
|
||||
#endif
|
||||
size += ProtoSize::calc_bool(1, msg.missing_state);
|
||||
return size;
|
||||
}
|
||||
void SwitchCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) {
|
||||
@@ -1632,7 +1630,6 @@ uint8_t *ClimateStateResponse::encode_msg(const void *self, ProtoWriteBuffer &bu
|
||||
#ifdef USE_DEVICES
|
||||
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 16, msg.device_id);
|
||||
#endif
|
||||
pos = ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 17, msg.missing_state);
|
||||
return pos;
|
||||
}
|
||||
uint32_t ClimateStateResponse::calc_size_msg(const void *self) {
|
||||
@@ -1655,7 +1652,6 @@ uint32_t ClimateStateResponse::calc_size_msg(const void *self) {
|
||||
#ifdef USE_DEVICES
|
||||
size += ProtoSize::calc_uint32(2, msg.device_id);
|
||||
#endif
|
||||
size += ProtoSize::calc_bool(2, msg.missing_state);
|
||||
return size;
|
||||
}
|
||||
void ClimateCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) {
|
||||
@@ -1810,7 +1806,6 @@ uint8_t *WaterHeaterStateResponse::encode_msg(const void *self, ProtoWriteBuffer
|
||||
if (uint32_t raw = float_to_raw(msg.target_temperature_high); raw != 0) [[likely]] {
|
||||
pos = ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 69, raw);
|
||||
}
|
||||
pos = ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 9, msg.missing_state);
|
||||
return pos;
|
||||
}
|
||||
uint32_t WaterHeaterStateResponse::calc_size_msg(const void *self) {
|
||||
@@ -1826,7 +1821,6 @@ uint32_t WaterHeaterStateResponse::calc_size_msg(const void *self) {
|
||||
size += ProtoSize::calc_uint32(1, msg.state);
|
||||
size += ProtoSize::calc_float(1, msg.target_temperature_low);
|
||||
size += ProtoSize::calc_float(1, msg.target_temperature_high);
|
||||
size += ProtoSize::calc_bool(1, msg.missing_state);
|
||||
return size;
|
||||
}
|
||||
void WaterHeaterCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t *data,
|
||||
|
||||
@@ -1109,12 +1109,11 @@ class ListEntitiesSwitchResponse final : public InfoResponseProtoMessage {
|
||||
class SwitchStateResponse final : public StateResponseProtoMessage {
|
||||
public:
|
||||
static constexpr uint16_t MESSAGE_TYPE = 26;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 13;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 11;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const LogString *message_name() const override { return LOG_STR("switch_state_response"); }
|
||||
#endif
|
||||
bool state{false};
|
||||
bool missing_state{false};
|
||||
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
|
||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
return encode_msg(this, buffer PROTO_ENCODE_DEBUG_ARG);
|
||||
@@ -1691,7 +1690,7 @@ class ListEntitiesClimateResponse final : public InfoResponseProtoMessage {
|
||||
class ClimateStateResponse final : public StateResponseProtoMessage {
|
||||
public:
|
||||
static constexpr uint16_t MESSAGE_TYPE = 47;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 71;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 68;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const LogString *message_name() const override { return LOG_STR("climate_state_response"); }
|
||||
#endif
|
||||
@@ -1708,7 +1707,6 @@ class ClimateStateResponse final : public StateResponseProtoMessage {
|
||||
StringRef custom_preset{nullptr, 0}; // null until set, encode only
|
||||
float current_humidity{0.0f};
|
||||
float target_humidity{0.0f};
|
||||
bool missing_state{false};
|
||||
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
|
||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
return encode_msg(this, buffer PROTO_ENCODE_DEBUG_ARG);
|
||||
@@ -1788,7 +1786,7 @@ class ListEntitiesWaterHeaterResponse final : public InfoResponseProtoMessage {
|
||||
class WaterHeaterStateResponse final : public StateResponseProtoMessage {
|
||||
public:
|
||||
static constexpr uint16_t MESSAGE_TYPE = 133;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 37;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 35;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const LogString *message_name() const override { return LOG_STR("water_heater_state_response"); }
|
||||
#endif
|
||||
@@ -1798,7 +1796,6 @@ class WaterHeaterStateResponse final : public StateResponseProtoMessage {
|
||||
uint32_t state{0};
|
||||
float target_temperature_low{0.0f};
|
||||
float target_temperature_high{0.0f};
|
||||
bool missing_state{false};
|
||||
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM);
|
||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
return encode_msg(this, buffer PROTO_ENCODE_DEBUG_ARG);
|
||||
|
||||
@@ -1342,7 +1342,6 @@ const char *SwitchStateResponse::dump_to(DumpBuffer &out) const {
|
||||
#ifdef USE_DEVICES
|
||||
dump_field(out, ESPHOME_PSTR("device_id"), this->device_id);
|
||||
#endif
|
||||
dump_field(out, ESPHOME_PSTR("missing_state"), this->missing_state);
|
||||
return out.c_str();
|
||||
}
|
||||
const char *SwitchCommandRequest::dump_to(DumpBuffer &out) const {
|
||||
@@ -1685,7 +1684,6 @@ const char *ClimateStateResponse::dump_to(DumpBuffer &out) const {
|
||||
#ifdef USE_DEVICES
|
||||
dump_field(out, ESPHOME_PSTR("device_id"), this->device_id);
|
||||
#endif
|
||||
dump_field(out, ESPHOME_PSTR("missing_state"), this->missing_state);
|
||||
return out.c_str();
|
||||
}
|
||||
const char *ClimateCommandRequest::dump_to(DumpBuffer &out) const {
|
||||
@@ -1753,7 +1751,6 @@ const char *WaterHeaterStateResponse::dump_to(DumpBuffer &out) const {
|
||||
dump_field(out, ESPHOME_PSTR("state"), this->state);
|
||||
dump_field(out, ESPHOME_PSTR("target_temperature_low"), this->target_temperature_low);
|
||||
dump_field(out, ESPHOME_PSTR("target_temperature_high"), this->target_temperature_high);
|
||||
dump_field(out, ESPHOME_PSTR("missing_state"), this->missing_state);
|
||||
return out.c_str();
|
||||
}
|
||||
const char *WaterHeaterCommandRequest::dump_to(DumpBuffer &out) const {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "esphome/components/network/util.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cerrno>
|
||||
#include <sys/select.h>
|
||||
|
||||
namespace esphome::async_tcp {
|
||||
|
||||
@@ -41,15 +42,7 @@ bool AsyncClient::connect(const char *host, uint16_t port) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (socket_->setblocking(false) != 0) {
|
||||
// Capture before the log and close() clobber errno
|
||||
const int saved_errno = errno;
|
||||
ESP_LOGE(TAG, "Failed to set nonblocking: errno %d", saved_errno);
|
||||
close();
|
||||
if (error_cb_)
|
||||
error_cb_(error_arg_, this, saved_errno);
|
||||
return false;
|
||||
}
|
||||
socket_->setblocking(false);
|
||||
|
||||
int err = socket_->connect((struct sockaddr *) &addr, addrlen);
|
||||
if (err == 0) {
|
||||
@@ -104,22 +97,45 @@ void AsyncClient::loop() {
|
||||
return;
|
||||
|
||||
if (connecting_) {
|
||||
int err = 0;
|
||||
switch (socket::poll_connect(*socket_, err)) {
|
||||
case socket::ConnectPollResult::CONNECT_POLL_RESULT_PENDING:
|
||||
break;
|
||||
case socket::ConnectPollResult::CONNECT_POLL_RESULT_CONNECTED:
|
||||
// For connecting, we need to check writability, not readability
|
||||
// The Application's select() only monitors read FDs, so we do our own check here
|
||||
// For ESP platforms lwip_select() might be faster, but this code isn't used
|
||||
// on those platforms anyway. If it was, we'd fix the Application select()
|
||||
// to report writability instead of doing it this way.
|
||||
int fd = socket_->get_fd();
|
||||
if (fd < 0) {
|
||||
ESP_LOGW(TAG, "Invalid socket fd");
|
||||
close();
|
||||
return;
|
||||
}
|
||||
|
||||
fd_set writefds;
|
||||
FD_ZERO(&writefds);
|
||||
FD_SET(fd, &writefds);
|
||||
|
||||
struct timeval tv = {0, 0};
|
||||
int ret = select(fd + 1, nullptr, &writefds, nullptr, &tv);
|
||||
|
||||
if (ret > 0 && FD_ISSET(fd, &writefds)) {
|
||||
int error = 0;
|
||||
socklen_t len = sizeof(error);
|
||||
if (socket_->getsockopt(SOL_SOCKET, SO_ERROR, &error, &len) == 0 && error == 0) {
|
||||
connecting_ = false;
|
||||
connected_ = true;
|
||||
if (connect_cb_)
|
||||
connect_cb_(connect_arg_, this);
|
||||
break;
|
||||
case socket::ConnectPollResult::CONNECT_POLL_RESULT_ERROR:
|
||||
ESP_LOGW(TAG, "Connection failed: %d", err);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Connection failed: %d", error);
|
||||
close();
|
||||
if (error_cb_)
|
||||
error_cb_(error_arg_, this, err);
|
||||
break;
|
||||
error_cb_(error_arg_, this, error);
|
||||
}
|
||||
} else if (ret < 0) {
|
||||
const int err = errno;
|
||||
ESP_LOGE(TAG, "Select error: %d", err);
|
||||
close();
|
||||
if (error_cb_)
|
||||
error_cb_(error_arg_, this, err);
|
||||
}
|
||||
} else if (connected_) {
|
||||
// For connected sockets, use the Application's select() results
|
||||
|
||||
@@ -85,9 +85,18 @@ async def to_code(config: ConfigType) -> None:
|
||||
|
||||
cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_TEMPERATURE, var.set_temperature)
|
||||
await sensors(CONF_HUMIDITY, var.set_humidity)
|
||||
await sensors(CONF_BATTERY_LEVEL, var.set_battery_level)
|
||||
await sensors(CONF_BATTERY_VOLTAGE, var.set_battery_voltage)
|
||||
await sensors(CONF_SIGNAL_STRENGTH, var.set_signal_strength)
|
||||
if temperature_config := config.get(CONF_TEMPERATURE):
|
||||
sens = await sensor.new_sensor(temperature_config)
|
||||
cg.add(var.set_temperature(sens))
|
||||
if humidity_config := config.get(CONF_HUMIDITY):
|
||||
sens = await sensor.new_sensor(humidity_config)
|
||||
cg.add(var.set_humidity(sens))
|
||||
if battery_level_config := config.get(CONF_BATTERY_LEVEL):
|
||||
sens = await sensor.new_sensor(battery_level_config)
|
||||
cg.add(var.set_battery_level(sens))
|
||||
if battery_voltage_config := config.get(CONF_BATTERY_VOLTAGE):
|
||||
sens = await sensor.new_sensor(battery_voltage_config)
|
||||
cg.add(var.set_battery_voltage(sens))
|
||||
if signal_strength_config := config.get(CONF_SIGNAL_STRENGTH):
|
||||
sens = await sensor.new_sensor(signal_strength_config)
|
||||
cg.add(var.set_signal_strength(sens))
|
||||
@@ -129,15 +129,30 @@ async def to_code(config: ConfigType) -> None:
|
||||
await cg.register_component(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_VOLTAGE, var.set_voltage_sensor)
|
||||
await sensors(CONF_CURRENT, var.set_current_sensor)
|
||||
await sensors(CONF_POWER, var.set_power_sensor)
|
||||
await sensors(CONF_REACTIVE_POWER, var.set_reactive_power_sensor)
|
||||
await sensors(CONF_POWER_FACTOR, var.set_power_factor_sensor)
|
||||
await sensors(CONF_FORWARD_ACTIVE_ENERGY, var.set_forward_active_energy_sensor)
|
||||
await sensors(CONF_REVERSE_ACTIVE_ENERGY, var.set_reverse_active_energy_sensor)
|
||||
await sensors(CONF_FREQUENCY, var.set_freq_sensor)
|
||||
if voltage_config := config.get(CONF_VOLTAGE):
|
||||
sens = await sensor.new_sensor(voltage_config)
|
||||
cg.add(var.set_voltage_sensor(sens))
|
||||
if current_config := config.get(CONF_CURRENT):
|
||||
sens = await sensor.new_sensor(current_config)
|
||||
cg.add(var.set_current_sensor(sens))
|
||||
if power_config := config.get(CONF_POWER):
|
||||
sens = await sensor.new_sensor(power_config)
|
||||
cg.add(var.set_power_sensor(sens))
|
||||
if reactive_power_config := config.get(CONF_REACTIVE_POWER):
|
||||
sens = await sensor.new_sensor(reactive_power_config)
|
||||
cg.add(var.set_reactive_power_sensor(sens))
|
||||
if power_factor_config := config.get(CONF_POWER_FACTOR):
|
||||
sens = await sensor.new_sensor(power_factor_config)
|
||||
cg.add(var.set_power_factor_sensor(sens))
|
||||
if forward_active_energy_config := config.get(CONF_FORWARD_ACTIVE_ENERGY):
|
||||
sens = await sensor.new_sensor(forward_active_energy_config)
|
||||
cg.add(var.set_forward_active_energy_sensor(sens))
|
||||
if reverse_active_energy_config := config.get(CONF_REVERSE_ACTIVE_ENERGY):
|
||||
sens = await sensor.new_sensor(reverse_active_energy_config)
|
||||
cg.add(var.set_reverse_active_energy_sensor(sens))
|
||||
if frequency_config := config.get(CONF_FREQUENCY):
|
||||
sens = await sensor.new_sensor(frequency_config)
|
||||
cg.add(var.set_freq_sensor(sens))
|
||||
cg.add(var.set_line_freq(config[CONF_LINE_FREQUENCY]))
|
||||
cg.add(var.set_meter_constant(config[CONF_METER_CONSTANT]))
|
||||
cg.add(var.set_pl_const(config[CONF_PL_CONST]))
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_MIC_GAIN
|
||||
from esphome.core import CoroPriority, coroutine_with_priority
|
||||
from esphome.const import CONF_ID, CONF_MIC_GAIN
|
||||
from esphome.core import ID, CoroPriority, coroutine_with_priority
|
||||
from esphome.cpp_generator import MockObj, TemplateArgsType
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@kbx81"]
|
||||
@@ -11,6 +12,8 @@ IS_PLATFORM_COMPONENT = True
|
||||
audio_adc_ns = cg.esphome_ns.namespace("audio_adc")
|
||||
AudioAdc = audio_adc_ns.class_("AudioAdc")
|
||||
|
||||
SetMicGainAction = audio_adc_ns.class_("SetMicGainAction", automation.Action)
|
||||
|
||||
|
||||
SET_MIC_GAIN_ACTION_SCHEMA = cv.maybe_simple_value(
|
||||
{
|
||||
@@ -21,11 +24,25 @@ SET_MIC_GAIN_ACTION_SCHEMA = cv.maybe_simple_value(
|
||||
)
|
||||
|
||||
|
||||
automation.register_apply_action(
|
||||
@automation.register_action(
|
||||
"audio_adc.set_mic_gain",
|
||||
SetMicGainAction,
|
||||
SET_MIC_GAIN_ACTION_SCHEMA,
|
||||
automation.ApplyField(CONF_MIC_GAIN, "set_mic_gain", cg.float_),
|
||||
synchronous=True,
|
||||
)
|
||||
async def audio_adc_set_mic_gain_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
template_ = await cg.templatable(config.get(CONF_MIC_GAIN), args, cg.float_)
|
||||
cg.add(var.set_mic_gain(template_))
|
||||
|
||||
return var
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.CORE)
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "audio_adc.h"
|
||||
|
||||
namespace esphome::audio_adc {
|
||||
|
||||
template<typename... Ts> class SetMicGainAction final : public Action<Ts...> {
|
||||
public:
|
||||
explicit SetMicGainAction(AudioAdc *audio_adc) : audio_adc_(audio_adc) {}
|
||||
|
||||
TEMPLATABLE_VALUE(float, mic_gain)
|
||||
|
||||
void play(const Ts &...x) override { this->audio_adc_->set_mic_gain(this->mic_gain_.value(x...)); }
|
||||
|
||||
protected:
|
||||
AudioAdc *audio_adc_;
|
||||
};
|
||||
|
||||
} // namespace esphome::audio_adc
|
||||
@@ -61,16 +61,14 @@ from esphome.const import (
|
||||
DEVICE_CLASS_VIBRATION,
|
||||
DEVICE_CLASS_WINDOW,
|
||||
)
|
||||
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import (
|
||||
SubEntities,
|
||||
entity_duplicate_validator,
|
||||
queue_entity_register,
|
||||
setup_device_class,
|
||||
setup_entity,
|
||||
)
|
||||
from esphome.cpp_generator import MockObj, MockObjClass
|
||||
from esphome.types import ConfigType
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
from esphome.util import Registry
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
@@ -136,6 +134,9 @@ MultiClickTriggerBase = binary_sensor_ns.class_(
|
||||
MultiClickTrigger = binary_sensor_ns.class_("MultiClickTrigger", MultiClickTriggerBase)
|
||||
MultiClickTriggerEvent = binary_sensor_ns.struct("MultiClickTriggerEvent")
|
||||
|
||||
BinarySensorInvalidateAction = binary_sensor_ns.class_(
|
||||
"BinarySensorInvalidateAction", automation.Action
|
||||
)
|
||||
|
||||
# Filters
|
||||
Filter = binary_sensor_ns.class_("Filter")
|
||||
@@ -634,13 +635,6 @@ async def new_binary_sensor(config, *args):
|
||||
return var
|
||||
|
||||
|
||||
def sub_binary_sensors(
|
||||
config: ConfigType, *, parent: MockObj | ID | None = None
|
||||
) -> SubEntities:
|
||||
"""Return a SubEntities bound to new_binary_sensor."""
|
||||
return SubEntities(new_binary_sensor, config, parent)
|
||||
|
||||
|
||||
BINARY_SENSOR_CONDITION_SCHEMA = maybe_simple_id(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(BinarySensor),
|
||||
@@ -661,16 +655,20 @@ async def to_code(config):
|
||||
cg.add_global(binary_sensor_ns.using)
|
||||
|
||||
|
||||
automation.register_apply_action(
|
||||
@automation.register_action(
|
||||
"binary_sensor.invalidate_state",
|
||||
BinarySensorInvalidateAction,
|
||||
cv.maybe_simple_value(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(BinarySensor),
|
||||
},
|
||||
key=CONF_ID,
|
||||
),
|
||||
automation.ApplyCall("invalidate_state()"),
|
||||
synchronous=True,
|
||||
)
|
||||
async def binary_sensor_invalidate_state_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
|
||||
# automation.cpp only implements the click/double_click/multi_click triggers
|
||||
|
||||
@@ -120,4 +120,14 @@ template<size_t N> class MultiClickTrigger final : public MultiClickTriggerBase
|
||||
std::array<MultiClickTriggerEvent, N> timing_storage_{};
|
||||
};
|
||||
|
||||
template<typename... Ts> class BinarySensorInvalidateAction final : public Action<Ts...> {
|
||||
public:
|
||||
explicit BinarySensorInvalidateAction(BinarySensor *sensor) : sensor_(sensor) {}
|
||||
|
||||
void play(const Ts &...x) override { this->sensor_->invalidate_state(); }
|
||||
|
||||
protected:
|
||||
BinarySensor *sensor_;
|
||||
};
|
||||
|
||||
} // namespace esphome::binary_sensor
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/datatypes.h"
|
||||
|
||||
@@ -46,6 +47,8 @@ struct sbe24_t { // NOLINT(readability-identifier-naming,altera-struct-pack-ali
|
||||
int8_t h{0};
|
||||
} __attribute__((packed));
|
||||
|
||||
template<typename... Ts> class ResetEnergyAction;
|
||||
|
||||
class BL0906;
|
||||
|
||||
using ActionCallbackFuncPtr = void (BL0906::*)();
|
||||
@@ -82,10 +85,9 @@ class BL0906 final : public PollingComponent, public uart::UARTDevice {
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
|
||||
/// Queue an energy counter reset for the next poll
|
||||
void reset_energy() { this->enqueue_action_(&BL0906::reset_energy_); }
|
||||
|
||||
protected:
|
||||
template<typename... Ts> friend class ResetEnergyAction;
|
||||
|
||||
void reset_energy_();
|
||||
|
||||
void read_data_(uint8_t address, float reference, sensor::Sensor *sensor);
|
||||
@@ -101,4 +103,9 @@ class BL0906 final : public PollingComponent, public uart::UARTDevice {
|
||||
std::vector<ActionCallbackFuncPtr> action_queue_{};
|
||||
};
|
||||
|
||||
template<typename... Ts> class ResetEnergyAction final : public Action<Ts...>, public Parented<BL0906> {
|
||||
public:
|
||||
void play(const Ts &...x) override { this->parent_->enqueue_action_(&BL0906::reset_energy_); }
|
||||
};
|
||||
|
||||
} // namespace esphome::bl0906
|
||||
@@ -32,6 +32,8 @@ from esphome.const import (
|
||||
UNIT_VOLT,
|
||||
UNIT_WATT,
|
||||
)
|
||||
from esphome.core import ID
|
||||
from esphome.cpp_generator import MockObj, TemplateArgsType
|
||||
from esphome.types import ConfigType
|
||||
|
||||
# Import ICONS not included in esphome's const.py, from the local components const.py
|
||||
@@ -43,6 +45,7 @@ CONF_TOTAL_ENERGY = "total_energy"
|
||||
|
||||
bl0906_ns = cg.esphome_ns.namespace("bl0906")
|
||||
BL0906 = bl0906_ns.class_("BL0906", cg.PollingComponent, uart.UARTDevice)
|
||||
ResetEnergyAction = bl0906_ns.class_("ResetEnergyAction", automation.Action)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
@@ -135,25 +138,40 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
)
|
||||
|
||||
|
||||
automation.register_apply_action(
|
||||
@automation.register_action(
|
||||
"bl0906.reset_energy",
|
||||
ResetEnergyAction,
|
||||
maybe_simple_id(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(BL0906),
|
||||
}
|
||||
),
|
||||
automation.ApplyCall("reset_energy()"),
|
||||
synchronous=True,
|
||||
)
|
||||
async def reset_energy_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
return var
|
||||
|
||||
|
||||
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_FREQUENCY, var.set_frequency_sensor)
|
||||
await sensors(CONF_TEMPERATURE, var.set_temperature_sensor)
|
||||
await sensors(CONF_VOLTAGE, var.set_voltage_sensor)
|
||||
if frequency_config := config.get(CONF_FREQUENCY):
|
||||
sens = await sensor.new_sensor(frequency_config)
|
||||
cg.add(var.set_frequency_sensor(sens))
|
||||
if temperature_config := config.get(CONF_TEMPERATURE):
|
||||
sens = await sensor.new_sensor(temperature_config)
|
||||
cg.add(var.set_temperature_sensor(sens))
|
||||
if voltage_config := config.get(CONF_VOLTAGE):
|
||||
sens = await sensor.new_sensor(voltage_config)
|
||||
cg.add(var.set_voltage_sensor(sens))
|
||||
|
||||
for i in range(6):
|
||||
if channel_config := config.get(f"{CONF_CHANNEL}_{i + 1}"):
|
||||
@@ -167,5 +185,10 @@ async def to_code(config: ConfigType) -> None:
|
||||
sens = await sensor.new_sensor(energy_config)
|
||||
cg.add(getattr(var, f"set_energy_{i + 1}_sensor")(sens))
|
||||
|
||||
await sensors(CONF_TOTAL_POWER, var.set_total_power_sensor)
|
||||
await sensors(CONF_TOTAL_ENERGY, var.set_total_energy_sensor)
|
||||
if total_power_config := config.get(CONF_TOTAL_POWER):
|
||||
sens = await sensor.new_sensor(total_power_config)
|
||||
cg.add(var.set_total_power_sensor(sens))
|
||||
|
||||
if total_energy_config := config.get(CONF_TOTAL_ENERGY):
|
||||
sens = await sensor.new_sensor(total_energy_config)
|
||||
cg.add(var.set_total_energy_sensor(sens))
|
||||
@@ -94,12 +94,27 @@ async def to_code(config: ConfigType) -> None:
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_VOLTAGE, var.set_voltage_sensor)
|
||||
await sensors(CONF_CURRENT_1, var.set_current_sensor_1)
|
||||
await sensors(CONF_CURRENT_2, var.set_current_sensor_2)
|
||||
await sensors(CONF_ACTIVE_POWER_1, var.set_power_sensor_1)
|
||||
await sensors(CONF_ACTIVE_POWER_2, var.set_power_sensor_2)
|
||||
await sensors(CONF_ENERGY_1, var.set_energy_sensor_1)
|
||||
await sensors(CONF_ENERGY_2, var.set_energy_sensor_2)
|
||||
await sensors(CONF_ENERGY_TOTAL, var.set_energy_sensor_sum)
|
||||
if voltage_config := config.get(CONF_VOLTAGE):
|
||||
sens = await sensor.new_sensor(voltage_config)
|
||||
cg.add(var.set_voltage_sensor(sens))
|
||||
if current_1_config := config.get(CONF_CURRENT_1):
|
||||
sens = await sensor.new_sensor(current_1_config)
|
||||
cg.add(var.set_current_sensor_1(sens))
|
||||
if current_2_config := config.get(CONF_CURRENT_2):
|
||||
sens = await sensor.new_sensor(current_2_config)
|
||||
cg.add(var.set_current_sensor_2(sens))
|
||||
if active_power_1_config := config.get(CONF_ACTIVE_POWER_1):
|
||||
sens = await sensor.new_sensor(active_power_1_config)
|
||||
cg.add(var.set_power_sensor_1(sens))
|
||||
if active_power_2_config := config.get(CONF_ACTIVE_POWER_2):
|
||||
sens = await sensor.new_sensor(active_power_2_config)
|
||||
cg.add(var.set_power_sensor_2(sens))
|
||||
if energy_1_config := config.get(CONF_ENERGY_1):
|
||||
sens = await sensor.new_sensor(energy_1_config)
|
||||
cg.add(var.set_energy_sensor_1(sens))
|
||||
if energy_2_config := config.get(CONF_ENERGY_2):
|
||||
sens = await sensor.new_sensor(energy_2_config)
|
||||
cg.add(var.set_energy_sensor_2(sens))
|
||||
if energy_total_config := config.get(CONF_ENERGY_TOTAL):
|
||||
sens = await sensor.new_sensor(energy_total_config)
|
||||
cg.add(var.set_energy_sensor_sum(sens))
|
||||
@@ -231,13 +231,24 @@ async def to_code(config: ConfigType) -> None:
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_VOLTAGE, var.set_voltage_sensor)
|
||||
await sensors(CONF_CURRENT, var.set_current_sensor)
|
||||
await sensors(CONF_POWER, var.set_power_sensor)
|
||||
await sensors(CONF_ENERGY, var.set_energy_sensor)
|
||||
await sensors(CONF_INTERNAL_TEMPERATURE, var.set_internal_temperature_sensor)
|
||||
await sensors(CONF_EXTERNAL_TEMPERATURE, var.set_external_temperature_sensor)
|
||||
if voltage_config := config.get(CONF_VOLTAGE):
|
||||
sens = await sensor.new_sensor(voltage_config)
|
||||
cg.add(var.set_voltage_sensor(sens))
|
||||
if current_config := config.get(CONF_CURRENT):
|
||||
sens = await sensor.new_sensor(current_config)
|
||||
cg.add(var.set_current_sensor(sens))
|
||||
if power_config := config.get(CONF_POWER):
|
||||
sens = await sensor.new_sensor(power_config)
|
||||
cg.add(var.set_power_sensor(sens))
|
||||
if energy_config := config.get(CONF_ENERGY):
|
||||
sens = await sensor.new_sensor(energy_config)
|
||||
cg.add(var.set_energy_sensor(sens))
|
||||
if internal_temperature_config := config.get(CONF_INTERNAL_TEMPERATURE):
|
||||
sens = await sensor.new_sensor(internal_temperature_config)
|
||||
cg.add(var.set_internal_temperature_sensor(sens))
|
||||
if external_temperature_config := config.get(CONF_EXTERNAL_TEMPERATURE):
|
||||
sens = await sensor.new_sensor(external_temperature_config)
|
||||
cg.add(var.set_external_temperature_sensor(sens))
|
||||
|
||||
# enable legacy mode
|
||||
cg.add(var.set_legacy_mode(config.get(CONF_LEGACY_MODE)))
|
||||
|
||||
@@ -101,12 +101,21 @@ async def to_code(config: ConfigType) -> None:
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_VOLTAGE, var.set_voltage_sensor)
|
||||
await sensors(CONF_CURRENT, var.set_current_sensor)
|
||||
await sensors(CONF_POWER, var.set_power_sensor)
|
||||
await sensors(CONF_ENERGY, var.set_energy_sensor)
|
||||
await sensors(CONF_FREQUENCY, var.set_frequency_sensor)
|
||||
if voltage_config := config.get(CONF_VOLTAGE):
|
||||
sens = await sensor.new_sensor(voltage_config)
|
||||
cg.add(var.set_voltage_sensor(sens))
|
||||
if current_config := config.get(CONF_CURRENT):
|
||||
sens = await sensor.new_sensor(current_config)
|
||||
cg.add(var.set_current_sensor(sens))
|
||||
if power_config := config.get(CONF_POWER):
|
||||
sens = await sensor.new_sensor(power_config)
|
||||
cg.add(var.set_power_sensor(sens))
|
||||
if energy_config := config.get(CONF_ENERGY):
|
||||
sens = await sensor.new_sensor(energy_config)
|
||||
cg.add(var.set_energy_sensor(sens))
|
||||
if frequency_config := config.get(CONF_FREQUENCY):
|
||||
sens = await sensor.new_sensor(frequency_config)
|
||||
cg.add(var.set_frequency_sensor(sens))
|
||||
cg.add(var.set_line_freq(config[CONF_LINE_FREQUENCY]))
|
||||
cg.add(var.set_address(config[CONF_ADDRESS]))
|
||||
cg.add(var.set_reset(config[CONF_RESET]))
|
||||
|
||||
@@ -207,20 +207,26 @@ BLE_REMOVE_BOND_ACTION_SCHEMA = cv.Schema(
|
||||
)
|
||||
|
||||
|
||||
automation.register_simple_action(
|
||||
@automation.register_action(
|
||||
"ble_client.disconnect",
|
||||
BLEDisconnectAction,
|
||||
BLE_CONNECT_ACTION_SCHEMA,
|
||||
synchronous=False,
|
||||
)
|
||||
async def ble_disconnect_to_code(config, action_id, template_arg, args):
|
||||
parent = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, parent)
|
||||
|
||||
|
||||
automation.register_simple_action(
|
||||
@automation.register_action(
|
||||
"ble_client.connect",
|
||||
BLEConnectAction,
|
||||
BLE_CONNECT_ACTION_SCHEMA,
|
||||
synchronous=False,
|
||||
)
|
||||
async def ble_connect_to_code(config, action_id, template_arg, args):
|
||||
parent = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, parent)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
@@ -322,12 +328,15 @@ async def passkey_reply_to_code(config, action_id, template_arg, args):
|
||||
return var
|
||||
|
||||
|
||||
automation.register_simple_action(
|
||||
@automation.register_action(
|
||||
"ble_client.remove_bond",
|
||||
BLERemoveBondAction,
|
||||
BLE_REMOVE_BOND_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def remove_bond_to_code(config, action_id, template_arg, args):
|
||||
parent = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, parent)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
|
||||
@@ -7,7 +7,6 @@ from esphome.components.uart import (
|
||||
uart_ns,
|
||||
)
|
||||
from esphome.components.zephyr import zephyr_add_prj_conf
|
||||
from esphome.components.zephyr_ble_server import request_ble_l2cap_mtu
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_DEBUG,
|
||||
@@ -62,7 +61,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
zephyr_add_prj_conf("BT_NUS", True)
|
||||
request_ble_l2cap_mtu(247) # DLE sweet spot: one NUS frame per LL PDU
|
||||
expose_log = config[CONF_TYPE] == CONF_LOGS
|
||||
cg.add(var.set_expose_log(expose_log))
|
||||
if expose_log:
|
||||
|
||||
@@ -34,4 +34,24 @@ class BM8563 final : public time::RealTimeClock, public i2c::I2CDevice {
|
||||
uint8_t byte_to_bcd2_(uint8_t value);
|
||||
};
|
||||
|
||||
template<typename... Ts> class WriteAction final : public Action<Ts...>, public Parented<BM8563> {
|
||||
public:
|
||||
void play(const Ts &...x) override { this->parent_->write_time(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class ReadAction final : public Action<Ts...>, public Parented<BM8563> {
|
||||
public:
|
||||
void play(const Ts &...x) override { this->parent_->read_time(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class TimerAction final : public Action<Ts...>, public Parented<BM8563> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(uint32_t, duration)
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
auto duration = this->duration_.value(x...);
|
||||
this->parent_->start_timer(duration);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::bm8563
|
||||
@@ -3,6 +3,8 @@ import esphome.codegen as cg
|
||||
from esphome.components import i2c, time
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_DURATION, CONF_ID
|
||||
from esphome.core import ID
|
||||
from esphome.cpp_generator import MockObj, TemplateArgsType
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
@@ -11,6 +13,9 @@ I2C_ADDR = 0x51
|
||||
|
||||
bm8563_ns = cg.esphome_ns.namespace("bm8563")
|
||||
BM8563 = bm8563_ns.class_("BM8563", time.RealTimeClock, i2c.I2CDevice)
|
||||
WriteAction = bm8563_ns.class_("WriteAction", automation.Action)
|
||||
ReadAction = bm8563_ns.class_("ReadAction", automation.Action)
|
||||
TimerAction = bm8563_ns.class_("TimerAction", automation.Action)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
time.TIME_SCHEMA.extend(
|
||||
@@ -23,30 +28,70 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
|
||||
|
||||
BM8563_ACTION_SCHEMA = automation.maybe_simple_id(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(BM8563),
|
||||
}
|
||||
@automation.register_action(
|
||||
"bm8563.write_time",
|
||||
WriteAction,
|
||||
automation.maybe_simple_id(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(BM8563),
|
||||
}
|
||||
),
|
||||
synchronous=True,
|
||||
)
|
||||
async def bm8563_write_time_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
return var
|
||||
|
||||
automation.register_apply_action(
|
||||
"bm8563.write_time", BM8563_ACTION_SCHEMA, automation.ApplyCall("write_time()")
|
||||
)
|
||||
|
||||
automation.register_apply_action(
|
||||
"bm8563.read_time", BM8563_ACTION_SCHEMA, automation.ApplyCall("read_time()")
|
||||
)
|
||||
|
||||
automation.register_apply_action(
|
||||
@automation.register_action(
|
||||
"bm8563.start_timer",
|
||||
TimerAction,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(BM8563),
|
||||
cv.Required(CONF_DURATION): cv.templatable(cv.positive_time_period_seconds),
|
||||
}
|
||||
),
|
||||
automation.ApplyField(CONF_DURATION, "start_timer", cg.uint32),
|
||||
synchronous=True,
|
||||
)
|
||||
async def bm8563_start_timer_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
template_ = await cg.templatable(config[CONF_DURATION], args, cg.uint32)
|
||||
cg.add(var.set_duration(template_))
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"bm8563.read_time",
|
||||
ReadAction,
|
||||
automation.maybe_simple_id(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(BM8563),
|
||||
}
|
||||
),
|
||||
synchronous=True,
|
||||
)
|
||||
async def bm8563_read_time_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
return var
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
|
||||
@@ -90,14 +90,20 @@ async def to_code_base(config: ConfigType) -> MockObj:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
if await sensors(CONF_TEMPERATURE, var.set_temperature_sensor):
|
||||
oversampling = config[CONF_TEMPERATURE][CONF_OVERSAMPLING]
|
||||
cg.add(var.set_temperature_oversampling(oversampling))
|
||||
if await sensors(CONF_PRESSURE, var.set_pressure_sensor):
|
||||
cg.add(var.set_pressure_oversampling(config[CONF_PRESSURE][CONF_OVERSAMPLING]))
|
||||
if await sensors(CONF_HUMIDITY, var.set_humidity_sensor):
|
||||
cg.add(var.set_humidity_oversampling(config[CONF_HUMIDITY][CONF_OVERSAMPLING]))
|
||||
if temperature_config := config.get(CONF_TEMPERATURE):
|
||||
sens = await sensor.new_sensor(temperature_config)
|
||||
cg.add(var.set_temperature_sensor(sens))
|
||||
cg.add(var.set_temperature_oversampling(temperature_config[CONF_OVERSAMPLING]))
|
||||
|
||||
if pressure_config := config.get(CONF_PRESSURE):
|
||||
sens = await sensor.new_sensor(pressure_config)
|
||||
cg.add(var.set_pressure_sensor(sens))
|
||||
cg.add(var.set_pressure_oversampling(pressure_config[CONF_OVERSAMPLING]))
|
||||
|
||||
if humidity_config := config.get(CONF_HUMIDITY):
|
||||
sens = await sensor.new_sensor(humidity_config)
|
||||
cg.add(var.set_humidity_sensor(sens))
|
||||
cg.add(var.set_humidity_oversampling(humidity_config[CONF_OVERSAMPLING]))
|
||||
|
||||
cg.add(var.set_iir_filter(config[CONF_IIR_FILTER]))
|
||||
|
||||
|
||||
@@ -72,9 +72,18 @@ async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await setup_bthome_mithermometer(var, config)
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_TEMPERATURE, var.set_temperature)
|
||||
await sensors(CONF_HUMIDITY, var.set_humidity)
|
||||
await sensors(CONF_BATTERY_LEVEL, var.set_battery_level)
|
||||
await sensors(CONF_BATTERY_VOLTAGE, var.set_battery_voltage)
|
||||
await sensors(CONF_SIGNAL_STRENGTH, var.set_signal_strength)
|
||||
if temp_sens := config.get(CONF_TEMPERATURE):
|
||||
sens = await sensor.new_sensor(temp_sens)
|
||||
cg.add(var.set_temperature(sens))
|
||||
if humi_sens := config.get(CONF_HUMIDITY):
|
||||
sens = await sensor.new_sensor(humi_sens)
|
||||
cg.add(var.set_humidity(sens))
|
||||
if batl_sens := config.get(CONF_BATTERY_LEVEL):
|
||||
sens = await sensor.new_sensor(batl_sens)
|
||||
cg.add(var.set_battery_level(sens))
|
||||
if batv_sens := config.get(CONF_BATTERY_VOLTAGE):
|
||||
sens = await sensor.new_sensor(batv_sens)
|
||||
cg.add(var.set_battery_voltage(sens))
|
||||
if sgnl_sens := config.get(CONF_SIGNAL_STRENGTH):
|
||||
sens = await sensor.new_sensor(sgnl_sens)
|
||||
cg.add(var.set_signal_strength(sens))
|
||||
@@ -18,13 +18,12 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import (
|
||||
SubEntities,
|
||||
entity_duplicate_validator,
|
||||
queue_entity_register,
|
||||
setup_device_class,
|
||||
setup_entity,
|
||||
)
|
||||
from esphome.cpp_generator import MockObj, MockObjClass
|
||||
from esphome.cpp_generator import MockObj, MockObjClass, TemplateArgsType
|
||||
from esphome.types import ConfigType, SafeExpType
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
@@ -41,6 +40,8 @@ button_ns = cg.esphome_ns.namespace("button")
|
||||
Button = button_ns.class_("Button", cg.EntityBase)
|
||||
ButtonPtr = Button.operator("ptr")
|
||||
|
||||
PressAction = button_ns.class_("PressAction", automation.Action)
|
||||
|
||||
validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_")
|
||||
|
||||
|
||||
@@ -115,13 +116,6 @@ async def new_button(config: ConfigType, *args: SafeExpType) -> MockObj:
|
||||
return var
|
||||
|
||||
|
||||
def sub_buttons(
|
||||
config: ConfigType, *, parent: MockObj | ID | None = None
|
||||
) -> SubEntities:
|
||||
"""Return a SubEntities bound to new_button."""
|
||||
return SubEntities(new_button, config, parent)
|
||||
|
||||
|
||||
BUTTON_PRESS_SCHEMA = maybe_simple_id(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(Button),
|
||||
@@ -129,9 +123,17 @@ BUTTON_PRESS_SCHEMA = maybe_simple_id(
|
||||
)
|
||||
|
||||
|
||||
automation.register_apply_action(
|
||||
"button.press", BUTTON_PRESS_SCHEMA, automation.ApplyCall("press()")
|
||||
@automation.register_action(
|
||||
"button.press", PressAction, BUTTON_PRESS_SCHEMA, synchronous=True
|
||||
)
|
||||
async def button_press_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.CORE)
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/button/button.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
namespace esphome::button {
|
||||
|
||||
template<typename... Ts> class PressAction final : public Action<Ts...> {
|
||||
public:
|
||||
explicit PressAction(Button *button) : button_(button) {}
|
||||
|
||||
void play(const Ts &...x) override { this->button_->press(); }
|
||||
|
||||
protected:
|
||||
Button *button_;
|
||||
};
|
||||
|
||||
} // namespace esphome::button
|
||||
@@ -351,6 +351,10 @@ async def to_code(config):
|
||||
|
||||
|
||||
# Actions
|
||||
BeginTxAction = ns.class_("BeginTxAction", automation.Action)
|
||||
BeginRxAction = ns.class_("BeginRxAction", automation.Action)
|
||||
ResetAction = ns.class_("ResetAction", automation.Action)
|
||||
SetIdleAction = ns.class_("SetIdleAction", automation.Action)
|
||||
SendPacketAction = ns.class_(
|
||||
"SendPacketAction", automation.Action, cg.Parented.template(CC1101Component)
|
||||
)
|
||||
@@ -360,15 +364,22 @@ CC1101_ACTION_SCHEMA = cv.Schema(
|
||||
)
|
||||
|
||||
|
||||
for _name, _call in (
|
||||
("cc1101.begin_tx", "begin_tx()"),
|
||||
("cc1101.begin_rx", "begin_rx()"),
|
||||
("cc1101.reset", "reset()"),
|
||||
("cc1101.set_idle", "set_idle()"),
|
||||
):
|
||||
automation.register_apply_action(
|
||||
_name, CC1101_ACTION_SCHEMA, automation.ApplyCall(_call)
|
||||
)
|
||||
@automation.register_action(
|
||||
"cc1101.begin_tx", BeginTxAction, CC1101_ACTION_SCHEMA, synchronous=True
|
||||
)
|
||||
@automation.register_action(
|
||||
"cc1101.begin_rx", BeginRxAction, CC1101_ACTION_SCHEMA, synchronous=True
|
||||
)
|
||||
@automation.register_action(
|
||||
"cc1101.reset", ResetAction, CC1101_ACTION_SCHEMA, synchronous=True
|
||||
)
|
||||
@automation.register_action(
|
||||
"cc1101.set_idle", SetIdleAction, CC1101_ACTION_SCHEMA, synchronous=True
|
||||
)
|
||||
async def cc1101_action_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
return var
|
||||
|
||||
|
||||
def validate_raw_data(value):
|
||||
|
||||
@@ -130,6 +130,26 @@ class CC1101Component final : public Component,
|
||||
};
|
||||
|
||||
// Action Wrappers
|
||||
template<typename... Ts> class BeginTxAction final : public Action<Ts...>, public Parented<CC1101Component> {
|
||||
public:
|
||||
void play(const Ts &...x) override { this->parent_->begin_tx(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class BeginRxAction final : public Action<Ts...>, public Parented<CC1101Component> {
|
||||
public:
|
||||
void play(const Ts &...x) override { this->parent_->begin_rx(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class ResetAction final : public Action<Ts...>, public Parented<CC1101Component> {
|
||||
public:
|
||||
void play(const Ts &...x) override { this->parent_->reset(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class SetIdleAction final : public Action<Ts...>, public Parented<CC1101Component> {
|
||||
public:
|
||||
void play(const Ts &...x) override { this->parent_->set_idle(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class SendPacketAction final : public Action<Ts...>, public Parented<CC1101Component> {
|
||||
public:
|
||||
void set_data_template(std::function<std::vector<uint8_t>(Ts...)> func) { this->data_func_ = func; }
|
||||
|
||||
@@ -435,7 +435,6 @@ void Climate::save_state_(const ClimateTraits &traits) {
|
||||
}
|
||||
|
||||
void Climate::publish_state() {
|
||||
this->set_has_state(true);
|
||||
ESP_LOGV(TAG, "'%s' >>", this->name_.c_str());
|
||||
auto traits = this->get_traits();
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
|
||||
@@ -22,4 +23,14 @@ class CM1106Component final : public PollingComponent, public uart::UARTDevice {
|
||||
bool cm1106_write_command_(const uint8_t *command, size_t command_len, uint8_t *response, size_t response_len);
|
||||
};
|
||||
|
||||
template<typename... Ts> class CM1106CalibrateZeroAction final : public Action<Ts...> {
|
||||
public:
|
||||
CM1106CalibrateZeroAction(CM1106Component *cm1106) : cm1106_(cm1106) {}
|
||||
|
||||
void play(const Ts &...x) override { this->cm1106_->calibrate_zero(400); }
|
||||
|
||||
protected:
|
||||
CM1106Component *cm1106_;
|
||||
};
|
||||
|
||||
} // namespace esphome::cm1106
|
||||
@@ -13,6 +13,8 @@ from esphome.const import (
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_PARTS_PER_MILLION,
|
||||
)
|
||||
from esphome.core import ID
|
||||
from esphome.cpp_generator import MockObj, TemplateArgsType
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DEPENDENCIES = ["uart"]
|
||||
@@ -22,6 +24,10 @@ cm1106_ns = cg.esphome_ns.namespace("cm1106")
|
||||
CM1106Component = cm1106_ns.class_(
|
||||
"CM1106Component", cg.PollingComponent, uart.UARTDevice
|
||||
)
|
||||
CM1106CalibrateZeroAction = cm1106_ns.class_(
|
||||
"CM1106CalibrateZeroAction",
|
||||
automation.Action,
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
@@ -66,8 +72,18 @@ CALIBRATION_ACTION_SCHEMA = maybe_simple_id(
|
||||
)
|
||||
|
||||
|
||||
automation.register_apply_action(
|
||||
@automation.register_action(
|
||||
"cm1106.calibrate_zero",
|
||||
CM1106CalibrateZeroAction,
|
||||
CALIBRATION_ACTION_SCHEMA,
|
||||
automation.ApplyCall("calibrate_zero(400)"),
|
||||
synchronous=True,
|
||||
)
|
||||
async def cm1106_calibration_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
"""Service code generation entry point."""
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
@@ -15,7 +15,6 @@ CONF_CLIMATE_ID = "climate_id"
|
||||
CONF_CO2_EQUIVALENT = "co2_equivalent"
|
||||
CONF_COLOR_DEPTH = "color_depth"
|
||||
CONF_COLUMNS = "columns"
|
||||
CONF_CONVERSION_RATE = "conversion_rate"
|
||||
CONF_CRC_ENABLE = "crc_enable"
|
||||
CONF_DATA_BITS = "data_bits"
|
||||
CONF_DESCRIPTION = "description"
|
||||
@@ -24,7 +23,6 @@ CONF_ENABLE_OTA_DOWNGRADE_PROTECTION = "enable_ota_downgrade_protection"
|
||||
CONF_ENABLED = "enabled"
|
||||
CONF_GYROSCOPE_ODR = "gyroscope_odr"
|
||||
CONF_GYROSCOPE_RANGE = "gyroscope_range"
|
||||
CONF_HOST = "host"
|
||||
CONF_HUMIDITY_SOURCE = "humidity_source"
|
||||
CONF_IAQ = "iaq"
|
||||
CONF_IGNORE_NOT_FOUND = "ignore_not_found"
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
"""Standard CSS named colors, as 0xRRGGBB values."""
|
||||
|
||||
CSS_COLORS: dict[str, int] = {
|
||||
"aliceblue": 0xF0F8FF,
|
||||
"antiquewhite": 0xFAEBD7,
|
||||
"aqua": 0x00FFFF,
|
||||
"aquamarine": 0x7FFFD4,
|
||||
"azure": 0xF0FFFF,
|
||||
"beige": 0xF5F5DC,
|
||||
"bisque": 0xFFE4C4,
|
||||
"black": 0x000000,
|
||||
"blanchedalmond": 0xFFEBCD,
|
||||
"blue": 0x0000FF,
|
||||
"blueviolet": 0x8A2BE2,
|
||||
"brown": 0xA52A2A,
|
||||
"burlywood": 0xDEB887,
|
||||
"cadetblue": 0x5F9EA0,
|
||||
"chartreuse": 0x7FFF00,
|
||||
"chocolate": 0xD2691E,
|
||||
"coral": 0xFF7F50,
|
||||
"cornflowerblue": 0x6495ED,
|
||||
"cornsilk": 0xFFF8DC,
|
||||
"crimson": 0xDC143C,
|
||||
"cyan": 0x00FFFF,
|
||||
"darkblue": 0x00008B,
|
||||
"darkcyan": 0x008B8B,
|
||||
"darkgoldenrod": 0xB8860B,
|
||||
"darkgray": 0xA9A9A9,
|
||||
"darkgreen": 0x006400,
|
||||
"darkgrey": 0xA9A9A9,
|
||||
"darkkhaki": 0xBDB76B,
|
||||
"darkmagenta": 0x8B008B,
|
||||
"darkolivegreen": 0x556B2F,
|
||||
"darkorange": 0xFF8C00,
|
||||
"darkorchid": 0x9932CC,
|
||||
"darkred": 0x8B0000,
|
||||
"darksalmon": 0xE9967A,
|
||||
"darkseagreen": 0x8FBC8F,
|
||||
"darkslateblue": 0x483D8B,
|
||||
"darkslategray": 0x2F4F4F,
|
||||
"darkslategrey": 0x2F4F4F,
|
||||
"darkturquoise": 0x00CED1,
|
||||
"darkviolet": 0x9400D3,
|
||||
"deeppink": 0xFF1493,
|
||||
"deepskyblue": 0x00BFFF,
|
||||
"dimgray": 0x696969,
|
||||
"dimgrey": 0x696969,
|
||||
"dodgerblue": 0x1E90FF,
|
||||
"firebrick": 0xB22222,
|
||||
"floralwhite": 0xFFFAF0,
|
||||
"forestgreen": 0x228B22,
|
||||
"fuchsia": 0xFF00FF,
|
||||
"gainsboro": 0xDCDCDC,
|
||||
"ghostwhite": 0xF8F8FF,
|
||||
"goldenrod": 0xDAA520,
|
||||
"gold": 0xFFD700,
|
||||
"gray": 0x808080,
|
||||
"green": 0x008000,
|
||||
"greenyellow": 0xADFF2F,
|
||||
"grey": 0x808080,
|
||||
"honeydew": 0xF0FFF0,
|
||||
"hotpink": 0xFF69B4,
|
||||
"indianred": 0xCD5C5C,
|
||||
"indigo": 0x4B0082,
|
||||
"ivory": 0xFFFFF0,
|
||||
"khaki": 0xF0E68C,
|
||||
"lavenderblush": 0xFFF0F5,
|
||||
"lavender": 0xE6E6FA,
|
||||
"lawngreen": 0x7CFC00,
|
||||
"lemonchiffon": 0xFFFACD,
|
||||
"lightblue": 0xADD8E6,
|
||||
"lightcoral": 0xF08080,
|
||||
"lightcyan": 0xE0FFFF,
|
||||
"lightgoldenrodyellow": 0xFAFAD2,
|
||||
"lightgray": 0xD3D3D3,
|
||||
"lightgreen": 0x90EE90,
|
||||
"lightgrey": 0xD3D3D3,
|
||||
"lightpink": 0xFFB6C1,
|
||||
"lightsalmon": 0xFFA07A,
|
||||
"lightseagreen": 0x20B2AA,
|
||||
"lightskyblue": 0x87CEFA,
|
||||
"lightslategray": 0x778899,
|
||||
"lightslategrey": 0x778899,
|
||||
"lightsteelblue": 0xB0C4DE,
|
||||
"lightyellow": 0xFFFFE0,
|
||||
"lime": 0x00FF00,
|
||||
"limegreen": 0x32CD32,
|
||||
"linen": 0xFAF0E6,
|
||||
"magenta": 0xFF00FF,
|
||||
"maroon": 0x800000,
|
||||
"mediumaquamarine": 0x66CDAA,
|
||||
"mediumblue": 0x0000CD,
|
||||
"mediumorchid": 0xBA55D3,
|
||||
"mediumpurple": 0x9370DB,
|
||||
"mediumseagreen": 0x3CB371,
|
||||
"mediumslateblue": 0x7B68EE,
|
||||
"mediumspringgreen": 0x00FA9A,
|
||||
"mediumturquoise": 0x48D1CC,
|
||||
"mediumvioletred": 0xC71585,
|
||||
"midnightblue": 0x191970,
|
||||
"mintcream": 0xF5FFFA,
|
||||
"mistyrose": 0xFFE4E1,
|
||||
"moccasin": 0xFFE4B5,
|
||||
"navajowhite": 0xFFDEAD,
|
||||
"navy": 0x000080,
|
||||
"oldlace": 0xFDF5E6,
|
||||
"olive": 0x808000,
|
||||
"olivedrab": 0x6B8E23,
|
||||
"orange": 0xFFA500,
|
||||
"orangered": 0xFF4500,
|
||||
"orchid": 0xDA70D6,
|
||||
"palegoldenrod": 0xEEE8AA,
|
||||
"palegreen": 0x98FB98,
|
||||
"paleturquoise": 0xAFEEEE,
|
||||
"palevioletred": 0xDB7093,
|
||||
"papayawhip": 0xFFEFD5,
|
||||
"peachpuff": 0xFFDAB9,
|
||||
"peru": 0xCD853F,
|
||||
"pink": 0xFFC0CB,
|
||||
"plum": 0xDDA0DD,
|
||||
"powderblue": 0xB0E0E6,
|
||||
"purple": 0x800080,
|
||||
"rebeccapurple": 0x663399,
|
||||
"red": 0xFF0000,
|
||||
"rosybrown": 0xBC8F8F,
|
||||
"royalblue": 0x4169E1,
|
||||
"saddlebrown": 0x8B4513,
|
||||
"salmon": 0xFA8072,
|
||||
"sandybrown": 0xF4A460,
|
||||
"seagreen": 0x2E8B57,
|
||||
"seashell": 0xFFF5EE,
|
||||
"sienna": 0xA0522D,
|
||||
"silver": 0xC0C0C0,
|
||||
"skyblue": 0x87CEEB,
|
||||
"slateblue": 0x6A5ACD,
|
||||
"slategray": 0x708090,
|
||||
"slategrey": 0x708090,
|
||||
"snow": 0xFFFAFA,
|
||||
"springgreen": 0x00FF7F,
|
||||
"steelblue": 0x4682B4,
|
||||
"tan": 0xD2B48C,
|
||||
"teal": 0x008080,
|
||||
"thistle": 0xD8BFD8,
|
||||
"tomato": 0xFF6347,
|
||||
"turquoise": 0x40E0D0,
|
||||
"violet": 0xEE82EE,
|
||||
"wheat": 0xF5DEB3,
|
||||
"white": 0xFFFFFF,
|
||||
"whitesmoke": 0xF5F5F5,
|
||||
"yellow": 0xFFFF00,
|
||||
"yellowgreen": 0x9ACD32,
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
CODEOWNERS = ["@clydebarrow"]
|
||||
@@ -1,28 +0,0 @@
|
||||
#include "counter_sensor.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::counter {
|
||||
|
||||
static const char *const TAG = "counter";
|
||||
|
||||
void CounterSensor::setup() {
|
||||
if (this->restore_) {
|
||||
this->pref_ = this->make_entity_preference<int64_t>();
|
||||
this->pref_.load(&this->value_);
|
||||
}
|
||||
this->publish_state(static_cast<float>(this->value_));
|
||||
}
|
||||
|
||||
void CounterSensor::set_value(int64_t value) {
|
||||
this->value_ = value;
|
||||
this->publish_state(static_cast<float>(value));
|
||||
if (this->restore_)
|
||||
this->pref_.save(&this->value_);
|
||||
}
|
||||
|
||||
void CounterSensor::dump_config() {
|
||||
LOG_SENSOR("", "Counter", this);
|
||||
ESP_LOGCONFIG(TAG, " Restore: %s", YESNO(this->restore_));
|
||||
}
|
||||
|
||||
} // namespace esphome::counter
|
||||
@@ -1,47 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
|
||||
namespace esphome::counter {
|
||||
|
||||
/// A counter held as a 64-bit integer. The published sensor state is a float, so it is exact only up to 2^24.
|
||||
class CounterSensor final : public sensor::Sensor, public Component {
|
||||
public:
|
||||
/// The counter starts at initial_value unless a stored value is restored.
|
||||
CounterSensor(bool restore, int64_t initial_value) : value_(initial_value), restore_(restore) {}
|
||||
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
// restore value before `on_boot` automations run
|
||||
float get_setup_priority() const override { return setup_priority::DATA + 50.0f; }
|
||||
|
||||
/// Increment by one each time the given sensor publishes a state.
|
||||
void count_updates_from(sensor::Sensor *source) {
|
||||
source->add_on_state_callback([this](float) { this->increment(); });
|
||||
}
|
||||
|
||||
/// Increment by one each time the given binary sensor changes to true.
|
||||
template<typename T> void count_true_from(T *source) {
|
||||
source->add_on_state_callback([this](bool state) {
|
||||
if (state)
|
||||
this->increment();
|
||||
});
|
||||
}
|
||||
|
||||
void set_value(int64_t value);
|
||||
/// overflow on addition of signed numbers is undefined - use the well defined unsigned version
|
||||
void increment(int64_t amount = 1) {
|
||||
this->set_value(static_cast<int64_t>(static_cast<uint64_t>(this->value_) + static_cast<uint64_t>(amount)));
|
||||
}
|
||||
|
||||
protected:
|
||||
ESPPreferenceObject pref_;
|
||||
int64_t value_;
|
||||
bool restore_;
|
||||
};
|
||||
|
||||
} // namespace esphome::counter
|
||||
@@ -1,85 +0,0 @@
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import binary_sensor, sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BINARY_SENSOR,
|
||||
CONF_ID,
|
||||
CONF_INITIAL_VALUE,
|
||||
CONF_RESTORE,
|
||||
CONF_SENSOR,
|
||||
CONF_VALUE,
|
||||
ICON_COUNTER,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
counter_ns = cg.esphome_ns.namespace("counter")
|
||||
CounterSensor = counter_ns.class_("CounterSensor", sensor.Sensor, cg.Component)
|
||||
|
||||
# The lowest value is left out because its C++ literal cannot be written portably.
|
||||
INT64_MAX = 2**63 - 1
|
||||
COUNTER_RANGE = cv.int_range(min=-INT64_MAX, max=INT64_MAX)
|
||||
COUNTER_VALUE = cv.templatable(COUNTER_RANGE)
|
||||
|
||||
|
||||
def _not_own_source(config: ConfigType) -> ConfigType:
|
||||
if (source := config.get(CONF_SENSOR)) is not None and source.id == config[
|
||||
CONF_ID
|
||||
].id:
|
||||
raise cv.Invalid("A counter cannot count its own updates", [CONF_SENSOR])
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
sensor.sensor_schema(
|
||||
CounterSensor,
|
||||
icon=ICON_COUNTER,
|
||||
accuracy_decimals=0,
|
||||
)
|
||||
.extend(
|
||||
{
|
||||
cv.Optional(CONF_RESTORE, default=True): cv.boolean,
|
||||
cv.Optional(CONF_INITIAL_VALUE, default=0): COUNTER_RANGE,
|
||||
cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor),
|
||||
cv.Optional(CONF_BINARY_SENSOR): cv.use_id(binary_sensor.BinarySensor),
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA),
|
||||
_not_own_source,
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(
|
||||
config[CONF_ID], config[CONF_RESTORE], config[CONF_INITIAL_VALUE]
|
||||
)
|
||||
await cg.register_component(var, config)
|
||||
await sensor.register_sensor(var, config)
|
||||
if (source := config.get(CONF_SENSOR)) is not None:
|
||||
cg.add(var.count_updates_from(await cg.get_variable(source)))
|
||||
if (source := config.get(CONF_BINARY_SENSOR)) is not None:
|
||||
cg.add(var.count_true_from(await cg.get_variable(source)))
|
||||
|
||||
|
||||
automation.register_apply_action(
|
||||
"counter.set_value",
|
||||
cv.maybe_simple_value(
|
||||
{
|
||||
cv.GenerateID(CONF_ID): cv.use_id(CounterSensor),
|
||||
cv.Required(CONF_VALUE): COUNTER_VALUE,
|
||||
},
|
||||
key=CONF_VALUE,
|
||||
),
|
||||
automation.ApplyField(CONF_VALUE, "set_value", cg.int64),
|
||||
)
|
||||
|
||||
automation.register_apply_action(
|
||||
"counter.increment",
|
||||
automation.maybe_simple_id(
|
||||
{
|
||||
cv.GenerateID(CONF_ID): cv.use_id(CounterSensor),
|
||||
cv.Optional(CONF_VALUE, default=1): COUNTER_VALUE,
|
||||
}
|
||||
),
|
||||
automation.ApplyField(CONF_VALUE, "increment", cg.int64),
|
||||
)
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/spi/spi.h"
|
||||
|
||||
@@ -107,4 +108,14 @@ class CS5460AComponent final : public Component,
|
||||
uint32_t prev_raw_energy_{0};
|
||||
};
|
||||
|
||||
template<typename... Ts> class CS5460ARestartAction final : public Action<Ts...> {
|
||||
public:
|
||||
CS5460ARestartAction(CS5460AComponent *cs5460a) : cs5460a_(cs5460a) {}
|
||||
|
||||
void play(const Ts &...x) override { cs5460a_->restart(); }
|
||||
|
||||
protected:
|
||||
CS5460AComponent *cs5460a_;
|
||||
};
|
||||
|
||||
} // namespace esphome::cs5460a
|
||||
@@ -17,6 +17,8 @@ from esphome.const import (
|
||||
UNIT_VOLT,
|
||||
UNIT_WATT,
|
||||
)
|
||||
from esphome.core import ID
|
||||
from esphome.cpp_generator import MockObj, TemplateArgsType
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@balrog-kun"]
|
||||
@@ -30,6 +32,7 @@ PGA_GAIN_OPTIONS = {
|
||||
}
|
||||
|
||||
CS5460AComponent = cs5460a_ns.class_("CS5460AComponent", spi.SPIDevice, cg.Component)
|
||||
CS5460ARestartAction = cs5460a_ns.class_("CS5460ARestartAction", automation.Action)
|
||||
|
||||
CONF_SAMPLES = "samples"
|
||||
CONF_PHASE_OFFSET = "phase_offset"
|
||||
@@ -128,12 +131,21 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add(var.set_power_sensor(sens))
|
||||
|
||||
|
||||
automation.register_apply_action(
|
||||
@automation.register_action(
|
||||
"cs5460a.restart",
|
||||
CS5460ARestartAction,
|
||||
maybe_simple_id(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(CS5460AComponent),
|
||||
}
|
||||
),
|
||||
automation.ApplyCall("restart()"),
|
||||
synchronous=True,
|
||||
)
|
||||
async def restart_action_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
@@ -98,11 +98,24 @@ async def to_code(config: ConfigType) -> None:
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_VOLTAGE, var.set_voltage_sensor)
|
||||
await sensors(CONF_CURRENT, var.set_current_sensor)
|
||||
await sensors(CONF_POWER, var.set_power_sensor)
|
||||
await sensors(CONF_ENERGY, var.set_energy_sensor)
|
||||
await sensors(CONF_APPARENT_POWER, var.set_apparent_power_sensor)
|
||||
await sensors(CONF_REACTIVE_POWER, var.set_reactive_power_sensor)
|
||||
await sensors(CONF_POWER_FACTOR, var.set_power_factor_sensor)
|
||||
if voltage_config := config.get(CONF_VOLTAGE):
|
||||
sens = await sensor.new_sensor(voltage_config)
|
||||
cg.add(var.set_voltage_sensor(sens))
|
||||
if current_config := config.get(CONF_CURRENT):
|
||||
sens = await sensor.new_sensor(current_config)
|
||||
cg.add(var.set_current_sensor(sens))
|
||||
if power_config := config.get(CONF_POWER):
|
||||
sens = await sensor.new_sensor(power_config)
|
||||
cg.add(var.set_power_sensor(sens))
|
||||
if energy_config := config.get(CONF_ENERGY):
|
||||
sens = await sensor.new_sensor(energy_config)
|
||||
cg.add(var.set_energy_sensor(sens))
|
||||
if apparent_power_config := config.get(CONF_APPARENT_POWER):
|
||||
sens = await sensor.new_sensor(apparent_power_config)
|
||||
cg.add(var.set_apparent_power_sensor(sens))
|
||||
if reactive_power_config := config.get(CONF_REACTIVE_POWER):
|
||||
sens = await sensor.new_sensor(reactive_power_config)
|
||||
cg.add(var.set_reactive_power_sensor(sens))
|
||||
if power_factor_config := config.get(CONF_POWER_FACTOR):
|
||||
sens = await sensor.new_sensor(power_factor_config)
|
||||
cg.add(var.set_power_factor_sensor(sens))
|
||||
@@ -1,5 +1,4 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.nrf52.framework import include_west_project
|
||||
from esphome.components.zephyr import zephyr_add_prj_conf
|
||||
from esphome.config_helpers import filter_source_files_from_platform
|
||||
import esphome.config_validation as cv
|
||||
@@ -52,8 +51,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
zephyr_add_prj_conf("HWINFO", True)
|
||||
# gdb thread support
|
||||
zephyr_add_prj_conf("DEBUG_THREAD_INFO", True)
|
||||
# RTT sources live in the segger project
|
||||
include_west_project("segger")
|
||||
# RTT
|
||||
zephyr_add_prj_conf("USE_SEGGER_RTT", True)
|
||||
zephyr_add_prj_conf("RTT_CONSOLE", True)
|
||||
zephyr_add_prj_conf("LOG", True)
|
||||
|
||||
@@ -106,13 +106,32 @@ CONFIG_SCHEMA = {
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_DEBUG_ID])
|
||||
debug_component = await cg.get_variable(config[CONF_DEBUG_ID])
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_FREE, hub.set_free_sensor)
|
||||
await sensors(CONF_BLOCK, hub.set_block_sensor)
|
||||
await sensors(CONF_FRAGMENTATION, hub.set_fragmentation_sensor)
|
||||
await sensors(CONF_MIN_FREE, hub.set_min_free_sensor)
|
||||
await sensors(CONF_LOOP_TIME, hub.set_loop_time_sensor)
|
||||
await sensors(CONF_PSRAM, hub.set_psram_sensor)
|
||||
await sensors(CONF_CPU_FREQUENCY, hub.set_cpu_frequency_sensor)
|
||||
if free_conf := config.get(CONF_FREE):
|
||||
sens = await sensor.new_sensor(free_conf)
|
||||
cg.add(debug_component.set_free_sensor(sens))
|
||||
|
||||
if block_conf := config.get(CONF_BLOCK):
|
||||
sens = await sensor.new_sensor(block_conf)
|
||||
cg.add(debug_component.set_block_sensor(sens))
|
||||
|
||||
if fragmentation_conf := config.get(CONF_FRAGMENTATION):
|
||||
sens = await sensor.new_sensor(fragmentation_conf)
|
||||
cg.add(debug_component.set_fragmentation_sensor(sens))
|
||||
|
||||
if min_free_conf := config.get(CONF_MIN_FREE):
|
||||
sens = await sensor.new_sensor(min_free_conf)
|
||||
cg.add(debug_component.set_min_free_sensor(sens))
|
||||
|
||||
if loop_time_conf := config.get(CONF_LOOP_TIME):
|
||||
sens = await sensor.new_sensor(loop_time_conf)
|
||||
cg.add(debug_component.set_loop_time_sensor(sens))
|
||||
|
||||
if psram_conf := config.get(CONF_PSRAM):
|
||||
sens = await sensor.new_sensor(psram_conf)
|
||||
cg.add(debug_component.set_psram_sensor(sens))
|
||||
|
||||
if cpu_freq_conf := config.get(CONF_CPU_FREQUENCY):
|
||||
sens = await sensor.new_sensor(cpu_freq_conf)
|
||||
cg.add(debug_component.set_cpu_frequency_sensor(sens))
|
||||
@@ -31,8 +31,11 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_DEBUG_ID])
|
||||
debug_component = await cg.get_variable(config[CONF_DEBUG_ID])
|
||||
|
||||
text_sensors = text_sensor.sub_text_sensors(config)
|
||||
await text_sensors(CONF_DEVICE, hub.set_device_info_sensor)
|
||||
await text_sensors(CONF_RESET_REASON, hub.set_reset_reason_sensor)
|
||||
if CONF_DEVICE in config:
|
||||
sens = await text_sensor.new_text_sensor(config[CONF_DEVICE])
|
||||
cg.add(debug_component.set_device_info_sensor(sens))
|
||||
if CONF_RESET_REASON in config:
|
||||
sens = await text_sensor.new_text_sensor(config[CONF_RESET_REASON])
|
||||
cg.add(debug_component.set_reset_reason_sensor(sens))
|
||||
@@ -214,6 +214,16 @@ def _validate_sleep_duration(value: core.TimePeriod) -> core.TimePeriod:
|
||||
deep_sleep_ns = cg.esphome_ns.namespace("deep_sleep")
|
||||
DeepSleepComponent = deep_sleep_ns.class_("DeepSleepComponent", cg.Component)
|
||||
EnterDeepSleepAction = deep_sleep_ns.class_("EnterDeepSleepAction", automation.Action)
|
||||
PreventDeepSleepAction = deep_sleep_ns.class_(
|
||||
"PreventDeepSleepAction",
|
||||
automation.Action,
|
||||
cg.Parented.template(DeepSleepComponent),
|
||||
)
|
||||
AllowDeepSleepAction = deep_sleep_ns.class_(
|
||||
"AllowDeepSleepAction",
|
||||
automation.Action,
|
||||
cg.Parented.template(DeepSleepComponent),
|
||||
)
|
||||
|
||||
WakeupPinMode = deep_sleep_ns.enum("WakeupPinMode")
|
||||
WAKEUP_PIN_MODES = {
|
||||
@@ -480,15 +490,27 @@ async def deep_sleep_enter_to_code(
|
||||
return var
|
||||
|
||||
|
||||
for _name, _call in (
|
||||
("deep_sleep.prevent", "prevent_deep_sleep()"),
|
||||
("deep_sleep.allow", "allow_deep_sleep()"),
|
||||
):
|
||||
automation.register_apply_action(
|
||||
_name,
|
||||
automation.maybe_simple_id(DEEP_SLEEP_ACTION_SCHEMA),
|
||||
automation.ApplyCall(_call),
|
||||
)
|
||||
@automation.register_action(
|
||||
"deep_sleep.prevent",
|
||||
PreventDeepSleepAction,
|
||||
automation.maybe_simple_id(DEEP_SLEEP_ACTION_SCHEMA),
|
||||
synchronous=True,
|
||||
)
|
||||
@automation.register_action(
|
||||
"deep_sleep.allow",
|
||||
AllowDeepSleepAction,
|
||||
automation.maybe_simple_id(DEEP_SLEEP_ACTION_SCHEMA),
|
||||
synchronous=True,
|
||||
)
|
||||
async def deep_sleep_action_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
return var
|
||||
|
||||
|
||||
FILTER_SOURCE_FILES = filter_source_files_from_platform(
|
||||
|
||||
@@ -121,6 +121,8 @@ class Ext1WakeTrigger : public Trigger<>, public Component {
|
||||
|
||||
template<typename... Ts> class EnterDeepSleepAction;
|
||||
|
||||
template<typename... Ts> class PreventDeepSleepAction;
|
||||
|
||||
/** This component allows setting up the node to go into deep sleep mode to conserve battery.
|
||||
*
|
||||
* To set this component up, first set *when* the deep sleep should trigger using set_run_cycles
|
||||
@@ -296,4 +298,15 @@ template<typename... Ts> class EnterDeepSleepAction final : public Action<Ts...>
|
||||
#endif
|
||||
};
|
||||
|
||||
template<typename... Ts>
|
||||
class PreventDeepSleepAction final : public Action<Ts...>, public Parented<DeepSleepComponent> {
|
||||
public:
|
||||
void play(const Ts &...x) override { this->parent_->prevent_deep_sleep(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class AllowDeepSleepAction final : public Action<Ts...>, public Parented<DeepSleepComponent> {
|
||||
public:
|
||||
void play(const Ts &...x) override { this->parent_->allow_deep_sleep(); }
|
||||
};
|
||||
|
||||
} // namespace esphome::deep_sleep
|
||||
@@ -12,6 +12,9 @@ CODEOWNERS = ["@glmnet"]
|
||||
|
||||
dfplayer_ns = cg.esphome_ns.namespace("dfplayer")
|
||||
DFPlayer = dfplayer_ns.class_("DFPlayer", cg.Component)
|
||||
DFPlayerIsPlayingCondition = dfplayer_ns.class_(
|
||||
"DFPlayerIsPlayingCondition", automation.Condition
|
||||
)
|
||||
|
||||
MULTI_CONF = True
|
||||
CONF_FOLDER = "folder"
|
||||
@@ -196,12 +199,16 @@ automation.register_apply_action(
|
||||
)
|
||||
|
||||
|
||||
automation.register_apply_condition(
|
||||
@automation.register_condition(
|
||||
"dfplayer.is_playing",
|
||||
DFPlayerIsPlayingCondition,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(DFPlayer),
|
||||
}
|
||||
),
|
||||
"is_playing()",
|
||||
)
|
||||
async def dfplayer_is_playing_to_code(config, condition_id, template_arg, args):
|
||||
var = cg.new_Pvariable(condition_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
return var
|
||||
@@ -77,4 +77,9 @@ class DFPlayer final : public uart::UARTDevice, public Component {
|
||||
CallbackManager<void()> on_finished_playback_callback_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class DFPlayerIsPlayingCondition final : public Condition<Ts...>, public Parented<DFPlayer> {
|
||||
public:
|
||||
bool check(const Ts &...x) override { return this->parent_->is_playing(); }
|
||||
};
|
||||
|
||||
} // namespace esphome::dfplayer
|
||||
@@ -20,6 +20,9 @@ DfrobotSen0395Component = dfrobot_sen0395_ns.class_(
|
||||
)
|
||||
|
||||
# Actions
|
||||
DfrobotSen0395ResetAction = dfrobot_sen0395_ns.class_(
|
||||
"DfrobotSen0395ResetAction", automation.Action
|
||||
)
|
||||
DfrobotSen0395SettingsAction = dfrobot_sen0395_ns.class_(
|
||||
"DfrobotSen0395SettingsAction", automation.Action
|
||||
)
|
||||
@@ -46,15 +49,26 @@ async def to_code(config: ConfigType) -> None:
|
||||
await uart.register_uart_device(var, config)
|
||||
|
||||
|
||||
automation.register_apply_action(
|
||||
@automation.register_action(
|
||||
"dfrobot_sen0395.reset",
|
||||
DfrobotSen0395ResetAction,
|
||||
maybe_simple_id(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(DfrobotSen0395Component),
|
||||
}
|
||||
),
|
||||
automation.ApplyCall("reset_system()"),
|
||||
synchronous=True,
|
||||
)
|
||||
async def dfrobot_sen0395_reset_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
|
||||
return var
|
||||
|
||||
|
||||
def range_segment_list(input: Any) -> list:
|
||||
|
||||
@@ -7,6 +7,12 @@
|
||||
|
||||
namespace esphome::dfrobot_sen0395 {
|
||||
|
||||
template<typename... Ts>
|
||||
class DfrobotSen0395ResetAction final : public Action<Ts...>, public Parented<DfrobotSen0395Component> {
|
||||
public:
|
||||
void play(const Ts &...x) { this->parent_->enqueue(make_unique<ResetSystemCommand>()); }
|
||||
};
|
||||
|
||||
template<typename... Ts>
|
||||
class DfrobotSen0395SettingsAction final : public Action<Ts...>, public Parented<DfrobotSen0395Component> {
|
||||
public:
|
||||
|
||||
@@ -94,8 +94,6 @@ class DfrobotSen0395Component final : public uart::UARTDevice, public Component
|
||||
#endif
|
||||
|
||||
int8_t enqueue(std::unique_ptr<Command> cmd);
|
||||
/// Queue a sensor reset
|
||||
void reset_system() { this->enqueue(make_unique<ResetSystemCommand>()); }
|
||||
|
||||
protected:
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
|
||||
@@ -39,6 +39,9 @@ DisplayPage = display_ns.class_("DisplayPage")
|
||||
DisplayPagePtr = DisplayPage.operator("ptr")
|
||||
DisplayRef = Display.operator("ref")
|
||||
DisplayPageShowAction = display_ns.class_("DisplayPageShowAction", automation.Action)
|
||||
DisplayIsDisplayingPageCondition = display_ns.class_(
|
||||
"DisplayIsDisplayingPageCondition", automation.Condition
|
||||
)
|
||||
DisplayOnPageChangeTrigger = display_ns.class_(
|
||||
"DisplayOnPageChangeTrigger", automation.Trigger
|
||||
)
|
||||
@@ -298,8 +301,9 @@ automation.register_apply_action(
|
||||
)
|
||||
|
||||
|
||||
automation.register_apply_condition(
|
||||
@automation.register_condition(
|
||||
"display.is_displaying_page",
|
||||
DisplayIsDisplayingPageCondition,
|
||||
cv.maybe_simple_value(
|
||||
{
|
||||
cv.GenerateID(CONF_ID): cv.use_id(Display),
|
||||
@@ -307,8 +311,13 @@ automation.register_apply_condition(
|
||||
},
|
||||
key=CONF_PAGE_ID,
|
||||
),
|
||||
automation.ApplyCall("get_active_page() == {}", ((CONF_PAGE_ID, DisplayPagePtr),)),
|
||||
)
|
||||
async def display_is_displaying_page_to_code(config, condition_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
page = await cg.get_variable(config[CONF_PAGE_ID])
|
||||
var = cg.new_Pvariable(condition_id, template_arg, paren)
|
||||
cg.add(var.set_page(page))
|
||||
return var
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.CORE)
|
||||
|
||||
@@ -848,6 +848,18 @@ template<typename... Ts> class DisplayPageShowAction final : public Action<Ts...
|
||||
}
|
||||
};
|
||||
|
||||
template<typename... Ts> class DisplayIsDisplayingPageCondition final : public Condition<Ts...> {
|
||||
public:
|
||||
DisplayIsDisplayingPageCondition(Display *parent) : parent_(parent) {}
|
||||
|
||||
void set_page(DisplayPage *page) { this->page_ = page; }
|
||||
bool check(const Ts &...x) override { return this->parent_->get_active_page() == this->page_; }
|
||||
|
||||
protected:
|
||||
Display *parent_;
|
||||
DisplayPage *page_;
|
||||
};
|
||||
|
||||
class DisplayOnPageChangeTrigger final : public Trigger<DisplayPage *, DisplayPage *> {
|
||||
public:
|
||||
explicit DisplayOnPageChangeTrigger(Display *parent) { parent->add_on_page_change_trigger(this); }
|
||||
|
||||
@@ -55,6 +55,18 @@ MenuItemSwitch = display_menu_base_ns.class_("MenuItemSwitch")
|
||||
MenuItemCommand = display_menu_base_ns.class_("MenuItemCommand")
|
||||
MenuItemCustom = display_menu_base_ns.class_("MenuItemCustom")
|
||||
|
||||
UpAction = display_menu_base_ns.class_("UpAction", automation.Action)
|
||||
DownAction = display_menu_base_ns.class_("DownAction", automation.Action)
|
||||
LeftAction = display_menu_base_ns.class_("LeftAction", automation.Action)
|
||||
RightAction = display_menu_base_ns.class_("RightAction", automation.Action)
|
||||
EnterAction = display_menu_base_ns.class_("EnterAction", automation.Action)
|
||||
ShowAction = display_menu_base_ns.class_("ShowAction", automation.Action)
|
||||
HideAction = display_menu_base_ns.class_("HideAction", automation.Action)
|
||||
ShowMainAction = display_menu_base_ns.class_("ShowMainAction", automation.Action)
|
||||
|
||||
IsActiveCondition = display_menu_base_ns.class_(
|
||||
"IsActiveCondition", automation.Condition
|
||||
)
|
||||
|
||||
MenuItemType = display_menu_base_ns.enum("MenuItemType")
|
||||
|
||||
@@ -282,23 +294,85 @@ MENU_ACTION_SCHEMA = maybe_simple_id(
|
||||
)
|
||||
|
||||
|
||||
for _name, _call in (
|
||||
("display_menu.up", "up()"),
|
||||
("display_menu.down", "down()"),
|
||||
("display_menu.left", "left()"),
|
||||
("display_menu.right", "right()"),
|
||||
("display_menu.enter", "enter()"),
|
||||
("display_menu.show", "show()"),
|
||||
("display_menu.hide", "hide()"),
|
||||
("display_menu.show_main", "show_main()"),
|
||||
):
|
||||
automation.register_apply_action(
|
||||
_name, MENU_ACTION_SCHEMA, automation.ApplyCall(_call)
|
||||
)
|
||||
|
||||
automation.register_apply_condition(
|
||||
"display_menu.is_active", MENU_ACTION_SCHEMA, "is_active()"
|
||||
@automation.register_action(
|
||||
"display_menu.up", UpAction, MENU_ACTION_SCHEMA, synchronous=True
|
||||
)
|
||||
async def menu_up_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"display_menu.down", DownAction, MENU_ACTION_SCHEMA, synchronous=True
|
||||
)
|
||||
async def menu_down_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"display_menu.left", LeftAction, MENU_ACTION_SCHEMA, synchronous=True
|
||||
)
|
||||
async def menu_left_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"display_menu.right", RightAction, MENU_ACTION_SCHEMA, synchronous=True
|
||||
)
|
||||
async def menu_right_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"display_menu.enter", EnterAction, MENU_ACTION_SCHEMA, synchronous=True
|
||||
)
|
||||
async def menu_enter_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"display_menu.show", ShowAction, MENU_ACTION_SCHEMA, synchronous=True
|
||||
)
|
||||
async def menu_show_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"display_menu.hide", HideAction, MENU_ACTION_SCHEMA, synchronous=True
|
||||
)
|
||||
async def menu_hide_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"display_menu.show_main",
|
||||
ShowMainAction,
|
||||
MENU_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def menu_show_main_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
|
||||
@automation.register_condition(
|
||||
"display_menu.is_active",
|
||||
IsActiveCondition,
|
||||
automation.maybe_simple_id(
|
||||
{
|
||||
cv.GenerateID(CONF_ID): cv.use_id(DisplayMenuComponent),
|
||||
}
|
||||
),
|
||||
)
|
||||
async def display_menu_is_active_to_code(config, condition_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(condition_id, template_arg, paren)
|
||||
|
||||
|
||||
async def menu_item_to_code(menu, config, parent):
|
||||
|
||||
@@ -5,6 +5,94 @@
|
||||
|
||||
namespace esphome::display_menu_base {
|
||||
|
||||
template<typename... Ts> class UpAction final : public Action<Ts...> {
|
||||
public:
|
||||
explicit UpAction(DisplayMenuComponent *menu) : menu_(menu) {}
|
||||
|
||||
void play(const Ts &...x) override { this->menu_->up(); }
|
||||
|
||||
protected:
|
||||
DisplayMenuComponent *menu_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class DownAction final : public Action<Ts...> {
|
||||
public:
|
||||
explicit DownAction(DisplayMenuComponent *menu) : menu_(menu) {}
|
||||
|
||||
void play(const Ts &...x) override { this->menu_->down(); }
|
||||
|
||||
protected:
|
||||
DisplayMenuComponent *menu_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class LeftAction final : public Action<Ts...> {
|
||||
public:
|
||||
explicit LeftAction(DisplayMenuComponent *menu) : menu_(menu) {}
|
||||
|
||||
void play(const Ts &...x) override { this->menu_->left(); }
|
||||
|
||||
protected:
|
||||
DisplayMenuComponent *menu_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class RightAction final : public Action<Ts...> {
|
||||
public:
|
||||
explicit RightAction(DisplayMenuComponent *menu) : menu_(menu) {}
|
||||
|
||||
void play(const Ts &...x) override { this->menu_->right(); }
|
||||
|
||||
protected:
|
||||
DisplayMenuComponent *menu_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class EnterAction final : public Action<Ts...> {
|
||||
public:
|
||||
explicit EnterAction(DisplayMenuComponent *menu) : menu_(menu) {}
|
||||
|
||||
void play(const Ts &...x) override { this->menu_->enter(); }
|
||||
|
||||
protected:
|
||||
DisplayMenuComponent *menu_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class ShowAction final : public Action<Ts...> {
|
||||
public:
|
||||
explicit ShowAction(DisplayMenuComponent *menu) : menu_(menu) {}
|
||||
|
||||
void play(const Ts &...x) override { this->menu_->show(); }
|
||||
|
||||
protected:
|
||||
DisplayMenuComponent *menu_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class HideAction final : public Action<Ts...> {
|
||||
public:
|
||||
explicit HideAction(DisplayMenuComponent *menu) : menu_(menu) {}
|
||||
|
||||
void play(const Ts &...x) override { this->menu_->hide(); }
|
||||
|
||||
protected:
|
||||
DisplayMenuComponent *menu_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class ShowMainAction final : public Action<Ts...> {
|
||||
public:
|
||||
explicit ShowMainAction(DisplayMenuComponent *menu) : menu_(menu) {}
|
||||
|
||||
void play(const Ts &...x) override { this->menu_->show_main(); }
|
||||
|
||||
protected:
|
||||
DisplayMenuComponent *menu_;
|
||||
};
|
||||
template<typename... Ts> class IsActiveCondition final : public Condition<Ts...> {
|
||||
public:
|
||||
explicit IsActiveCondition(DisplayMenuComponent *menu) : menu_(menu) {}
|
||||
bool check(const Ts &...x) override { return this->menu_->is_active(); }
|
||||
|
||||
protected:
|
||||
DisplayMenuComponent *menu_;
|
||||
};
|
||||
|
||||
class DisplayMenuOnEnterTrigger final : public Trigger<const MenuItem *> {
|
||||
public:
|
||||
explicit DisplayMenuOnEnterTrigger(MenuItem *parent) : parent_(parent) {
|
||||
|
||||
@@ -55,4 +55,13 @@ class DS1307Component final : public time::RealTimeClock, public i2c::I2CDevice
|
||||
} ds1307_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class WriteAction final : public Action<Ts...>, public Parented<DS1307Component> {
|
||||
public:
|
||||
void play(const Ts &...x) override { this->parent_->write_time(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class ReadAction final : public Action<Ts...>, public Parented<DS1307Component> {
|
||||
public:
|
||||
void play(const Ts &...x) override { this->parent_->read_time(); }
|
||||
};
|
||||
} // namespace esphome::ds1307
|
||||
@@ -3,12 +3,16 @@ import esphome.codegen as cg
|
||||
from esphome.components import i2c, time
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.core import ID
|
||||
from esphome.cpp_generator import MockObj, TemplateArgsType
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@badbadc0ffee"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
ds1307_ns = cg.esphome_ns.namespace("ds1307")
|
||||
DS1307Component = ds1307_ns.class_("DS1307Component", time.RealTimeClock, i2c.I2CDevice)
|
||||
WriteAction = ds1307_ns.class_("WriteAction", automation.Action)
|
||||
ReadAction = ds1307_ns.class_("ReadAction", automation.Action)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = time.TIME_SCHEMA.extend(
|
||||
@@ -18,25 +22,46 @@ CONFIG_SCHEMA = time.TIME_SCHEMA.extend(
|
||||
).extend(i2c.i2c_device_schema(0x68))
|
||||
|
||||
|
||||
automation.register_apply_action(
|
||||
@automation.register_action(
|
||||
"ds1307.write_time",
|
||||
WriteAction,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(DS1307Component),
|
||||
}
|
||||
),
|
||||
automation.ApplyCall("write_time()"),
|
||||
synchronous=True,
|
||||
)
|
||||
async def ds1307_write_time_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
return var
|
||||
|
||||
automation.register_apply_action(
|
||||
|
||||
@automation.register_action(
|
||||
"ds1307.read_time",
|
||||
ReadAction,
|
||||
automation.maybe_simple_id(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(DS1307Component),
|
||||
}
|
||||
),
|
||||
automation.ApplyCall("read_time()"),
|
||||
synchronous=True,
|
||||
)
|
||||
async def ds1307_read_time_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
return var
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <cinttypes>
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
@@ -46,4 +47,27 @@ class DutyTimeSensor final : public sensor::Sensor, public PollingComponent {
|
||||
bool restore_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class BaseAction : public Action<Ts...>, public Parented<DutyTimeSensor> {};
|
||||
|
||||
template<typename... Ts> class StartAction : public BaseAction<Ts...> {
|
||||
void play(const Ts &...x) override { this->parent_->start(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class StopAction : public BaseAction<Ts...> {
|
||||
void play(const Ts &...x) override { this->parent_->stop(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class ResetAction : public BaseAction<Ts...> {
|
||||
void play(const Ts &...x) override { this->parent_->reset(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class RunningCondition final : public Condition<Ts...>, public Parented<DutyTimeSensor> {
|
||||
public:
|
||||
explicit RunningCondition(DutyTimeSensor *parent, bool state) : Parented(parent), state_(state) {}
|
||||
|
||||
protected:
|
||||
bool check(const Ts &...x) override { return this->parent_->is_running() == this->state_; }
|
||||
bool state_;
|
||||
};
|
||||
|
||||
} // namespace esphome::duty_time_sensor
|
||||
@@ -1,4 +1,10 @@
|
||||
from esphome import automation
|
||||
from esphome.automation import (
|
||||
Action,
|
||||
Condition,
|
||||
maybe_simple_id,
|
||||
register_action,
|
||||
register_condition,
|
||||
)
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import binary_sensor, sensor
|
||||
import esphome.config_validation as cv
|
||||
@@ -13,6 +19,8 @@ from esphome.const import (
|
||||
STATE_CLASS_TOTAL_INCREASING,
|
||||
UNIT_SECOND,
|
||||
)
|
||||
from esphome.core import ID
|
||||
from esphome.cpp_generator import MockObj, TemplateArgsType
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CONF_LAST_TIME = "last_time"
|
||||
@@ -21,6 +29,14 @@ duty_time_sensor_ns = cg.esphome_ns.namespace("duty_time_sensor")
|
||||
DutyTimeSensor = duty_time_sensor_ns.class_(
|
||||
"DutyTimeSensor", sensor.Sensor, cg.PollingComponent
|
||||
)
|
||||
BaseAction = duty_time_sensor_ns.class_("BaseAction", Action, cg.Parented)
|
||||
StartAction = duty_time_sensor_ns.class_("StartAction", BaseAction)
|
||||
StopAction = duty_time_sensor_ns.class_("StopAction", BaseAction)
|
||||
ResetAction = duty_time_sensor_ns.class_("ResetAction", BaseAction)
|
||||
SetAction = duty_time_sensor_ns.class_("SetAction", BaseAction)
|
||||
RunningCondition = duty_time_sensor_ns.class_(
|
||||
"RunningCondition", Condition, cg.Parented
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
@@ -70,25 +86,76 @@ async def to_code(config: ConfigType) -> None:
|
||||
|
||||
# AUTOMATIONS
|
||||
|
||||
DUTY_TIME_ID_SCHEMA = automation.maybe_simple_id(
|
||||
DUTY_TIME_ID_SCHEMA = maybe_simple_id(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(DutyTimeSensor),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
for _name, _call in (
|
||||
("sensor.duty_time.start", "start()"),
|
||||
("sensor.duty_time.stop", "stop()"),
|
||||
("sensor.duty_time.reset", "reset()"),
|
||||
):
|
||||
automation.register_apply_action(
|
||||
_name, DUTY_TIME_ID_SCHEMA, automation.ApplyCall(_call)
|
||||
)
|
||||
@register_action(
|
||||
"sensor.duty_time.start", StartAction, DUTY_TIME_ID_SCHEMA, synchronous=True
|
||||
)
|
||||
async def sensor_runtime_start_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
return var
|
||||
|
||||
automation.register_apply_condition(
|
||||
"sensor.duty_time.is_running", DUTY_TIME_ID_SCHEMA, "is_running()"
|
||||
|
||||
@register_action(
|
||||
"sensor.duty_time.stop", StopAction, DUTY_TIME_ID_SCHEMA, synchronous=True
|
||||
)
|
||||
automation.register_apply_condition(
|
||||
"sensor.duty_time.is_not_running", DUTY_TIME_ID_SCHEMA, "is_running() == false"
|
||||
async def sensor_runtime_stop_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
return var
|
||||
|
||||
|
||||
@register_action(
|
||||
"sensor.duty_time.reset", ResetAction, DUTY_TIME_ID_SCHEMA, synchronous=True
|
||||
)
|
||||
async def sensor_runtime_reset_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
return var
|
||||
|
||||
|
||||
@register_condition(
|
||||
"sensor.duty_time.is_running", RunningCondition, DUTY_TIME_ID_SCHEMA
|
||||
)
|
||||
async def duty_time_is_running_to_code(
|
||||
config: ConfigType,
|
||||
condition_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(condition_id, template_arg, paren, True)
|
||||
|
||||
|
||||
@register_condition(
|
||||
"sensor.duty_time.is_not_running", RunningCondition, DUTY_TIME_ID_SCHEMA
|
||||
)
|
||||
async def duty_time_is_not_running_to_code(
|
||||
config: ConfigType,
|
||||
condition_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(condition_id, template_arg, paren, False)
|
||||
@@ -1,6 +1,5 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c
|
||||
from esphome.components.const import CONF_CONVERSION_RATE
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_INVERTED, CONF_RESOLUTION
|
||||
from esphome.types import ConfigType
|
||||
@@ -14,6 +13,7 @@ MULTI_CONF = True
|
||||
CONF_PWM = "pwm"
|
||||
CONF_DIVIDER = "divider"
|
||||
CONF_DAC = "dac"
|
||||
CONF_CONVERSION_RATE = "conversion_rate"
|
||||
|
||||
CONF_EMC2101_ID = "emc2101_id"
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ from esphome.const import (
|
||||
CONF_RX_BUFFER_SIZE,
|
||||
CONF_UART_ID,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.core import CORE, ID
|
||||
from esphome.cpp_generator import MockObj, TemplateArgsType
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
@@ -22,6 +23,9 @@ DEPENDENCIES = ["uart"]
|
||||
emontx_ns = cg.esphome_ns.namespace("emontx")
|
||||
EmonTx = emontx_ns.class_("EmonTx", cg.Component, uart.UARTDevice)
|
||||
|
||||
# Action to send command to emonTx
|
||||
EmonTxSendCommandAction = emontx_ns.class_("EmonTxSendCommandAction", automation.Action)
|
||||
|
||||
CONF_EMONTX_ID = "emontx_id"
|
||||
CONF_TAG_NAME = "tag_name"
|
||||
CONF_ON_JSON = "on_json"
|
||||
@@ -135,16 +139,20 @@ EMONTX_SEND_COMMAND_ACTION_SCHEMA = cv.Schema(
|
||||
)
|
||||
|
||||
|
||||
def _plain_literal(config: ConfigType, value: str) -> str:
|
||||
return str(cg.safe_exp(value))
|
||||
|
||||
|
||||
automation.register_apply_action(
|
||||
@automation.register_action(
|
||||
"emontx.send_command",
|
||||
EmonTxSendCommandAction,
|
||||
EMONTX_SEND_COMMAND_ACTION_SCHEMA,
|
||||
# A constant is a plain literal for the const char * overload; a lambda returns a
|
||||
# std::string and takes the inline overload.
|
||||
automation.ApplyField(
|
||||
CONF_COMMAND, "send_command", cg.std_string, const_fn=_plain_literal
|
||||
),
|
||||
synchronous=True,
|
||||
)
|
||||
async def emontx_send_command_action_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
template_ = await cg.templatable(config[CONF_COMMAND], args, cg.std_string)
|
||||
cg.add(var.set_command(template_))
|
||||
return var
|
||||
@@ -94,9 +94,9 @@ void EmonTx::dump_config() {
|
||||
*
|
||||
* @param command The command string to send (LF will be appended automatically).
|
||||
*/
|
||||
void EmonTx::send_command(const char *command) {
|
||||
ESP_LOGD(TAG, "Sending command to emonTx: %s", command);
|
||||
this->write_str(command);
|
||||
void EmonTx::send_command(const std::string &command) {
|
||||
ESP_LOGD(TAG, "Sending command to emonTx: %s", command.c_str());
|
||||
this->write_str(command.c_str());
|
||||
this->write_byte('\n');
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
@@ -38,8 +39,7 @@ class EmonTx final : public Component, public uart::UARTDevice {
|
||||
template<typename F> void add_on_data_callback(F &&callback) { this->data_callbacks_.add(std::forward<F>(callback)); }
|
||||
|
||||
// Send command to emonTx via UART
|
||||
void send_command(const char *command);
|
||||
void send_command(const std::string &command) { this->send_command(command.c_str()); }
|
||||
void send_command(const std::string &command);
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
void init_sensors(size_t count) { this->sensors_.init(count); }
|
||||
@@ -58,4 +58,12 @@ class EmonTx final : public Component, public uart::UARTDevice {
|
||||
std::array<char, MAX_LINE_LENGTH + 1> buffer_{};
|
||||
};
|
||||
|
||||
// Action to send command to emonTx
|
||||
template<typename... Ts> class EmonTxSendCommandAction final : public Action<Ts...>, public Parented<EmonTx> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(std::string, command)
|
||||
|
||||
void play(const Ts &...x) override { this->parent_->send_command(this->command_.value(x...)); }
|
||||
};
|
||||
|
||||
} // namespace esphome::emontx
|
||||
@@ -11,10 +11,7 @@ from typing import Any
|
||||
|
||||
from esphome import yaml_util
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const import (
|
||||
CONF_ENABLE_OTA_DOWNGRADE_PROTECTION,
|
||||
CONF_IGNORE_NOT_FOUND,
|
||||
)
|
||||
from esphome.components.const import CONF_ENABLE_OTA_DOWNGRADE_PROTECTION
|
||||
from esphome.config_helpers import filter_source_files_from_defines
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
@@ -114,7 +111,6 @@ CONF_ENGINEERING_SAMPLE = "engineering_sample"
|
||||
CONF_INCLUDE_BUILTIN_IDF_COMPONENTS = "include_builtin_idf_components"
|
||||
CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert"
|
||||
CONF_EXECUTE_FROM_PSRAM = "execute_from_psram"
|
||||
CONF_NVS_CACHE_IN_PSRAM = "nvs_cache_in_psram"
|
||||
CONF_FLASH_CHIP = "flash_chip"
|
||||
CONF_KEY_ID = "key_id"
|
||||
CONF_MINIMUM_CHIP_REVISION = "minimum_chip_revision"
|
||||
@@ -1599,29 +1595,6 @@ def final_validate(config) -> None:
|
||||
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_EXECUTE_FROM_PSRAM],
|
||||
)
|
||||
)
|
||||
if advanced.get(CONF_NVS_CACHE_IN_PSRAM):
|
||||
psram_conf = full_config.get(PSRAM_DOMAIN)
|
||||
if (
|
||||
psram_conf is None
|
||||
or psram_conf[CONF_DISABLED]
|
||||
or psram_conf[CONF_IGNORE_NOT_FOUND]
|
||||
):
|
||||
errs.append(
|
||||
cv.Invalid(
|
||||
f"'{CONF_NVS_CACHE_IN_PSRAM}' requires PSRAM with 'ignore_not_found: false'",
|
||||
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_NVS_CACHE_IN_PSRAM],
|
||||
)
|
||||
)
|
||||
if (
|
||||
advanced.get(CONF_NVS_ENCRYPTION) is not None
|
||||
or conf_fw[CONF_SDKCONFIG_OPTIONS].get("CONFIG_NVS_ENCRYPTION") == "y"
|
||||
):
|
||||
errs.append(
|
||||
cv.Invalid(
|
||||
f"'{CONF_NVS_CACHE_IN_PSRAM}' cannot be used with NVS encryption; the keys must stay in internal RAM",
|
||||
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_NVS_CACHE_IN_PSRAM],
|
||||
)
|
||||
)
|
||||
|
||||
final_validate_pins(full_config)
|
||||
|
||||
@@ -2062,7 +2035,6 @@ FRAMEWORK_SCHEMA = cv.Schema(
|
||||
cv.Optional(CONF_RINGBUF_IN_IRAM, default=False): cv.boolean,
|
||||
cv.Optional(CONF_HEAP_IN_IRAM, default=False): cv.boolean,
|
||||
cv.Optional(CONF_EXECUTE_FROM_PSRAM, default=False): cv.boolean,
|
||||
cv.Optional(CONF_NVS_CACHE_IN_PSRAM): cv.boolean,
|
||||
cv.Optional(CONF_LOOP_TASK_STACK_SIZE, default=8192): cv.int_range(
|
||||
min=8192, max=32768
|
||||
),
|
||||
@@ -2389,20 +2361,6 @@ async def _set_libc_picolibc_newlib_compat() -> None:
|
||||
)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
async def _apply_nvs_cache_in_psram(explicit: bool) -> None:
|
||||
"""Keep the NVS cache in PSRAM unless NVS encryption is on, however it was enabled."""
|
||||
# The encrypted partition object holds the derived keys, which must stay in internal RAM
|
||||
if is_idf_sdkconfig_option_enabled("CONFIG_NVS_ENCRYPTION"):
|
||||
if explicit:
|
||||
_LOGGER.warning(
|
||||
"%s ignored: NVS encryption keeps the NVS cache in internal RAM",
|
||||
CONF_NVS_CACHE_IN_PSRAM,
|
||||
)
|
||||
return
|
||||
set_idf_sdkconfig_default("CONFIG_NVS_ALLOCATE_CACHE_IN_SPIRAM", True)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
async def _reconcile_certificate_bundle_sdkconfig() -> None:
|
||||
"""Enable the mbedTLS certificate bundle only when something asked for it.
|
||||
@@ -2955,17 +2913,6 @@ async def to_code(config):
|
||||
if advanced[CONF_EXECUTE_FROM_PSRAM]:
|
||||
add_idf_sdkconfig_option("CONFIG_SPIRAM_XIP_FROM_PSRAM", True)
|
||||
|
||||
# Imported here as psram imports this module
|
||||
from esphome.components.psram import is_guaranteed as psram_is_guaranteed
|
||||
|
||||
# Frees internal heap (the cache scales with the NVS partition) but slows NVS, so only
|
||||
# where PSRAM is known to be fitted. Decided at FINAL so every way of enabling NVS
|
||||
# encryption has been seen and a user's sdkconfig_options value wins.
|
||||
# Unset means on; only an explicit true is worth a warning when it has to be dropped.
|
||||
requested = advanced.get(CONF_NVS_CACHE_IN_PSRAM)
|
||||
if requested is not False and psram_is_guaranteed():
|
||||
CORE.add_job(_apply_nvs_cache_in_psram, requested is True)
|
||||
|
||||
# Apply LWIP core locking for better socket performance
|
||||
# This is already enabled by default in Arduino framework, where it provides
|
||||
# significant performance benefits. Our benchmarks show socket operations are
|
||||
|
||||
@@ -32,7 +32,8 @@ from esphome.const import (
|
||||
CONF_NAME,
|
||||
CONF_NAME_ADD_MAC_SUFFIX,
|
||||
)
|
||||
from esphome.core import CORE, TimePeriod
|
||||
from esphome.core import CORE, ID, TimePeriod
|
||||
from esphome.cpp_generator import MockObj, TemplateArgsType
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
@@ -605,27 +606,38 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add_define("USE_ESP32_BLE_UUID")
|
||||
|
||||
|
||||
automation.register_bare_condition(
|
||||
"ble.enabled",
|
||||
BLEEnabledCondition,
|
||||
cv.Schema({}),
|
||||
@automation.register_condition("ble.enabled", BLEEnabledCondition, cv.Schema({}))
|
||||
async def ble_enabled_to_code(
|
||||
config: ConfigType,
|
||||
condition_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
return cg.new_Pvariable(condition_id, template_arg)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"ble.enable", BLEEnableAction, cv.Schema({}), synchronous=True
|
||||
)
|
||||
async def ble_enable_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
return cg.new_Pvariable(action_id, template_arg)
|
||||
|
||||
|
||||
automation.register_bare_action(
|
||||
"ble.enable",
|
||||
BLEEnableAction,
|
||||
cv.Schema({}),
|
||||
synchronous=True,
|
||||
)
|
||||
|
||||
|
||||
automation.register_bare_action(
|
||||
"ble.disable",
|
||||
BLEDisableAction,
|
||||
cv.Schema({}),
|
||||
synchronous=True,
|
||||
@automation.register_action(
|
||||
"ble.disable", BLEDisableAction, cv.Schema({}), synchronous=True
|
||||
)
|
||||
async def ble_disable_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
return cg.new_Pvariable(action_id, template_arg)
|
||||
|
||||
|
||||
# ble_advertising.cpp is fully #ifdef'd on USE_ESP32_BLE_ADVERTISING, set
|
||||
|
||||
Loaded 100 of 739 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user