mirror of
https://github.com/esphome/esphome.git
synced 2026-06-26 22:45:00 +00:00
Compare commits
1 Commits
test-devic
...
api-server
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
acc15ff495 |
33
.clang-tidy
33
.clang-tidy
@@ -5,30 +5,24 @@ Checks: >-
|
||||
-altera-*,
|
||||
-android-*,
|
||||
-boost-*,
|
||||
-bugprone-derived-method-shadowing-base-method,
|
||||
-bugprone-easily-swappable-parameters,
|
||||
-bugprone-implicit-widening-of-multiplication-result,
|
||||
-bugprone-invalid-enum-default-initialization,
|
||||
-bugprone-multi-level-implicit-pointer-conversion,
|
||||
-bugprone-narrowing-conversions,
|
||||
-bugprone-tagged-union-member-count,
|
||||
-bugprone-signed-char-misuse,
|
||||
-bugprone-switch-missing-default-case,
|
||||
-cert-dcl50-cpp,
|
||||
-cert-err33-c,
|
||||
-cert-err58-cpp,
|
||||
-cert-int09-c,
|
||||
-cert-oop57-cpp,
|
||||
-cert-str34-c,
|
||||
-clang-analyzer-optin.core.EnumCastOutOfRange,
|
||||
-clang-analyzer-optin.cplusplus.UninitializedObject,
|
||||
-clang-analyzer-osx.*,
|
||||
-clang-analyzer-security.ArrayBound,
|
||||
-clang-diagnostic-delete-abstract-non-virtual-dtor,
|
||||
-clang-diagnostic-delete-non-abstract-non-virtual-dtor,
|
||||
-clang-diagnostic-deprecated-declarations,
|
||||
-clang-diagnostic-ignored-optimization-argument,
|
||||
-clang-diagnostic-missing-designated-field-initializers,
|
||||
-clang-diagnostic-missing-field-initializers,
|
||||
-clang-diagnostic-shadow-field,
|
||||
-clang-diagnostic-unused-const-variable,
|
||||
@@ -48,7 +42,6 @@ Checks: >-
|
||||
-cppcoreguidelines-owning-memory,
|
||||
-cppcoreguidelines-prefer-member-initializer,
|
||||
-cppcoreguidelines-pro-bounds-array-to-pointer-decay,
|
||||
-cppcoreguidelines-pro-bounds-avoid-unchecked-container-access,
|
||||
-cppcoreguidelines-pro-bounds-constant-array-index,
|
||||
-cppcoreguidelines-pro-bounds-pointer-arithmetic,
|
||||
-cppcoreguidelines-pro-type-const-cast,
|
||||
@@ -61,13 +54,12 @@ Checks: >-
|
||||
-cppcoreguidelines-rvalue-reference-param-not-moved,
|
||||
-cppcoreguidelines-special-member-functions,
|
||||
-cppcoreguidelines-use-default-member-init,
|
||||
-cppcoreguidelines-use-enum-class,
|
||||
-cppcoreguidelines-virtual-class-destructor,
|
||||
-fuchsia-default-arguments-calls,
|
||||
-fuchsia-default-arguments-declarations,
|
||||
-fuchsia-multiple-inheritance,
|
||||
-fuchsia-overloaded-operator,
|
||||
-fuchsia-statically-constructed-objects,
|
||||
-fuchsia-default-arguments-declarations,
|
||||
-fuchsia-default-arguments-calls,
|
||||
-google-build-using-namespace,
|
||||
-google-explicit-constructor,
|
||||
-google-readability-braces-around-statements,
|
||||
@@ -79,23 +71,16 @@ Checks: >-
|
||||
-llvm-else-after-return,
|
||||
-llvm-header-guard,
|
||||
-llvm-include-order,
|
||||
-llvm-prefer-static-over-anonymous-namespace,
|
||||
-llvm-qualified-auto,
|
||||
-llvm-use-ranges,
|
||||
-llvmlibc-*,
|
||||
-misc-const-correctness,
|
||||
-misc-include-cleaner,
|
||||
-misc-multiple-inheritance,
|
||||
-misc-no-recursion,
|
||||
-misc-non-private-member-variables-in-classes,
|
||||
-misc-override-with-different-visibility,
|
||||
-misc-unused-parameters,
|
||||
-misc-use-anonymous-namespace,
|
||||
-misc-use-internal-linkage,
|
||||
-modernize-avoid-bind,
|
||||
-modernize-avoid-variadic-functions,
|
||||
-modernize-avoid-c-arrays,
|
||||
-modernize-avoid-c-style-cast,
|
||||
-modernize-concat-nested-namespaces,
|
||||
-modernize-macro-to-enum,
|
||||
-modernize-return-braced-init-list,
|
||||
@@ -103,42 +88,32 @@ Checks: >-
|
||||
-modernize-use-auto,
|
||||
-modernize-use-constraints,
|
||||
-modernize-use-default-member-init,
|
||||
-modernize-use-designated-initializers,
|
||||
-modernize-use-equals-default,
|
||||
-modernize-use-integer-sign-comparison,
|
||||
-modernize-use-nodiscard,
|
||||
-modernize-use-nullptr,
|
||||
-modernize-use-ranges,
|
||||
-modernize-use-nodiscard,
|
||||
-modernize-use-nullptr,
|
||||
-modernize-use-trailing-return-type,
|
||||
-mpi-*,
|
||||
-objc-*,
|
||||
-performance-enum-size,
|
||||
-portability-avoid-pragma-once,
|
||||
-portability-template-virtual-member-function,
|
||||
-readability-ambiguous-smartptr-reset-call,
|
||||
-readability-avoid-nested-conditional-operator,
|
||||
-readability-container-contains,
|
||||
-readability-container-data-pointer,
|
||||
-readability-convert-member-functions-to-static,
|
||||
-readability-else-after-return,
|
||||
-readability-enum-initial-value,
|
||||
-readability-function-cognitive-complexity,
|
||||
-readability-implicit-bool-conversion,
|
||||
-readability-isolate-declaration,
|
||||
-readability-magic-numbers,
|
||||
-readability-make-member-function-const,
|
||||
-readability-math-missing-parentheses,
|
||||
-readability-named-parameter,
|
||||
-readability-redundant-casting,
|
||||
-readability-redundant-inline-specifier,
|
||||
-readability-redundant-member-init,
|
||||
-readability-redundant-parentheses,
|
||||
-readability-redundant-string-init,
|
||||
-readability-redundant-typename,
|
||||
-readability-uppercase-literal-suffix,
|
||||
-readability-use-anyofallof,
|
||||
-readability-use-std-min-max,
|
||||
-readability-use-concise-preprocessor-directives,
|
||||
WarningsAsErrors: '*'
|
||||
FormatStyle: google
|
||||
CheckOptions:
|
||||
|
||||
@@ -1 +1 @@
|
||||
0c7f309d70eca8e3efd510092ddb23c530f3934c49371717efa124b788d761f8
|
||||
1b1ce6324c50c4595703c7df0a8a479b4fe84b71ff1a8793cce1a16f17a33324
|
||||
|
||||
4
.github/workflows/auto-label-pr.yml
vendored
4
.github/workflows/auto-label-pr.yml
vendored
@@ -27,9 +27,9 @@ jobs:
|
||||
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v2
|
||||
with:
|
||||
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
|
||||
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Auto Label PR
|
||||
|
||||
115
.github/workflows/ci.yml
vendored
115
.github/workflows/ci.yml
vendored
@@ -39,7 +39,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Generate cache-key
|
||||
id: cache-key
|
||||
run: echo key="${{ hashFiles('requirements.txt', 'requirements_dev.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
|
||||
run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
python -m venv venv
|
||||
. venv/bin/activate
|
||||
python --version
|
||||
pip install -r requirements.txt -r requirements_dev.txt -r requirements_test.txt pre-commit
|
||||
pip install -r requirements.txt -r requirements_test.txt pre-commit
|
||||
pip install -e .
|
||||
|
||||
pylint:
|
||||
@@ -108,81 +108,6 @@ jobs:
|
||||
script/generate-esp32-boards.py --check
|
||||
script/generate-rp2040-boards.py --check
|
||||
|
||||
import-time:
|
||||
name: Check import esphome.__main__ time
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- common
|
||||
- determine-jobs
|
||||
if: needs.determine-jobs.outputs.import-time == 'true'
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Check import time against budget and write waterfall HAR
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
script/check_import_time.py --check --har importtime.har
|
||||
- name: Upload waterfall HAR
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: import-time-waterfall
|
||||
path: importtime.har
|
||||
if-no-files-found: ignore
|
||||
retention-days: 14
|
||||
|
||||
device-builder:
|
||||
name: Test downstream esphome/device-builder
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- common
|
||||
- determine-jobs
|
||||
if: needs.determine-jobs.outputs.device-builder == 'true'
|
||||
steps:
|
||||
- name: Check out esphome (this PR)
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
path: esphome
|
||||
- name: Check out esphome/device-builder
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
repository: esphome/device-builder
|
||||
ref: main
|
||||
path: device-builder
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- name: Set up uv
|
||||
# Mirrors the install shape device-builder's own CI uses
|
||||
# (esphome/device-builder#192): uv replaces pip for the
|
||||
# install step (order-of-magnitude faster on cold boots,
|
||||
# with its own wheel cache). actions/setup-python still
|
||||
# provides the interpreter.
|
||||
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||
with:
|
||||
enable-cache: true
|
||||
- name: Install device-builder + esphome from PR
|
||||
# Install device-builder with its esphome + test extras
|
||||
# first so its pinned versions of pytest/etc. land, then
|
||||
# overlay the PR's esphome so the downstream tests run
|
||||
# against this PR's Python code. ``--system`` installs into
|
||||
# the runner's Python instead of a venv.
|
||||
run: |
|
||||
uv pip install --system -e './device-builder[esphome,test]'
|
||||
uv pip install --system -e ./esphome
|
||||
- name: Run device-builder pytest
|
||||
# ``-n auto`` runs under pytest-xdist (matches device-builder's
|
||||
# own CI). No ``--cov`` here -- this is purely a downstream
|
||||
# smoke check against this PR's esphome code.
|
||||
working-directory: device-builder
|
||||
run: pytest -q -n auto --maxfail=5 --durations=10 --no-cov --ignore=tests/benchmarks
|
||||
|
||||
pytest:
|
||||
name: Run pytest
|
||||
strategy:
|
||||
@@ -246,12 +171,11 @@ jobs:
|
||||
- common
|
||||
outputs:
|
||||
integration-tests: ${{ steps.determine.outputs.integration-tests }}
|
||||
integration-test-buckets: ${{ steps.determine.outputs.integration-test-buckets }}
|
||||
integration-tests-run-all: ${{ steps.determine.outputs.integration-tests-run-all }}
|
||||
integration-test-files: ${{ steps.determine.outputs.integration-test-files }}
|
||||
clang-tidy: ${{ steps.determine.outputs.clang-tidy }}
|
||||
clang-tidy-mode: ${{ steps.determine.outputs.clang-tidy-mode }}
|
||||
python-linters: ${{ steps.determine.outputs.python-linters }}
|
||||
import-time: ${{ steps.determine.outputs.import-time }}
|
||||
device-builder: ${{ steps.determine.outputs.device-builder }}
|
||||
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 }}
|
||||
@@ -290,12 +214,11 @@ jobs:
|
||||
|
||||
# Extract individual fields
|
||||
echo "integration-tests=$(echo "$output" | jq -r '.integration_tests')" >> $GITHUB_OUTPUT
|
||||
echo "integration-test-buckets=$(echo "$output" | jq -c '.integration_test_buckets')" >> $GITHUB_OUTPUT
|
||||
echo "integration-tests-run-all=$(echo "$output" | jq -r '.integration_tests_run_all')" >> $GITHUB_OUTPUT
|
||||
echo "integration-test-files=$(echo "$output" | jq -c '.integration_test_files')" >> $GITHUB_OUTPUT
|
||||
echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT
|
||||
echo "clang-tidy-mode=$(echo "$output" | jq -r '.clang_tidy_mode')" >> $GITHUB_OUTPUT
|
||||
echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT
|
||||
echo "import-time=$(echo "$output" | jq -r '.import_time')" >> $GITHUB_OUTPUT
|
||||
echo "device-builder=$(echo "$output" | jq -r '.device_builder')" >> $GITHUB_OUTPUT
|
||||
echo "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
|
||||
@@ -314,16 +237,12 @@ jobs:
|
||||
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
|
||||
|
||||
integration-tests:
|
||||
name: Run integration tests (${{ matrix.bucket.name }})
|
||||
name: Run integration tests
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- common
|
||||
- determine-jobs
|
||||
if: needs.determine-jobs.outputs.integration-tests == 'true'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
bucket: ${{ fromJson(needs.determine-jobs.outputs.integration-test-buckets) }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
@@ -350,14 +269,19 @@ jobs:
|
||||
run: echo "::add-matcher::.github/workflows/matchers/pytest.json"
|
||||
- name: Run integration tests
|
||||
env:
|
||||
# JSON array of test paths; parsed into a bash array below to avoid
|
||||
# shell word-splitting / glob hazards.
|
||||
BUCKET_TESTS: ${{ toJson(matrix.bucket.tests) }}
|
||||
INTEGRATION_TEST_FILES: ${{ needs.determine-jobs.outputs.integration-test-files }}
|
||||
INTEGRATION_TESTS_RUN_ALL: ${{ needs.determine-jobs.outputs.integration-tests-run-all }}
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
mapfile -t test_files < <(echo "$BUCKET_TESTS" | jq -r '.[]')
|
||||
echo "Bucket ${{ matrix.bucket.name }}: running ${#test_files[@]} integration tests"
|
||||
pytest -vv --no-cov --tb=native -n auto "${test_files[@]}"
|
||||
if [[ "$INTEGRATION_TESTS_RUN_ALL" == "true" ]]; then
|
||||
echo "Running all integration tests"
|
||||
pytest -vv --no-cov --tb=native -n auto tests/integration/
|
||||
else
|
||||
# Parse JSON array into bash array to avoid shell expansion issues
|
||||
mapfile -t test_files < <(echo "$INTEGRATION_TEST_FILES" | jq -r '.[]')
|
||||
echo "Running ${#test_files[@]} specific integration tests"
|
||||
pytest -vv --no-cov --tb=native -n auto "${test_files[@]}"
|
||||
fi
|
||||
|
||||
cpp-unit-tests:
|
||||
name: Run C++ unit tests
|
||||
@@ -415,7 +339,7 @@ jobs:
|
||||
echo "binary=$BINARY" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Run CodSpeed benchmarks
|
||||
uses: CodSpeedHQ/action@c381be0bfd20e844fb45594f6aa182ffcd94545c # v4.15.0
|
||||
uses: CodSpeedHQ/action@658a901452bb54c799643e060733b7afe9121b8d # v4.14.0
|
||||
with:
|
||||
run: ${{ steps.build.outputs.binary }}
|
||||
mode: simulation
|
||||
@@ -1112,7 +1036,6 @@ jobs:
|
||||
- clang-tidy-nosplit
|
||||
- clang-tidy-split
|
||||
- determine-jobs
|
||||
- device-builder
|
||||
- test-build-components-split
|
||||
- pre-commit-ci-lite
|
||||
- memory-impact-target-branch
|
||||
|
||||
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -58,7 +58,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
||||
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
@@ -86,6 +86,6 @@ jobs:
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
||||
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
2
.github/workflows/lock.yml
vendored
2
.github/workflows/lock.yml
vendored
@@ -8,4 +8,4 @@ on:
|
||||
|
||||
jobs:
|
||||
lock:
|
||||
uses: esphome/workflows/.github/workflows/lock.yml@025a1e6255610c498ed590403b7e510b69e474df # 2026.4.1
|
||||
uses: esphome/workflows/.github/workflows/lock.yml@3c4e8446aa1029f1c346a482034b3ee1489077ca # 2026.4.0
|
||||
|
||||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -223,7 +223,7 @@ jobs:
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||
with:
|
||||
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
|
||||
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
owner: esphome
|
||||
repositories: home-assistant-addon
|
||||
@@ -258,7 +258,7 @@ jobs:
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||
with:
|
||||
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
|
||||
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
owner: esphome
|
||||
repositories: esphome-schema
|
||||
@@ -289,7 +289,7 @@ jobs:
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||
with:
|
||||
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
|
||||
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
owner: esphome
|
||||
repositories: version-notifier
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -146,6 +146,5 @@ sdkconfig.*
|
||||
|
||||
/components
|
||||
/managed_components
|
||||
/dependencies.lock
|
||||
|
||||
api-docs/
|
||||
|
||||
@@ -347,7 +347,6 @@ esphome/components/modbus_controller/select/* @martgras @stegm
|
||||
esphome/components/modbus_controller/sensor/* @martgras
|
||||
esphome/components/modbus_controller/switch/* @martgras
|
||||
esphome/components/modbus_controller/text_sensor/* @martgras
|
||||
esphome/components/modbus_server/* @exciton
|
||||
esphome/components/mopeka_ble/* @Fabian-Schmidt @spbrogan
|
||||
esphome/components/mopeka_pro_check/* @spbrogan
|
||||
esphome/components/mopeka_std_check/* @Fabian-Schmidt
|
||||
|
||||
@@ -21,7 +21,7 @@ import argcomplete
|
||||
# Note: Do not import modules from esphome.components here, as this would
|
||||
# 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
|
||||
from esphome import const, writer, yaml_util
|
||||
import esphome.codegen as cg
|
||||
from esphome.config import iter_component_configs, read_config, strip_default_ids
|
||||
from esphome.const import (
|
||||
@@ -72,12 +72,7 @@ from esphome.util import (
|
||||
run_external_process,
|
||||
safe_print,
|
||||
)
|
||||
|
||||
# Keep expensive imports (zeroconf, writer, yaml_util, etc.) out of this
|
||||
# module's top level. Every `esphome` invocation — including fast paths
|
||||
# like `esphome version` — pays the cost of what's imported here before
|
||||
# any command runs. Import inside the function that needs it instead.
|
||||
# `script/check_import_time.py` enforces a budget in CI.
|
||||
from esphome.zeroconf import discover_mdns_devices
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -246,8 +241,6 @@ def _discover_mac_suffix_devices() -> list[str] | None:
|
||||
"""
|
||||
if not (has_name_add_mac_suffix() and has_mdns() and has_non_ip_address()):
|
||||
return None
|
||||
from esphome.zeroconf import discover_mdns_devices
|
||||
|
||||
_LOGGER.info("Discovering devices...")
|
||||
if not (discovered := discover_mdns_devices(CORE.name)):
|
||||
_LOGGER.warning(
|
||||
@@ -667,7 +660,7 @@ def run_miniterm(config: ConfigType, port: str, args) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def _wrap_to_code(name, comp, yaml_util):
|
||||
def wrap_to_code(name, comp):
|
||||
coro = coroutine(comp.to_code)
|
||||
|
||||
@functools.wraps(comp.to_code)
|
||||
@@ -687,8 +680,6 @@ def _wrap_to_code(name, comp, yaml_util):
|
||||
|
||||
|
||||
def write_cpp(config: ConfigType, native_idf: bool = False) -> int:
|
||||
from esphome import writer
|
||||
|
||||
if not get_bool_env(ENV_NOGITIGNORE):
|
||||
writer.write_gitignore()
|
||||
|
||||
@@ -700,21 +691,17 @@ def write_cpp(config: ConfigType, native_idf: bool = False) -> int:
|
||||
|
||||
|
||||
def generate_cpp_contents(config: ConfigType) -> None:
|
||||
from esphome import yaml_util
|
||||
|
||||
_LOGGER.info("Generating C++ source...")
|
||||
|
||||
for name, component, conf in iter_component_configs(CORE.config):
|
||||
if component.to_code is not None:
|
||||
coro = _wrap_to_code(name, component, yaml_util)
|
||||
coro = wrap_to_code(name, component)
|
||||
CORE.add_job(coro, conf)
|
||||
|
||||
CORE.flush_tasks()
|
||||
|
||||
|
||||
def write_cpp_file(native_idf: bool = False) -> int:
|
||||
from esphome import writer
|
||||
|
||||
code_s = indent(CORE.cpp_main_section)
|
||||
writer.write_cpp(code_s)
|
||||
|
||||
@@ -1125,16 +1112,15 @@ def upload_program(
|
||||
|
||||
remote_port = int(ota_conf[CONF_PORT])
|
||||
password = ota_conf.get(CONF_PASSWORD)
|
||||
if getattr(args, "file", None) is not None:
|
||||
binary = Path(args.file)
|
||||
else:
|
||||
binary = CORE.firmware_bin
|
||||
|
||||
# Resolve MQTT magic strings to actual IP addresses
|
||||
network_devices = _resolve_network_devices(devices, config, args)
|
||||
|
||||
binary = CORE.firmware_bin
|
||||
ota_type = espota2.OTA_TYPE_UPDATE_APP
|
||||
if getattr(args, "file", None) is not None:
|
||||
binary = Path(args.file)
|
||||
|
||||
return espota2.run_ota(network_devices, remote_port, password, binary, ota_type)
|
||||
return espota2.run_ota(network_devices, remote_port, password, binary)
|
||||
|
||||
|
||||
def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int | None:
|
||||
@@ -1194,8 +1180,6 @@ def command_wizard(args: ArgsProtocol) -> int | None:
|
||||
|
||||
|
||||
def command_config(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
from esphome import yaml_util
|
||||
|
||||
if not CORE.verbose:
|
||||
config = strip_default_ids(config)
|
||||
output = yaml_util.dump(config, args.show_secrets)
|
||||
@@ -1337,8 +1321,6 @@ def command_clean_mqtt(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
|
||||
|
||||
def command_clean_all(args: ArgsProtocol) -> int | None:
|
||||
from esphome import writer
|
||||
|
||||
try:
|
||||
writer.clean_all(args.configuration)
|
||||
except OSError as err:
|
||||
@@ -1354,8 +1336,6 @@ def command_version(args: ArgsProtocol) -> int | None:
|
||||
|
||||
|
||||
def command_clean(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
from esphome import writer
|
||||
|
||||
try:
|
||||
writer.clean_build()
|
||||
except OSError as err:
|
||||
@@ -1558,8 +1538,6 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
|
||||
|
||||
|
||||
def command_rename(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
from esphome import yaml_util
|
||||
|
||||
new_name = args.name
|
||||
for c in new_name:
|
||||
if c not in ALLOWED_NAME_CHARS:
|
||||
|
||||
@@ -793,11 +793,8 @@ class MemoryAnalyzer:
|
||||
"""Scan ESPHome source object files to map extern "C" symbols to components.
|
||||
|
||||
When no linker map file is available, this uses ``nm`` to scan ``.o`` files
|
||||
under ``src/`` (including ``src/main.cpp.o`` and everything beneath
|
||||
``src/esphome/``) and build a symbol-to-component mapping. This catches
|
||||
``extern "C"`` functions, the ESPHome-generated ``setup()``/``loop()``
|
||||
entry points in ``main.cpp``, and other symbols that lack C++ namespace
|
||||
prefixes.
|
||||
under ``src/esphome/`` and build a symbol-to-component mapping. This catches
|
||||
``extern "C"`` functions and other symbols that lack C++ namespace prefixes.
|
||||
|
||||
Skips scanning if ``_source_symbol_map`` was already populated by
|
||||
``_parse_map_file()``.
|
||||
@@ -809,12 +806,12 @@ class MemoryAnalyzer:
|
||||
if obj_dir is None:
|
||||
return
|
||||
|
||||
# Scan all ESPHome-owned source object files: src/main.cpp.o and src/esphome/...
|
||||
src_dir = obj_dir / "src"
|
||||
if not src_dir.is_dir():
|
||||
# Find ESPHome source object files
|
||||
esphome_src_dir = obj_dir / "src" / "esphome"
|
||||
if not esphome_src_dir.is_dir():
|
||||
return
|
||||
|
||||
obj_files = sorted(src_dir.rglob("*.o"))
|
||||
obj_files = sorted(esphome_src_dir.rglob("*.o"))
|
||||
if not obj_files:
|
||||
return
|
||||
|
||||
@@ -1067,10 +1064,6 @@ class MemoryAnalyzer:
|
||||
if component_name in self.external_components:
|
||||
return f"{_COMPONENT_PREFIX_EXTERNAL}{component_name}"
|
||||
|
||||
# ESPHome-generated entry point: src/main.cpp.o (contains setup()/loop())
|
||||
if len(parts) >= 2 and parts[-2:] == ("src", "main.cpp.o"):
|
||||
return _COMPONENT_CORE
|
||||
|
||||
# ESPHome core: src/esphome/core/... or src/esphome/...
|
||||
if "core" in parts and "esphome" in parts:
|
||||
return _COMPONENT_CORE
|
||||
|
||||
@@ -127,7 +127,7 @@ def validate_potentially_or_condition(value):
|
||||
return validate_condition(value)
|
||||
|
||||
|
||||
DelayAction = cg.esphome_ns.class_("DelayAction", Action)
|
||||
DelayAction = cg.esphome_ns.class_("DelayAction", Action, cg.Component)
|
||||
LambdaAction = cg.esphome_ns.class_("LambdaAction", Action)
|
||||
StatelessLambdaAction = cg.esphome_ns.class_("StatelessLambdaAction", Action)
|
||||
IfAction = cg.esphome_ns.class_("IfAction", Action)
|
||||
@@ -396,6 +396,7 @@ async def delay_action_to_code(
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_component(var, {})
|
||||
template_ = await cg.templatable(config, args, cg.uint32)
|
||||
cg.add(var.set_delay(template_))
|
||||
return var
|
||||
@@ -596,7 +597,7 @@ async def component_resume_action_to_code(
|
||||
comp = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, comp)
|
||||
if CONF_UPDATE_INTERVAL in config:
|
||||
template_ = await cg.templatable(config[CONF_UPDATE_INTERVAL], args, cg.uint32)
|
||||
template_ = await cg.templatable(config[CONF_UPDATE_INTERVAL], args, int)
|
||||
cg.add(var.set_update_interval(template_))
|
||||
return var
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ AdalightLightEffect::Frame AdalightLightEffect::parse_frame_(light::AddressableL
|
||||
uint8_t *led_data = &frame_[6];
|
||||
|
||||
for (int led = 0; led < accepted_led_count; led++, led_data += 3) {
|
||||
auto white = std::min({led_data[0], led_data[1], led_data[2]});
|
||||
auto white = std::min(std::min(led_data[0], led_data[1]), led_data[2]);
|
||||
|
||||
it[led].set(Color(led_data[0], led_data[1], led_data[2], white));
|
||||
}
|
||||
|
||||
@@ -62,12 +62,7 @@ void Animation::set_frame(int frame) {
|
||||
}
|
||||
|
||||
void Animation::update_data_start_() {
|
||||
uint32_t image_size = this->get_width_stride() * this->height_;
|
||||
// RGB565 with an alpha channel stores the alpha plane immediately after the RGB
|
||||
// plane within each frame, so the per-frame stride includes the alpha bytes.
|
||||
if (this->type_ == image::IMAGE_TYPE_RGB565 && this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
|
||||
image_size += static_cast<uint32_t>(this->width_) * this->height_;
|
||||
}
|
||||
const uint32_t image_size = this->get_width_stride() * this->height_;
|
||||
this->data_start_ = this->animation_data_start_ + image_size * this->current_frame_;
|
||||
}
|
||||
|
||||
|
||||
@@ -1419,8 +1419,6 @@ enum LockState {
|
||||
LOCK_STATE_JAMMED = 3;
|
||||
LOCK_STATE_LOCKING = 4;
|
||||
LOCK_STATE_UNLOCKING = 5;
|
||||
LOCK_STATE_OPENING = 6;
|
||||
LOCK_STATE_OPEN = 7;
|
||||
}
|
||||
enum LockCommand {
|
||||
LOCK_UNLOCK = 0;
|
||||
@@ -1639,7 +1637,7 @@ message BluetoothLEAdvertisementResponse {
|
||||
|
||||
message BluetoothLERawAdvertisement {
|
||||
option (inline_encode) = true;
|
||||
uint64 address = 1 [(force) = true, (mac_address) = true];
|
||||
uint64 address = 1 [(force) = true];
|
||||
sint32 rssi = 2 [(force) = true];
|
||||
uint32 address_type = 3 [(max_value) = 4];
|
||||
|
||||
|
||||
@@ -110,10 +110,4 @@ extend google.protobuf.FieldOptions {
|
||||
// length varint calculations and direct byte writes, since the length
|
||||
// varint is guaranteed to be 1 byte.
|
||||
optional uint32 max_data_length = 50018;
|
||||
|
||||
// mac_address: Field is a 48-bit MAC address stored in a uint64.
|
||||
// Emits encode_varint_raw_48bit which has a 7-byte fast path that avoids
|
||||
// the per-byte loop when the upper bits are non-zero (the common case
|
||||
// for real MAC addresses, since OUIs occupy the top 24 bits).
|
||||
optional bool mac_address = 50019 [default=false];
|
||||
}
|
||||
|
||||
@@ -2352,7 +2352,7 @@ BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCO
|
||||
uint8_t *len_pos = pos;
|
||||
ProtoEncode::reserve_byte(pos PROTO_ENCODE_DEBUG_ARG);
|
||||
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 8);
|
||||
ProtoEncode::encode_varint_raw_48bit(pos PROTO_ENCODE_DEBUG_ARG, sub_msg.address);
|
||||
ProtoEncode::encode_varint_raw_64(pos PROTO_ENCODE_DEBUG_ARG, sub_msg.address);
|
||||
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 16);
|
||||
ProtoEncode::encode_varint_raw_short(pos PROTO_ENCODE_DEBUG_ARG, encode_zigzag32(sub_msg.rssi));
|
||||
if (sub_msg.address_type) {
|
||||
@@ -2373,7 +2373,7 @@ BluetoothLERawAdvertisementsResponse::calculate_size() const {
|
||||
for (uint16_t i = 0; i < this->advertisements_len; i++) {
|
||||
auto &sub_msg = this->advertisements[i];
|
||||
size += 2;
|
||||
size += ProtoSize::calc_uint64_48bit_force(1, sub_msg.address);
|
||||
size += ProtoSize::calc_uint64_force(1, sub_msg.address);
|
||||
size += ProtoSize::calc_sint32_force(1, sub_msg.rssi);
|
||||
size += sub_msg.address_type ? 2 : 0;
|
||||
size += 2 + sub_msg.data_len;
|
||||
|
||||
@@ -181,8 +181,6 @@ enum LockState : uint32_t {
|
||||
LOCK_STATE_JAMMED = 3,
|
||||
LOCK_STATE_LOCKING = 4,
|
||||
LOCK_STATE_UNLOCKING = 5,
|
||||
LOCK_STATE_OPENING = 6,
|
||||
LOCK_STATE_OPEN = 7,
|
||||
};
|
||||
enum LockCommand : uint32_t {
|
||||
LOCK_UNLOCK = 0,
|
||||
|
||||
@@ -487,10 +487,6 @@ template<> const char *proto_enum_to_string<enums::LockState>(enums::LockState v
|
||||
return ESPHOME_PSTR("LOCK_STATE_LOCKING");
|
||||
case enums::LOCK_STATE_UNLOCKING:
|
||||
return ESPHOME_PSTR("LOCK_STATE_UNLOCKING");
|
||||
case enums::LOCK_STATE_OPENING:
|
||||
return ESPHOME_PSTR("LOCK_STATE_OPENING");
|
||||
case enums::LOCK_STATE_OPEN:
|
||||
return ESPHOME_PSTR("LOCK_STATE_OPEN");
|
||||
default:
|
||||
return ESPHOME_PSTR("UNKNOWN");
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ void APIServerConnectionBase::log_receive_message_(const LogString *name) {
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_API
|
||||
void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {
|
||||
// Check authentication/connection requirements
|
||||
switch (msg_type) {
|
||||
@@ -707,6 +706,5 @@ void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const ui
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif // USE_API
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
@@ -78,8 +78,7 @@ class ActionResponse {
|
||||
: success_(success), error_message_(error_message) {
|
||||
if (data == nullptr || data_len == 0)
|
||||
return;
|
||||
JsonDocument tmp = json::parse_json(data, data_len);
|
||||
swap(this->json_document_, tmp);
|
||||
this->json_document_ = json::parse_json(data, data_len);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -342,32 +342,6 @@ class ProtoEncode {
|
||||
}
|
||||
encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value);
|
||||
}
|
||||
/// Encode a 48-bit MAC address (stored in a uint64) as varint.
|
||||
/// Real MAC addresses occupy the full 48 bits (OUI in upper 24), so the
|
||||
/// fast path -- any non-zero bit in the top 6 of 48 -- emits exactly 7 bytes
|
||||
/// with no per-byte branch. Falls back to the general loop otherwise.
|
||||
/// Caller must guarantee value fits in 48 bits (checked in debug builds).
|
||||
static inline void ESPHOME_ALWAYS_INLINE encode_varint_raw_48bit(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint64_t value) {
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
assert(value < (1ULL << (MAC_ADDRESS_SIZE * 8)) && "encode_varint_raw_48bit: value exceeds 48 bits");
|
||||
#endif
|
||||
// 7-byte varint holds 49 bits (7 * 7), so a 48-bit value needs all 7 bytes
|
||||
// whenever bit 42 or higher is set (i.e. value >= 1 << (48 - 6)).
|
||||
if (value >= (1ULL << (MAC_ADDRESS_SIZE * 8 - 6))) [[likely]] {
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 7);
|
||||
pos[0] = static_cast<uint8_t>(value | 0x80);
|
||||
pos[1] = static_cast<uint8_t>((value >> 7) | 0x80);
|
||||
pos[2] = static_cast<uint8_t>((value >> 14) | 0x80);
|
||||
pos[3] = static_cast<uint8_t>((value >> 21) | 0x80);
|
||||
pos[4] = static_cast<uint8_t>((value >> 28) | 0x80);
|
||||
pos[5] = static_cast<uint8_t>((value >> 35) | 0x80);
|
||||
pos[6] = static_cast<uint8_t>(value >> 42);
|
||||
pos += 7;
|
||||
return;
|
||||
}
|
||||
encode_varint_raw_64(pos PROTO_ENCODE_DEBUG_ARG, value);
|
||||
}
|
||||
static inline void ESPHOME_ALWAYS_INLINE encode_field_raw(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, uint32_t type) {
|
||||
encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, (field_id << 3) | type);
|
||||
@@ -424,7 +398,6 @@ class ProtoEncode {
|
||||
if (len == 0 && !force)
|
||||
return;
|
||||
encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 2); // type 2: Length-delimited string
|
||||
// NOLINTNEXTLINE(readability-inconsistent-ifelse-braces) -- false positive on [[likely]] attribute
|
||||
if (len < VARINT_MAX_1_BYTE) [[likely]] {
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 1 + len);
|
||||
*pos++ = static_cast<uint8_t>(len);
|
||||
@@ -844,14 +817,6 @@ class ProtoSize {
|
||||
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_uint64_force(uint32_t field_id_size, uint64_t value) {
|
||||
return field_id_size + varint(value);
|
||||
}
|
||||
/// 48-bit MAC address variant: matches encode_varint_raw_48bit's fast path.
|
||||
/// When any of the top 6 of 48 bits is set the encoded varint is 7 bytes;
|
||||
/// otherwise fall back to the general size calculation.
|
||||
/// Caller must guarantee value fits in 48 bits (encoder asserts in debug).
|
||||
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_uint64_48bit_force(uint32_t field_id_size,
|
||||
uint64_t value) {
|
||||
return field_id_size + (value >= (1ULL << (MAC_ADDRESS_SIZE * 8 - 6)) ? 7 : varint(value));
|
||||
}
|
||||
static constexpr uint32_t calc_length(uint32_t field_id_size, size_t len) {
|
||||
return len ? field_id_size + varint(static_cast<uint32_t>(len)) + static_cast<uint32_t>(len) : 0;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,11 @@ class AQICalculator : public AbstractAQICalculator {
|
||||
uint16_t get_aqi(float pm2_5_value, float pm10_0_value) override {
|
||||
float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID);
|
||||
float pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID);
|
||||
float aqi = std::max({pm2_5_index, pm10_0_index, 0.0f});
|
||||
|
||||
float aqi = std::max(pm2_5_index, pm10_0_index);
|
||||
if (aqi < 0.0f) {
|
||||
aqi = 0.0f;
|
||||
}
|
||||
return static_cast<uint16_t>(std::lround(aqi));
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,11 @@ class CAQICalculator : public AbstractAQICalculator {
|
||||
uint16_t get_aqi(float pm2_5_value, float pm10_0_value) override {
|
||||
float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID);
|
||||
float pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID);
|
||||
float aqi = std::max({pm2_5_index, pm10_0_index, 0.0f});
|
||||
|
||||
float aqi = std::max(pm2_5_index, pm10_0_index);
|
||||
if (aqi < 0.0f) {
|
||||
aqi = 0.0f;
|
||||
}
|
||||
return static_cast<uint16_t>(std::lround(aqi));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from dataclasses import dataclass, field
|
||||
from dataclasses import dataclass
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.esp32 import (
|
||||
@@ -7,12 +7,7 @@ from esphome.components.esp32 import (
|
||||
include_builtin_idf_component,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BITS_PER_SAMPLE,
|
||||
CONF_NUM_CHANNELS,
|
||||
CONF_SAMPLE_RATE,
|
||||
CONF_SIZE,
|
||||
)
|
||||
from esphome.const import CONF_BITS_PER_SAMPLE, CONF_NUM_CHANNELS, CONF_SAMPLE_RATE
|
||||
from esphome.core import CORE
|
||||
import esphome.final_validate as fv
|
||||
|
||||
@@ -30,46 +25,13 @@ AUDIO_FILE_TYPE_ENUM = {
|
||||
"OPUS": AudioFileType.OPUS,
|
||||
}
|
||||
|
||||
MEMORY_PSRAM = "psram"
|
||||
MEMORY_INTERNAL = "internal"
|
||||
MEMORY_LOCATIONS = [MEMORY_PSRAM, MEMORY_INTERNAL]
|
||||
|
||||
|
||||
@dataclass
|
||||
class FlacOptions:
|
||||
buffer_memory: str | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class Mp3Options:
|
||||
buffer_memory: str | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class OpusPseudostackOptions:
|
||||
threadsafe: bool | None = None
|
||||
buffer_memory: str | None = None
|
||||
size: int | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class OpusOptions:
|
||||
floating_point: bool | None = None
|
||||
state_memory: str | None = None
|
||||
pseudostack: OpusPseudostackOptions = field(default_factory=OpusPseudostackOptions)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AudioData:
|
||||
flac_support: bool = False
|
||||
mp3_support: bool = False
|
||||
opus_support: bool = False
|
||||
# WAV defaults to True for backward compatibility; will become opt-in in a future release
|
||||
wav_support: bool = True
|
||||
micro_decoder_support: bool = False
|
||||
flac: FlacOptions = field(default_factory=FlacOptions)
|
||||
mp3: Mp3Options = field(default_factory=Mp3Options)
|
||||
opus: OpusOptions = field(default_factory=OpusOptions)
|
||||
|
||||
|
||||
def _get_data() -> AudioData:
|
||||
@@ -93,11 +55,6 @@ def request_opus_support() -> None:
|
||||
_get_data().opus_support = True
|
||||
|
||||
|
||||
def request_wav_support() -> None:
|
||||
"""Request WAV codec support for audio decoding."""
|
||||
_get_data().wav_support = True
|
||||
|
||||
|
||||
def request_micro_decoder_support() -> None:
|
||||
"""Request micro-decoder library support for audio decoding."""
|
||||
_get_data().micro_decoder_support = True
|
||||
@@ -110,78 +67,9 @@ CONF_MAX_CHANNELS = "max_channels"
|
||||
CONF_MIN_SAMPLE_RATE = "min_sample_rate"
|
||||
CONF_MAX_SAMPLE_RATE = "max_sample_rate"
|
||||
|
||||
CONF_CODECS = "codecs"
|
||||
CONF_WAV = "wav"
|
||||
CONF_FLAC = "flac"
|
||||
CONF_MP3 = "mp3"
|
||||
CONF_OPUS = "opus"
|
||||
CONF_BUFFER_MEMORY = "buffer_memory"
|
||||
CONF_FLOATING_POINT = "floating_point"
|
||||
CONF_STATE_MEMORY = "state_memory"
|
||||
CONF_PSEUDOSTACK = "pseudostack"
|
||||
CONF_THREADSAFE = "threadsafe"
|
||||
|
||||
|
||||
_MEMORY_LOCATION_VALIDATOR = cv.one_of(*MEMORY_LOCATIONS, lower=True)
|
||||
|
||||
|
||||
def _maybe_empty_codec(schema):
|
||||
"""Wrap a codec dict schema so that a bare key (None value) is treated as an empty dict."""
|
||||
|
||||
def validator(value):
|
||||
if value is None:
|
||||
value = {}
|
||||
return schema(value)
|
||||
|
||||
return validator
|
||||
|
||||
|
||||
CODEC_FLAC_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_BUFFER_MEMORY): _MEMORY_LOCATION_VALIDATOR,
|
||||
}
|
||||
)
|
||||
|
||||
CODEC_MP3_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_BUFFER_MEMORY): _MEMORY_LOCATION_VALIDATOR,
|
||||
}
|
||||
)
|
||||
|
||||
OPUS_PSEUDOSTACK_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_THREADSAFE): cv.boolean,
|
||||
cv.Optional(CONF_BUFFER_MEMORY): _MEMORY_LOCATION_VALIDATOR,
|
||||
cv.Optional(CONF_SIZE): cv.int_range(60000, 240000),
|
||||
}
|
||||
)
|
||||
|
||||
CODEC_OPUS_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_FLOATING_POINT): cv.boolean,
|
||||
cv.Optional(CONF_STATE_MEMORY): _MEMORY_LOCATION_VALIDATOR,
|
||||
cv.Optional(CONF_PSEUDOSTACK): _maybe_empty_codec(OPUS_PSEUDOSTACK_SCHEMA),
|
||||
}
|
||||
)
|
||||
|
||||
CODEC_WAV_SCHEMA = cv.Schema({})
|
||||
|
||||
CODECS_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_FLAC): _maybe_empty_codec(CODEC_FLAC_SCHEMA),
|
||||
cv.Optional(CONF_MP3): _maybe_empty_codec(CODEC_MP3_SCHEMA),
|
||||
cv.Optional(CONF_OPUS): _maybe_empty_codec(CODEC_OPUS_SCHEMA),
|
||||
cv.Optional(CONF_WAV): _maybe_empty_codec(CODEC_WAV_SCHEMA),
|
||||
}
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_CODECS): _maybe_empty_codec(CODECS_SCHEMA),
|
||||
}
|
||||
),
|
||||
cv.only_on_esp32,
|
||||
cv.Schema({}),
|
||||
)
|
||||
|
||||
AUDIO_COMPONENT_SCHEMA = cv.Schema(
|
||||
@@ -320,15 +208,6 @@ def final_validate_audio_schema(
|
||||
)
|
||||
|
||||
|
||||
def _emit_memory_pair(value: str | None, psram_key: str, internal_key: str) -> None:
|
||||
if value == MEMORY_PSRAM:
|
||||
add_idf_sdkconfig_option(psram_key, True)
|
||||
add_idf_sdkconfig_option(internal_key, False)
|
||||
elif value == MEMORY_INTERNAL:
|
||||
add_idf_sdkconfig_option(psram_key, False)
|
||||
add_idf_sdkconfig_option(internal_key, True)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
# Re-enable ESP-IDF's HTTP client (excluded by default to save compile time)
|
||||
include_builtin_idf_component("esp_http_client")
|
||||
@@ -340,38 +219,8 @@ async def to_code(config):
|
||||
|
||||
data = _get_data()
|
||||
|
||||
# Merge user-supplied codec configuration (additive: presence enables the codec)
|
||||
if codecs_config := config.get(CONF_CODECS):
|
||||
if (flac_config := codecs_config.get(CONF_FLAC)) is not None:
|
||||
data.flac_support = True
|
||||
if (buffer_memory := flac_config.get(CONF_BUFFER_MEMORY)) is not None:
|
||||
data.flac.buffer_memory = buffer_memory
|
||||
if (mp3_config := codecs_config.get(CONF_MP3)) is not None:
|
||||
data.mp3_support = True
|
||||
if (buffer_memory := mp3_config.get(CONF_BUFFER_MEMORY)) is not None:
|
||||
data.mp3.buffer_memory = buffer_memory
|
||||
if (opus_config := codecs_config.get(CONF_OPUS)) is not None:
|
||||
data.opus_support = True
|
||||
floating_point = opus_config.get(CONF_FLOATING_POINT)
|
||||
if floating_point is not None:
|
||||
data.opus.floating_point = floating_point
|
||||
if (state_memory := opus_config.get(CONF_STATE_MEMORY)) is not None:
|
||||
data.opus.state_memory = state_memory
|
||||
if (pseudostack_config := opus_config.get(CONF_PSEUDOSTACK)) is not None:
|
||||
threadsafe = pseudostack_config.get(CONF_THREADSAFE)
|
||||
if threadsafe is not None:
|
||||
data.opus.pseudostack.threadsafe = threadsafe
|
||||
if (
|
||||
buffer_memory := pseudostack_config.get(CONF_BUFFER_MEMORY)
|
||||
) is not None:
|
||||
data.opus.pseudostack.buffer_memory = buffer_memory
|
||||
if (size := pseudostack_config.get(CONF_SIZE)) is not None:
|
||||
data.opus.pseudostack.size = size
|
||||
if CONF_WAV in codecs_config:
|
||||
data.wav_support = True
|
||||
|
||||
if data.micro_decoder_support:
|
||||
add_idf_component(name="esphome/micro-decoder", ref="0.2.0")
|
||||
add_idf_component(name="esphome/micro-decoder", ref="0.1.1")
|
||||
|
||||
# All codecs are enabled by default in micro-decoder, so disable the ones that aren't requested to save flash
|
||||
if not data.flac_support:
|
||||
@@ -380,50 +229,13 @@ async def to_code(config):
|
||||
add_idf_sdkconfig_option("CONFIG_MICRO_DECODER_CODEC_MP3", False)
|
||||
if not data.opus_support:
|
||||
add_idf_sdkconfig_option("CONFIG_MICRO_DECODER_CODEC_OPUS", False)
|
||||
if not data.wav_support:
|
||||
add_idf_sdkconfig_option("CONFIG_MICRO_DECODER_CODEC_WAV", False)
|
||||
|
||||
# Configure each codec library.
|
||||
# Adds a define and IDF component for legacy `audio_decoder.cpp`.
|
||||
# Legacy audio_decoder.cpp support defines and components
|
||||
if data.flac_support:
|
||||
cg.add_define("USE_AUDIO_FLAC_SUPPORT")
|
||||
add_idf_component(name="esphome/micro-flac", ref="0.1.1")
|
||||
_emit_memory_pair(
|
||||
data.flac.buffer_memory,
|
||||
"CONFIG_MICRO_FLAC_PREFER_PSRAM",
|
||||
"CONFIG_MICRO_FLAC_PREFER_INTERNAL",
|
||||
)
|
||||
if data.mp3_support:
|
||||
cg.add_define("USE_AUDIO_MP3_SUPPORT")
|
||||
_emit_memory_pair(
|
||||
data.mp3.buffer_memory,
|
||||
"CONFIG_MP3_DECODER_PREFER_PSRAM",
|
||||
"CONFIG_MP3_DECODER_PREFER_INTERNAL",
|
||||
)
|
||||
if data.opus_support:
|
||||
cg.add_define("USE_AUDIO_OPUS_SUPPORT")
|
||||
add_idf_component(name="esphome/micro-opus", ref="0.4.0")
|
||||
if data.opus.floating_point is not None:
|
||||
add_idf_sdkconfig_option(
|
||||
"CONFIG_OPUS_FLOATING_POINT", data.opus.floating_point
|
||||
)
|
||||
_emit_memory_pair(
|
||||
data.opus.state_memory,
|
||||
"CONFIG_OPUS_STATE_PREFER_PSRAM",
|
||||
"CONFIG_OPUS_STATE_PREFER_INTERNAL",
|
||||
)
|
||||
if data.opus.pseudostack.threadsafe is True:
|
||||
add_idf_sdkconfig_option("CONFIG_OPUS_THREADSAFE_PSEUDOSTACK", True)
|
||||
add_idf_sdkconfig_option("CONFIG_OPUS_NONTHREADSAFE_PSEUDOSTACK", False)
|
||||
elif data.opus.pseudostack.threadsafe is False:
|
||||
add_idf_sdkconfig_option("CONFIG_OPUS_THREADSAFE_PSEUDOSTACK", False)
|
||||
add_idf_sdkconfig_option("CONFIG_OPUS_NONTHREADSAFE_PSEUDOSTACK", True)
|
||||
_emit_memory_pair(
|
||||
data.opus.pseudostack.buffer_memory,
|
||||
"CONFIG_OPUS_PSEUDOSTACK_PREFER_PSRAM",
|
||||
"CONFIG_OPUS_PSEUDOSTACK_PREFER_INTERNAL",
|
||||
)
|
||||
if data.opus.pseudostack.size is not None:
|
||||
add_idf_sdkconfig_option(
|
||||
"CONFIG_OPUS_PSEUDOSTACK_SIZE", data.opus.pseudostack.size
|
||||
)
|
||||
add_idf_component(name="esphome/micro-opus", ref="0.3.6")
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from dataclasses import dataclass, field
|
||||
from functools import partial
|
||||
import hashlib
|
||||
import logging
|
||||
from pathlib import Path
|
||||
@@ -20,7 +19,7 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core import CORE, ID, HexInt
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.external_files import download_web_files_in_config
|
||||
from esphome.external_files import download_content
|
||||
from esphome.types import ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -64,6 +63,15 @@ def _compute_local_file_path(value: ConfigType) -> Path:
|
||||
return base_dir / key
|
||||
|
||||
|
||||
def _download_web_file(value: ConfigType) -> ConfigType:
|
||||
url = value[CONF_URL]
|
||||
path = _compute_local_file_path(value)
|
||||
|
||||
download_content(url, path)
|
||||
_LOGGER.debug("download_web_file: path=%s", path)
|
||||
return value
|
||||
|
||||
|
||||
def _file_schema(value: ConfigType | str) -> ConfigType:
|
||||
if isinstance(value, str):
|
||||
return _validate_file_shorthand(value)
|
||||
@@ -134,10 +142,11 @@ LOCAL_SCHEMA = cv.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
WEB_SCHEMA = cv.Schema(
|
||||
WEB_SCHEMA = cv.All(
|
||||
{
|
||||
cv.Required(CONF_URL): cv.url,
|
||||
}
|
||||
},
|
||||
_download_web_file,
|
||||
)
|
||||
|
||||
|
||||
@@ -200,7 +209,6 @@ def _validate_supported_local_file(config: list[ConfigType]) -> list[ConfigType]
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.only_on_esp32,
|
||||
cv.ensure_list(MEDIA_FILE_TYPE_SCHEMA),
|
||||
partial(download_web_files_in_config, path_for=_compute_local_file_path),
|
||||
_validate_supported_local_file,
|
||||
)
|
||||
|
||||
|
||||
@@ -62,7 +62,6 @@ CONF_IS_WRGB = "is_wrgb"
|
||||
SUPPORTED_PINS = {
|
||||
libretiny.const.FAMILY_BK7231N: [16],
|
||||
libretiny.const.FAMILY_BK7231T: [16],
|
||||
libretiny.const.FAMILY_BK7238: [16],
|
||||
libretiny.const.FAMILY_BK7251: [16],
|
||||
}
|
||||
|
||||
|
||||
@@ -143,15 +143,15 @@ BinarySensorCondition = binary_sensor_ns.class_("BinarySensorCondition", Conditi
|
||||
|
||||
# Filters
|
||||
Filter = binary_sensor_ns.class_("Filter")
|
||||
TimeoutFilter = binary_sensor_ns.class_("TimeoutFilter", Filter)
|
||||
DelayedOnOffFilter = binary_sensor_ns.class_("DelayedOnOffFilter", Filter)
|
||||
DelayedOnFilter = binary_sensor_ns.class_("DelayedOnFilter", Filter)
|
||||
DelayedOffFilter = binary_sensor_ns.class_("DelayedOffFilter", Filter)
|
||||
TimeoutFilter = binary_sensor_ns.class_("TimeoutFilter", Filter, cg.Component)
|
||||
DelayedOnOffFilter = binary_sensor_ns.class_("DelayedOnOffFilter", Filter, cg.Component)
|
||||
DelayedOnFilter = binary_sensor_ns.class_("DelayedOnFilter", Filter, cg.Component)
|
||||
DelayedOffFilter = binary_sensor_ns.class_("DelayedOffFilter", Filter, cg.Component)
|
||||
InvertFilter = binary_sensor_ns.class_("InvertFilter", Filter)
|
||||
AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter, cg.Component)
|
||||
LambdaFilter = binary_sensor_ns.class_("LambdaFilter", Filter)
|
||||
StatelessLambdaFilter = binary_sensor_ns.class_("StatelessLambdaFilter", Filter)
|
||||
SettleFilter = binary_sensor_ns.class_("SettleFilter", Filter)
|
||||
SettleFilter = binary_sensor_ns.class_("SettleFilter", Filter, cg.Component)
|
||||
|
||||
_LOGGER = getLogger(__name__)
|
||||
|
||||
@@ -175,6 +175,7 @@ async def invert_filter_to_code(config, filter_id):
|
||||
)
|
||||
async def timeout_filter_to_code(config, filter_id):
|
||||
var = cg.new_Pvariable(filter_id)
|
||||
await cg.register_component(var, {})
|
||||
template_ = await cg.templatable(config, [], cg.uint32)
|
||||
cg.add(var.set_timeout_value(template_))
|
||||
return var
|
||||
@@ -202,6 +203,7 @@ async def timeout_filter_to_code(config, filter_id):
|
||||
)
|
||||
async def delayed_on_off_filter_to_code(config, filter_id):
|
||||
var = cg.new_Pvariable(filter_id)
|
||||
await cg.register_component(var, {})
|
||||
if isinstance(config, dict):
|
||||
template_ = await cg.templatable(config[CONF_TIME_ON], [], cg.uint32)
|
||||
cg.add(var.set_on_delay(template_))
|
||||
@@ -219,6 +221,7 @@ async def delayed_on_off_filter_to_code(config, filter_id):
|
||||
)
|
||||
async def delayed_on_filter_to_code(config, filter_id):
|
||||
var = cg.new_Pvariable(filter_id)
|
||||
await cg.register_component(var, {})
|
||||
template_ = await cg.templatable(config, [], cg.uint32)
|
||||
cg.add(var.set_delay(template_))
|
||||
return var
|
||||
@@ -231,6 +234,7 @@ async def delayed_on_filter_to_code(config, filter_id):
|
||||
)
|
||||
async def delayed_off_filter_to_code(config, filter_id):
|
||||
var = cg.new_Pvariable(filter_id)
|
||||
await cg.register_component(var, {})
|
||||
template_ = await cg.templatable(config, [], cg.uint32)
|
||||
cg.add(var.set_delay(template_))
|
||||
return var
|
||||
@@ -302,6 +306,7 @@ async def lambda_filter_to_code(config, filter_id):
|
||||
)
|
||||
async def settle_filter_to_code(config, filter_id):
|
||||
var = cg.new_Pvariable(filter_id)
|
||||
await cg.register_component(var, {})
|
||||
template_ = await cg.templatable(config, [], cg.uint32)
|
||||
cg.add(var.set_delay(template_))
|
||||
return var
|
||||
|
||||
@@ -50,31 +50,29 @@ void MultiClickTriggerBase::on_state_(bool state) {
|
||||
return;
|
||||
}
|
||||
|
||||
// at_index_ has a value here (the !has_value() branch above returns).
|
||||
size_t at_index = *this->at_index_;
|
||||
if (at_index == this->timing_count_) {
|
||||
if (*this->at_index_ == this->timing_count_) {
|
||||
this->trigger_();
|
||||
return;
|
||||
}
|
||||
|
||||
MultiClickTriggerEvent evt = this->timing_[at_index];
|
||||
MultiClickTriggerEvent evt = this->timing_[*this->at_index_];
|
||||
|
||||
if (evt.max_length != 4294967294UL) {
|
||||
ESP_LOGV(TAG, "A i=%zu min=%" PRIu32 " max=%" PRIu32, at_index, evt.min_length, evt.max_length); // NOLINT
|
||||
ESP_LOGV(TAG, "A i=%zu min=%" PRIu32 " max=%" PRIu32, *this->at_index_, evt.min_length, evt.max_length); // NOLINT
|
||||
this->schedule_is_valid_(evt.min_length);
|
||||
this->schedule_is_not_valid_(evt.max_length);
|
||||
} else if (at_index + 1 != this->timing_count_) {
|
||||
ESP_LOGV(TAG, "B i=%zu min=%" PRIu32, at_index, evt.min_length); // NOLINT
|
||||
} else if (*this->at_index_ + 1 != this->timing_count_) {
|
||||
ESP_LOGV(TAG, "B i=%zu min=%" PRIu32, *this->at_index_, evt.min_length); // NOLINT
|
||||
this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID);
|
||||
this->schedule_is_valid_(evt.min_length);
|
||||
} else {
|
||||
ESP_LOGV(TAG, "C i=%zu min=%" PRIu32, at_index, evt.min_length); // NOLINT
|
||||
ESP_LOGV(TAG, "C i=%zu min=%" PRIu32, *this->at_index_, evt.min_length); // NOLINT
|
||||
this->is_valid_ = false;
|
||||
this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID);
|
||||
this->set_timeout(MULTICLICK_TRIGGER_ID, evt.min_length, [this]() { this->trigger_(); });
|
||||
}
|
||||
|
||||
this->at_index_ = at_index + 1;
|
||||
*this->at_index_ = *this->at_index_ + 1;
|
||||
}
|
||||
void MultiClickTriggerBase::schedule_cooldown_() {
|
||||
ESP_LOGV(TAG, "Multi Click: Invalid length of press, starting cooldown of %" PRIu32 " ms", this->invalid_cooldown_);
|
||||
|
||||
@@ -4,14 +4,16 @@
|
||||
#include "filter.h"
|
||||
|
||||
#include "binary_sensor.h"
|
||||
#include "esphome/core/application.h"
|
||||
|
||||
namespace esphome::binary_sensor {
|
||||
|
||||
static const char *const TAG = "sensor.filter";
|
||||
|
||||
// AutorepeatFilter still inherits Component (it schedules two distinct timer
|
||||
// purposes), so it keeps the (Component *, id) scheduler API.
|
||||
// Timeout IDs for filter classes.
|
||||
// Each filter is its own Component instance, so the scheduler scopes
|
||||
// IDs by component pointer — no risk of collisions between instances.
|
||||
constexpr uint32_t FILTER_TIMEOUT_ID = 0;
|
||||
// AutorepeatFilter needs two distinct IDs (both timeouts on the same component)
|
||||
constexpr uint32_t AUTOREPEAT_TIMING_ID = 0;
|
||||
constexpr uint32_t AUTOREPEAT_ON_OFF_ID = 1;
|
||||
|
||||
@@ -32,40 +34,46 @@ void Filter::input(bool value) {
|
||||
}
|
||||
|
||||
void TimeoutFilter::input(bool value) {
|
||||
App.scheduler.set_timeout(this, this->timeout_delay_.value(), [this]() { this->parent_->invalidate_state(); });
|
||||
this->set_timeout(FILTER_TIMEOUT_ID, this->timeout_delay_.value(), [this]() { this->parent_->invalidate_state(); });
|
||||
// we do not de-dup here otherwise changes from invalid to valid state will not be output
|
||||
this->output(value);
|
||||
}
|
||||
|
||||
optional<bool> DelayedOnOffFilter::new_value(bool value) {
|
||||
if (value) {
|
||||
App.scheduler.set_timeout(this, this->on_delay_.value(), [this]() { this->output(true); });
|
||||
this->set_timeout(FILTER_TIMEOUT_ID, this->on_delay_.value(), [this]() { this->output(true); });
|
||||
} else {
|
||||
App.scheduler.set_timeout(this, this->off_delay_.value(), [this]() { this->output(false); });
|
||||
this->set_timeout(FILTER_TIMEOUT_ID, this->off_delay_.value(), [this]() { this->output(false); });
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
float DelayedOnOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
|
||||
|
||||
optional<bool> DelayedOnFilter::new_value(bool value) {
|
||||
if (value) {
|
||||
App.scheduler.set_timeout(this, this->delay_.value(), [this]() { this->output(true); });
|
||||
this->set_timeout(FILTER_TIMEOUT_ID, this->delay_.value(), [this]() { this->output(true); });
|
||||
return {};
|
||||
} else {
|
||||
App.scheduler.cancel_timeout(this);
|
||||
this->cancel_timeout(FILTER_TIMEOUT_ID);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
float DelayedOnFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
|
||||
|
||||
optional<bool> DelayedOffFilter::new_value(bool value) {
|
||||
if (!value) {
|
||||
App.scheduler.set_timeout(this, this->delay_.value(), [this]() { this->output(false); });
|
||||
this->set_timeout(FILTER_TIMEOUT_ID, this->delay_.value(), [this]() { this->output(false); });
|
||||
return {};
|
||||
} else {
|
||||
App.scheduler.cancel_timeout(this);
|
||||
this->cancel_timeout(FILTER_TIMEOUT_ID);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
float DelayedOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
|
||||
|
||||
optional<bool> InvertFilter::new_value(bool value) { return !value; }
|
||||
|
||||
// AutorepeatFilterBase
|
||||
@@ -110,18 +118,20 @@ optional<bool> LambdaFilter::new_value(bool value) { return this->f_(value); }
|
||||
|
||||
optional<bool> SettleFilter::new_value(bool value) {
|
||||
if (!this->steady_) {
|
||||
App.scheduler.set_timeout(this, this->delay_.value(), [this, value]() {
|
||||
this->set_timeout(FILTER_TIMEOUT_ID, this->delay_.value(), [this, value]() {
|
||||
this->steady_ = true;
|
||||
this->output(value);
|
||||
});
|
||||
return {};
|
||||
} else {
|
||||
this->steady_ = false;
|
||||
App.scheduler.set_timeout(this, this->delay_.value(), [this]() { this->steady_ = true; });
|
||||
this->set_timeout(FILTER_TIMEOUT_ID, this->delay_.value(), [this]() { this->steady_ = true; });
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
float SettleFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
|
||||
|
||||
} // namespace esphome::binary_sensor
|
||||
|
||||
#endif // USE_BINARY_SENSOR_FILTER
|
||||
|
||||
@@ -29,7 +29,7 @@ class Filter {
|
||||
Deduplicator<bool> dedup_;
|
||||
};
|
||||
|
||||
class TimeoutFilter : public Filter {
|
||||
class TimeoutFilter : public Filter, public Component {
|
||||
public:
|
||||
optional<bool> new_value(bool value) override { return value; }
|
||||
void input(bool value) override;
|
||||
@@ -39,10 +39,12 @@ class TimeoutFilter : public Filter {
|
||||
TemplatableFn<uint32_t> timeout_delay_{};
|
||||
};
|
||||
|
||||
class DelayedOnOffFilter final : public Filter {
|
||||
class DelayedOnOffFilter final : public Filter, public Component {
|
||||
public:
|
||||
optional<bool> new_value(bool value) override;
|
||||
|
||||
float get_setup_priority() const override;
|
||||
|
||||
template<typename T> void set_on_delay(T delay) { this->on_delay_ = delay; }
|
||||
template<typename T> void set_off_delay(T delay) { this->off_delay_ = delay; }
|
||||
|
||||
@@ -51,20 +53,24 @@ class DelayedOnOffFilter final : public Filter {
|
||||
TemplatableFn<uint32_t> off_delay_{};
|
||||
};
|
||||
|
||||
class DelayedOnFilter : public Filter {
|
||||
class DelayedOnFilter : public Filter, public Component {
|
||||
public:
|
||||
optional<bool> new_value(bool value) override;
|
||||
|
||||
float get_setup_priority() const override;
|
||||
|
||||
template<typename T> void set_delay(T delay) { this->delay_ = delay; }
|
||||
|
||||
protected:
|
||||
TemplatableFn<uint32_t> delay_{};
|
||||
};
|
||||
|
||||
class DelayedOffFilter : public Filter {
|
||||
class DelayedOffFilter : public Filter, public Component {
|
||||
public:
|
||||
optional<bool> new_value(bool value) override;
|
||||
|
||||
float get_setup_priority() const override;
|
||||
|
||||
template<typename T> void set_delay(T delay) { this->delay_ = delay; }
|
||||
|
||||
protected:
|
||||
@@ -140,10 +146,12 @@ class StatelessLambdaFilter : public Filter {
|
||||
optional<bool> (*f_)(bool);
|
||||
};
|
||||
|
||||
class SettleFilter : public Filter {
|
||||
class SettleFilter : public Filter, public Component {
|
||||
public:
|
||||
optional<bool> new_value(bool value) override;
|
||||
|
||||
float get_setup_priority() const override;
|
||||
|
||||
template<typename T> void set_delay(T delay) { this->delay_ = delay; }
|
||||
|
||||
protected:
|
||||
|
||||
@@ -78,43 +78,43 @@ void BME680Component::setup() {
|
||||
}
|
||||
|
||||
// Read calibration
|
||||
uint8_t coeff1[25];
|
||||
if (!this->read_bytes(BME680_REGISTER_COEFF1, coeff1, 25)) {
|
||||
uint8_t cal1[25];
|
||||
if (!this->read_bytes(BME680_REGISTER_COEFF1, cal1, 25)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
uint8_t coeff2[16];
|
||||
if (!this->read_bytes(BME680_REGISTER_COEFF2, coeff2, 16)) {
|
||||
uint8_t cal2[16];
|
||||
if (!this->read_bytes(BME680_REGISTER_COEFF2, cal2, 16)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
this->calibration_.t1 = coeff2[9] << 8 | coeff2[8];
|
||||
this->calibration_.t2 = coeff1[2] << 8 | coeff1[1];
|
||||
this->calibration_.t3 = coeff1[3];
|
||||
this->calibration_.t1 = cal2[9] << 8 | cal2[8];
|
||||
this->calibration_.t2 = cal1[2] << 8 | cal1[1];
|
||||
this->calibration_.t3 = cal1[3];
|
||||
|
||||
this->calibration_.h1 = coeff2[2] << 4 | (coeff2[1] & 0x0F);
|
||||
this->calibration_.h2 = coeff2[0] << 4 | coeff2[1] >> 4;
|
||||
this->calibration_.h3 = coeff2[3];
|
||||
this->calibration_.h4 = coeff2[4];
|
||||
this->calibration_.h5 = coeff2[5];
|
||||
this->calibration_.h6 = coeff2[6];
|
||||
this->calibration_.h7 = coeff2[7];
|
||||
this->calibration_.h1 = cal2[2] << 4 | (cal2[1] & 0x0F);
|
||||
this->calibration_.h2 = cal2[0] << 4 | cal2[1] >> 4;
|
||||
this->calibration_.h3 = cal2[3];
|
||||
this->calibration_.h4 = cal2[4];
|
||||
this->calibration_.h5 = cal2[5];
|
||||
this->calibration_.h6 = cal2[6];
|
||||
this->calibration_.h7 = cal2[7];
|
||||
|
||||
this->calibration_.p1 = coeff1[6] << 8 | coeff1[5];
|
||||
this->calibration_.p2 = coeff1[8] << 8 | coeff1[7];
|
||||
this->calibration_.p3 = coeff1[9];
|
||||
this->calibration_.p4 = coeff1[12] << 8 | coeff1[11];
|
||||
this->calibration_.p5 = coeff1[14] << 8 | coeff1[13];
|
||||
this->calibration_.p6 = coeff1[16];
|
||||
this->calibration_.p7 = coeff1[15];
|
||||
this->calibration_.p8 = coeff1[20] << 8 | coeff1[19];
|
||||
this->calibration_.p9 = coeff1[22] << 8 | coeff1[21];
|
||||
this->calibration_.p10 = coeff1[23];
|
||||
this->calibration_.p1 = cal1[6] << 8 | cal1[5];
|
||||
this->calibration_.p2 = cal1[8] << 8 | cal1[7];
|
||||
this->calibration_.p3 = cal1[9];
|
||||
this->calibration_.p4 = cal1[12] << 8 | cal1[11];
|
||||
this->calibration_.p5 = cal1[14] << 8 | cal1[13];
|
||||
this->calibration_.p6 = cal1[16];
|
||||
this->calibration_.p7 = cal1[15];
|
||||
this->calibration_.p8 = cal1[20] << 8 | cal1[19];
|
||||
this->calibration_.p9 = cal1[22] << 8 | cal1[21];
|
||||
this->calibration_.p10 = cal1[23];
|
||||
|
||||
this->calibration_.gh1 = coeff2[14];
|
||||
this->calibration_.gh2 = coeff2[12] << 8 | coeff2[13];
|
||||
this->calibration_.gh3 = coeff2[15];
|
||||
this->calibration_.gh1 = cal2[14];
|
||||
this->calibration_.gh2 = cal2[12] << 8 | cal2[13];
|
||||
this->calibration_.gh3 = cal2[15];
|
||||
|
||||
uint8_t temp_var = 0;
|
||||
if (!this->read_byte(0x02, &temp_var)) {
|
||||
|
||||
@@ -48,13 +48,13 @@ from esphome.const import (
|
||||
CONF_VISUAL,
|
||||
CONF_WEB_SERVER,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, Lambda, coroutine_with_priority
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import (
|
||||
entity_duplicate_validator,
|
||||
queue_entity_register,
|
||||
setup_entity,
|
||||
)
|
||||
from esphome.cpp_generator import LambdaExpression, MockObjClass
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
|
||||
@@ -487,57 +487,38 @@ CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema(
|
||||
)
|
||||
async def climate_control_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
|
||||
# All configured fields are folded into a single stateless lambda whose
|
||||
# constants live in flash; the action stores only a function pointer.
|
||||
# For custom_fan_mode/custom_preset the static-string path emits the
|
||||
# (const char *, size_t) overload of set_fan_mode/set_preset to avoid
|
||||
# constructing a std::string and calling runtime strlen.
|
||||
FIELDS = (
|
||||
(CONF_MODE, "set_mode", ClimateMode),
|
||||
(CONF_TARGET_TEMPERATURE, "set_target_temperature", cg.float_),
|
||||
(CONF_TARGET_TEMPERATURE_LOW, "set_target_temperature_low", cg.float_),
|
||||
(CONF_TARGET_TEMPERATURE_HIGH, "set_target_temperature_high", cg.float_),
|
||||
(CONF_TARGET_HUMIDITY, "set_target_humidity", cg.float_),
|
||||
(CONF_FAN_MODE, "set_fan_mode", ClimateFanMode),
|
||||
(CONF_CUSTOM_FAN_MODE, "set_fan_mode", cg.std_string),
|
||||
(CONF_PRESET, "set_preset", ClimatePreset),
|
||||
(CONF_CUSTOM_PRESET, "set_preset", cg.std_string),
|
||||
(CONF_SWING_MODE, "set_swing_mode", ClimateSwingMode),
|
||||
)
|
||||
|
||||
fwd_args = ", ".join(name for _, name in args)
|
||||
body_lines: list[str] = []
|
||||
|
||||
for conf_key, setter, type_ in FIELDS:
|
||||
if (value := config.get(conf_key)) is None:
|
||||
continue
|
||||
if isinstance(value, Lambda):
|
||||
inner = await cg.process_lambda(value, args, return_type=type_)
|
||||
body_lines.append(f"call.{setter}(({inner})({fwd_args}));")
|
||||
elif type_ is cg.std_string:
|
||||
# Static custom strings: emit a flash literal and pass the
|
||||
# UTF-8 byte length to skip the runtime strlen inside
|
||||
# set_fan_mode/set_preset.
|
||||
literal = cg.safe_exp(value)
|
||||
body_lines.append(
|
||||
f"call.{setter}({literal}, {len(value.encode('utf-8'))});"
|
||||
)
|
||||
else:
|
||||
body_lines.append(f"call.{setter}({cg.safe_exp(value)});")
|
||||
|
||||
# Match ControlAction::ApplyFn signature: const Ts &... for trigger args.
|
||||
apply_args = [
|
||||
(ClimateCall.operator("ref"), "call"),
|
||||
*((t.operator("const").operator("ref"), n) for t, n in args),
|
||||
]
|
||||
apply_lambda = LambdaExpression(
|
||||
["\n".join(body_lines)],
|
||||
apply_args,
|
||||
capture="",
|
||||
return_type=cg.void,
|
||||
)
|
||||
return cg.new_Pvariable(action_id, template_arg, paren, apply_lambda)
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
if (mode := config.get(CONF_MODE)) is not None:
|
||||
template_ = await cg.templatable(mode, args, ClimateMode)
|
||||
cg.add(var.set_mode(template_))
|
||||
if (target_temp := config.get(CONF_TARGET_TEMPERATURE)) is not None:
|
||||
template_ = await cg.templatable(target_temp, args, cg.float_)
|
||||
cg.add(var.set_target_temperature(template_))
|
||||
if (target_temp_low := config.get(CONF_TARGET_TEMPERATURE_LOW)) is not None:
|
||||
template_ = await cg.templatable(target_temp_low, args, cg.float_)
|
||||
cg.add(var.set_target_temperature_low(template_))
|
||||
if (target_temp_high := config.get(CONF_TARGET_TEMPERATURE_HIGH)) is not None:
|
||||
template_ = await cg.templatable(target_temp_high, args, cg.float_)
|
||||
cg.add(var.set_target_temperature_high(template_))
|
||||
if (target_humidity := config.get(CONF_TARGET_HUMIDITY)) is not None:
|
||||
template_ = await cg.templatable(target_humidity, args, cg.float_)
|
||||
cg.add(var.set_target_humidity(template_))
|
||||
if (fan_mode := config.get(CONF_FAN_MODE)) is not None:
|
||||
template_ = await cg.templatable(fan_mode, args, ClimateFanMode)
|
||||
cg.add(var.set_fan_mode(template_))
|
||||
if (custom_fan_mode := config.get(CONF_CUSTOM_FAN_MODE)) is not None:
|
||||
template_ = await cg.templatable(custom_fan_mode, args, cg.std_string)
|
||||
cg.add(var.set_custom_fan_mode(template_))
|
||||
if (preset := config.get(CONF_PRESET)) is not None:
|
||||
template_ = await cg.templatable(preset, args, ClimatePreset)
|
||||
cg.add(var.set_preset(template_))
|
||||
if (custom_preset := config.get(CONF_CUSTOM_PRESET)) is not None:
|
||||
template_ = await cg.templatable(custom_preset, args, cg.std_string)
|
||||
cg.add(var.set_custom_preset(template_))
|
||||
if (swing_mode := config.get(CONF_SWING_MODE)) is not None:
|
||||
template_ = await cg.templatable(swing_mode, args, ClimateSwingMode)
|
||||
cg.add(var.set_swing_mode(template_))
|
||||
return var
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.CORE)
|
||||
|
||||
@@ -5,25 +5,42 @@
|
||||
|
||||
namespace esphome::climate {
|
||||
|
||||
// All configured fields are baked into a single stateless lambda whose
|
||||
// constants live in flash. The action only stores one function pointer
|
||||
// plus one parent pointer, regardless of how many fields the user set.
|
||||
// Trigger args are forwarded to the apply function so user lambdas
|
||||
// (e.g. `target_temperature: !lambda "return x;"`) keep working.
|
||||
template<typename... Ts> class ControlAction : public Action<Ts...> {
|
||||
public:
|
||||
using ApplyFn = void (*)(ClimateCall &, const Ts &...);
|
||||
ControlAction(Climate *climate, ApplyFn apply) : climate_(climate), apply_(apply) {}
|
||||
explicit ControlAction(Climate *climate) : climate_(climate) {}
|
||||
|
||||
TEMPLATABLE_VALUE(ClimateMode, mode)
|
||||
TEMPLATABLE_VALUE(float, target_temperature)
|
||||
TEMPLATABLE_VALUE(float, target_temperature_low)
|
||||
TEMPLATABLE_VALUE(float, target_temperature_high)
|
||||
TEMPLATABLE_VALUE(float, target_humidity)
|
||||
TEMPLATABLE_VALUE(bool, away)
|
||||
TEMPLATABLE_VALUE(ClimateFanMode, fan_mode)
|
||||
TEMPLATABLE_VALUE(std::string, custom_fan_mode)
|
||||
TEMPLATABLE_VALUE(ClimatePreset, preset)
|
||||
TEMPLATABLE_VALUE(std::string, custom_preset)
|
||||
TEMPLATABLE_VALUE(ClimateSwingMode, swing_mode)
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
auto call = this->climate_->make_call();
|
||||
this->apply_(call, x...);
|
||||
call.set_mode(this->mode_.optional_value(x...));
|
||||
call.set_target_temperature(this->target_temperature_.optional_value(x...));
|
||||
call.set_target_temperature_low(this->target_temperature_low_.optional_value(x...));
|
||||
call.set_target_temperature_high(this->target_temperature_high_.optional_value(x...));
|
||||
call.set_target_humidity(this->target_humidity_.optional_value(x...));
|
||||
if (away_.has_value()) {
|
||||
call.set_preset(away_.value(x...) ? CLIMATE_PRESET_AWAY : CLIMATE_PRESET_HOME);
|
||||
}
|
||||
call.set_fan_mode(this->fan_mode_.optional_value(x...));
|
||||
call.set_fan_mode(this->custom_fan_mode_.optional_value(x...));
|
||||
call.set_preset(this->preset_.optional_value(x...));
|
||||
call.set_preset(this->custom_preset_.optional_value(x...));
|
||||
call.set_swing_mode(this->swing_mode_.optional_value(x...));
|
||||
call.perform();
|
||||
}
|
||||
|
||||
protected:
|
||||
Climate *climate_;
|
||||
ApplyFn apply_;
|
||||
};
|
||||
|
||||
class ControlTrigger : public Trigger<ClimateCall &> {
|
||||
|
||||
@@ -374,8 +374,7 @@ void Climate::save_state_(const ClimateTraits &traits) {
|
||||
#define TEMP_IGNORE_MEMACCESS
|
||||
#endif
|
||||
ClimateDeviceRestoreState state{};
|
||||
// initialize as zero (including padding) to prevent random data on stack triggering erase
|
||||
// NOLINTNEXTLINE(bugprone-raw-memory-call-on-non-trivial-type) -- intentional bytewise zero for RTC save
|
||||
// initialize as zero to prevent random data on stack triggering erase
|
||||
memset(&state, 0, sizeof(ClimateDeviceRestoreState));
|
||||
#ifdef TEMP_IGNORE_MEMACCESS
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
@@ -17,13 +17,11 @@ constexpr std::uintptr_t MBR_PARAM_PAGE_ADDR = 0xFFC;
|
||||
constexpr std::uintptr_t MBR_BOOTLOADER_ADDR = 0xFF8;
|
||||
|
||||
static inline uint32_t read_mem_u32(uintptr_t addr) {
|
||||
// NOLINTNEXTLINE(performance-no-int-to-ptr,clang-analyzer-core.FixedAddressDereference)
|
||||
return *reinterpret_cast<volatile uint32_t *>(addr);
|
||||
return *reinterpret_cast<volatile uint32_t *>(addr); // NOLINT(performance-no-int-to-ptr)
|
||||
}
|
||||
|
||||
static inline uint8_t read_mem_u8(uintptr_t addr) {
|
||||
// NOLINTNEXTLINE(performance-no-int-to-ptr,clang-analyzer-core.FixedAddressDereference)
|
||||
return *reinterpret_cast<volatile uint8_t *>(addr);
|
||||
return *reinterpret_cast<volatile uint8_t *>(addr); // NOLINT(performance-no-int-to-ptr)
|
||||
}
|
||||
|
||||
// defines from https://github.com/adafruit/Adafruit_nRF52_Bootloader which prints those information
|
||||
@@ -100,7 +98,6 @@ void DebugComponent::log_partition_info_() {
|
||||
#define NRF_PERIPH_ENABLED(periph, reg) \
|
||||
YESNO(((reg)->ENABLE & periph##_ENABLE_ENABLE_Msk) == (periph##_ENABLE_ENABLE_Enabled << periph##_ENABLE_ENABLE_Pos))
|
||||
|
||||
// NOLINTBEGIN(clang-analyzer-core.FixedAddressDereference) -- nRF peripheral registers are MMIO at fixed addresses
|
||||
static void log_peripherals_info() {
|
||||
// most peripherals are enabled only when in use so ESP_LOGV is enough
|
||||
ESP_LOGV(TAG, "Peripherals status:");
|
||||
@@ -134,7 +131,6 @@ static void log_peripherals_info() {
|
||||
YESNO((NRF_CRYPTOCELL->ENABLE & CRYPTOCELL_ENABLE_ENABLE_Msk) ==
|
||||
(CRYPTOCELL_ENABLE_ENABLE_Enabled << CRYPTOCELL_ENABLE_ENABLE_Pos)));
|
||||
}
|
||||
// NOLINTEND(clang-analyzer-core.FixedAddressDereference)
|
||||
#undef NRF_PERIPH_ENABLED
|
||||
#endif
|
||||
|
||||
@@ -163,9 +159,8 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
|
||||
char *buf = buffer.data();
|
||||
|
||||
// Main supply status
|
||||
// NOLINTNEXTLINE(clang-analyzer-core.FixedAddressDereference) -- NRF_POWER is MMIO at a fixed address
|
||||
auto regstatus = nrf_power_mainregstatus_get(NRF_POWER);
|
||||
const char *supply_status = (regstatus == NRF_POWER_MAINREGSTATUS_NORMAL) ? "Normal voltage." : "High voltage.";
|
||||
const char *supply_status =
|
||||
(nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_NORMAL) ? "Normal voltage." : "High voltage.";
|
||||
ESP_LOGD(TAG, "Main supply status: %s", supply_status);
|
||||
pos = buf_append_str(buf, size, pos, "|Main supply status: ");
|
||||
pos = buf_append_str(buf, size, pos, supply_status);
|
||||
|
||||
@@ -193,14 +193,11 @@ def _validate_ex1_wakeup_mode(value):
|
||||
|
||||
|
||||
def _validate_sleep_duration(value: core.TimePeriod) -> core.TimePeriod:
|
||||
if CORE.is_bk72xx:
|
||||
max_duration = core.TimePeriod(hours=36)
|
||||
if value > max_duration:
|
||||
raise cv.Invalid("sleep duration cannot be more than 36 hours on BK72XX")
|
||||
elif CORE.using_zephyr:
|
||||
max_duration = core.TimePeriod(days=49)
|
||||
if value > max_duration:
|
||||
raise cv.Invalid("sleep duration cannot be more than 49 days on Zephyr")
|
||||
if not CORE.is_bk72xx:
|
||||
return value
|
||||
max_duration = core.TimePeriod(hours=36)
|
||||
if value > max_duration:
|
||||
raise cv.Invalid("sleep duration cannot be more than 36 hours on BK72XX")
|
||||
return value
|
||||
|
||||
|
||||
|
||||
@@ -9,11 +9,18 @@ static const char *const TAG = "deep_sleep";
|
||||
// 5 seconds for deep sleep to ensure clean disconnect from Home Assistant
|
||||
static const uint32_t TEARDOWN_TIMEOUT_DEEP_SLEEP_MS = 5000;
|
||||
|
||||
bool global_has_deep_sleep = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
bool global_has_deep_sleep = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
std::atomic<DeepSleepComponent *> global_deep_sleep; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
void DeepSleepComponent::setup() {
|
||||
#ifdef USE_ZEPHYR
|
||||
k_sem_init(&this->wakeup_sem_, 0, 1);
|
||||
#endif
|
||||
global_has_deep_sleep = true;
|
||||
this->schedule_sleep_();
|
||||
// It can be used from another thread for waking up the device.
|
||||
// It should be called as last item in setup.
|
||||
global_deep_sleep.store(this);
|
||||
}
|
||||
|
||||
void DeepSleepComponent::schedule_sleep_() {
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include <atomic>
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#include <esp_sleep.h>
|
||||
#endif
|
||||
@@ -13,6 +15,10 @@
|
||||
#include "esphome/core/time.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_ZEPHYR
|
||||
#include <zephyr/kernel.h>
|
||||
#endif
|
||||
|
||||
#include <cinttypes>
|
||||
|
||||
namespace esphome {
|
||||
@@ -119,6 +125,9 @@ class DeepSleepComponent : public Component {
|
||||
|
||||
void prevent_deep_sleep();
|
||||
void allow_deep_sleep();
|
||||
#ifdef USE_ZEPHYR
|
||||
void wakeup();
|
||||
#endif
|
||||
|
||||
protected:
|
||||
// Returns nullopt if no run duration is set. Otherwise, returns the run
|
||||
@@ -158,6 +167,9 @@ class DeepSleepComponent : public Component {
|
||||
optional<uint32_t> run_duration_;
|
||||
bool next_enter_deep_sleep_{false};
|
||||
bool prevent_{false};
|
||||
#ifdef USE_ZEPHYR
|
||||
k_sem wakeup_sem_;
|
||||
#endif
|
||||
};
|
||||
|
||||
extern bool global_has_deep_sleep; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
@@ -244,5 +256,8 @@ template<typename... Ts> class AllowDeepSleepAction : public Action<Ts...>, publ
|
||||
void play(const Ts &...x) override { this->parent_->allow_deep_sleep(); }
|
||||
};
|
||||
|
||||
extern std::atomic<DeepSleepComponent *>
|
||||
global_deep_sleep; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
} // namespace deep_sleep
|
||||
} // namespace esphome
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
#include "deep_sleep_component.h"
|
||||
#ifdef USE_ZEPHYR
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/wake.h"
|
||||
#include <zephyr/sys/poweroff.h>
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/stats/stats.h>
|
||||
#include <zephyr/pm/pm.h>
|
||||
|
||||
namespace esphome::deep_sleep {
|
||||
|
||||
static const char *const TAG = "deep_sleep";
|
||||
|
||||
void DeepSleepComponent::wakeup() { k_sem_give(&this->wakeup_sem_); }
|
||||
|
||||
optional<uint32_t> DeepSleepComponent::get_run_duration_() const { return this->run_duration_; }
|
||||
|
||||
void DeepSleepComponent::dump_config_platform_() {}
|
||||
@@ -15,8 +19,9 @@ void DeepSleepComponent::dump_config_platform_() {}
|
||||
bool DeepSleepComponent::prepare_to_sleep_() { return true; }
|
||||
|
||||
void DeepSleepComponent::deep_sleep_() {
|
||||
k_timeout_t sleep_duration = K_FOREVER;
|
||||
if (this->sleep_duration_.has_value()) {
|
||||
esphome::internal::wakeable_delay(static_cast<uint32_t>(*this->sleep_duration_ / 1000));
|
||||
sleep_duration = K_USEC(*this->sleep_duration_);
|
||||
} else {
|
||||
#ifndef USE_ZIGBEE
|
||||
// the device can be woken up through one of the following signals:
|
||||
@@ -28,12 +33,11 @@ void DeepSleepComponent::deep_sleep_() {
|
||||
//
|
||||
// The system is reset when it wakes up from System OFF mode.
|
||||
sys_poweroff();
|
||||
#else
|
||||
esphome::internal::wakeable_delay(UINT32_MAX);
|
||||
#endif
|
||||
}
|
||||
const bool woke = esphome::wake_request_take();
|
||||
if (woke) {
|
||||
// It might wake up immediately if k_sem_give was called again after wake up
|
||||
int ret = k_sem_take(&this->wakeup_sem_, sleep_duration);
|
||||
if (ret == 0) {
|
||||
ESP_LOGD(TAG, "Woken up by another thread");
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Timeout expired (normal sleep)");
|
||||
|
||||
@@ -29,7 +29,7 @@ class DemoAlarmControlPanel : public AlarmControlPanel, public Component {
|
||||
protected:
|
||||
void control(const AlarmControlPanelCall &call) override {
|
||||
auto state = call.get_state().value_or(ACP_STATE_DISARMED);
|
||||
const auto &code = call.get_code();
|
||||
auto code = call.get_code();
|
||||
switch (state) {
|
||||
case ACP_STATE_ARMED_AWAY:
|
||||
if (this->get_requires_code_to_arm()) {
|
||||
|
||||
@@ -104,9 +104,8 @@ int8_t CircularCommandQueue::enqueue(std::unique_ptr<Command> cmd) {
|
||||
if (this->is_full()) {
|
||||
ESP_LOGE(TAG, "Command queue is full");
|
||||
return -1;
|
||||
} else if (this->is_empty()) {
|
||||
} else if (this->is_empty())
|
||||
front_++;
|
||||
}
|
||||
rear_ = (rear_ + 1) % COMMAND_QUEUE_SIZE;
|
||||
commands_[rear_] = std::move(cmd); // Transfer ownership using std::move
|
||||
return 1;
|
||||
|
||||
@@ -56,7 +56,8 @@ bool E131AddressableLightEffect::process_(int universe, const E131Packet &packet
|
||||
// limit amount of lights per universe and received
|
||||
// packet.count is the number of DMX bytes including start code; divide by channels to get the number of lights
|
||||
int lights_in_packet = (packet.count > 0) ? (packet.count - 1) / channels_ : 0;
|
||||
int output_end = std::min({it->size(), output_offset + get_lights_per_universe(), output_offset + lights_in_packet});
|
||||
int output_end =
|
||||
std::min(it->size(), std::min(output_offset + get_lights_per_universe(), output_offset + lights_in_packet));
|
||||
auto *input_data = packet.values + 1;
|
||||
|
||||
auto effect_name = get_name();
|
||||
|
||||
@@ -5,15 +5,6 @@
|
||||
// Implementation based on:
|
||||
// https://github.com/sciosense/ENS160_driver
|
||||
|
||||
// For best performance, the sensor shall be operated in normal indoor air in the range -5 to 60°C
|
||||
// (typical: 25°C); relative humidity: 20 to 80%RH (typical: 50%RH), non-condensing with no aggressive
|
||||
// or poisonous gases present. Prolonged exposure to environments outside these conditions can affect
|
||||
// performance and lifetime of the sensor.
|
||||
// The sensor is designed for indoor use and is not waterproof or dustproof. It should be protected from
|
||||
// water, condensation, dust, and aggressive gases. Note that the status will only be stored in non-volatile
|
||||
// memory after an initial 24 h of continuous operation. If unpowered before the conclusion of that period,
|
||||
// the ENS160 will resume "Initial Start-up" mode after re-powering.
|
||||
|
||||
#include "ens160_base.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
@@ -23,9 +14,7 @@ namespace ens160_base {
|
||||
|
||||
static const char *const TAG = "ens160";
|
||||
|
||||
// Datasheet specifies 10ms, but some users report that 10ms is not sufficient for the
|
||||
// sensor to boot and be ready for commands. 11ms seems to be a safe value.
|
||||
static const uint8_t ENS160_BOOTING = 11;
|
||||
static const uint8_t ENS160_BOOTING = 10;
|
||||
|
||||
static const uint16_t ENS160_PART_ID = 0x0160;
|
||||
|
||||
@@ -102,8 +91,6 @@ void ENS160Component::setup() {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
delay(ENS160_BOOTING);
|
||||
|
||||
// clear command
|
||||
if (!this->write_byte(ENS160_REG_COMMAND, ENS160_COMMAND_NOP)) {
|
||||
this->error_code_ = WRITE_FAILED;
|
||||
@@ -115,7 +102,6 @@ void ENS160Component::setup() {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
delay(ENS160_BOOTING);
|
||||
|
||||
// read firmware version
|
||||
if (!this->write_byte(ENS160_REG_COMMAND, ENS160_COMMAND_GET_APPVER)) {
|
||||
@@ -123,8 +109,6 @@ void ENS160Component::setup() {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
delay(ENS160_BOOTING);
|
||||
|
||||
uint8_t version_data[3];
|
||||
if (!this->read_bytes(ENS160_REG_GPR_READ_4, version_data, 3)) {
|
||||
this->error_code_ = READ_FAILED;
|
||||
@@ -239,6 +223,7 @@ void ENS160Component::update() {
|
||||
if (this->aqi_ != nullptr) {
|
||||
// remove reserved bits, just in case they are used in future
|
||||
data_aqi = ENS160_DATA_AQI & data_aqi;
|
||||
|
||||
this->aqi_->publish_state(data_aqi);
|
||||
}
|
||||
|
||||
|
||||
@@ -729,9 +729,6 @@ ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
|
||||
"dev": cv.Version(5, 5, 4),
|
||||
}
|
||||
ESP_IDF_PLATFORM_VERSION_LOOKUP = {
|
||||
cv.Version(
|
||||
6, 0, 1
|
||||
): "https://github.com/pioarduino/platform-espressif32.git#prep_IDF6",
|
||||
cv.Version(
|
||||
6, 0, 0
|
||||
): "https://github.com/pioarduino/platform-espressif32.git#prep_IDF6",
|
||||
@@ -1727,16 +1724,15 @@ async def to_code(config):
|
||||
CORE.relative_internal_path(".espressif")
|
||||
)
|
||||
|
||||
# Both ESP-IDF and ESP32 Arduino builds generate IDF app metadata. Keep
|
||||
# volatile build path/time data out of the binary so equivalent projects can
|
||||
# produce reproducible outputs and downstream tooling can reuse artifacts.
|
||||
add_idf_sdkconfig_option("CONFIG_APP_REPRODUCIBLE_BUILD", True)
|
||||
|
||||
if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
|
||||
cg.add_build_flag("-DUSE_ESP_IDF")
|
||||
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF")
|
||||
if use_platformio:
|
||||
cg.add_platformio_option("framework", "espidf")
|
||||
# Strip volatile build path/time metadata from PlatformIO-managed
|
||||
# ESP-IDF builds so equivalent projects can produce reproducible
|
||||
# outputs and downstream tooling can safely reuse artifacts.
|
||||
add_idf_sdkconfig_option("CONFIG_APP_REPRODUCIBLE_BUILD", True)
|
||||
|
||||
# Wrap std::__throw_* functions to abort immediately, eliminating ~3KB of
|
||||
# exception class overhead. See throw_stubs.cpp for implementation.
|
||||
@@ -1753,17 +1749,7 @@ async def to_code(config):
|
||||
|
||||
# Wrap FILE*-based printf functions to eliminate newlib's _vfprintf_r
|
||||
# (~11 KB). See printf_stubs.cpp for implementation.
|
||||
#
|
||||
# The wrap is only beneficial against newlib. Picolibc's tinystdio
|
||||
# implements vsnprintf by building a string-output FILE and calling
|
||||
# vfprintf, so vfprintf is unconditionally linked in by any caller
|
||||
# of snprintf/vsnprintf — effectively every build — and the wrap
|
||||
# saves nothing while costing ~170 B of shim. IDF 5.x defaults to
|
||||
# newlib on every variant; IDF 6.0+ switches to picolibc on every
|
||||
# variant.
|
||||
if conf[CONF_ADVANCED][CONF_ENABLE_FULL_PRINTF] or idf_version() >= cv.Version(
|
||||
6, 0, 0
|
||||
):
|
||||
if conf[CONF_ADVANCED][CONF_ENABLE_FULL_PRINTF]:
|
||||
cg.add_define("USE_FULL_PRINTF")
|
||||
else:
|
||||
for symbol in ("vprintf", "printf", "fprintf", "vfprintf"):
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "crash_handler.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "preferences.h"
|
||||
#include <esp_clk_tree.h>
|
||||
#include <esp_cpu.h>
|
||||
#include <esp_idf_version.h>
|
||||
#include <esp_ota_ops.h>
|
||||
#include <esp_task_wdt.h>
|
||||
#include <esp_timer.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
@@ -13,7 +22,63 @@ extern "C" __attribute__((weak)) void initArduino() {}
|
||||
|
||||
namespace esphome {
|
||||
|
||||
// HAL functions live in hal.cpp. This file keeps only the loop task setup.
|
||||
void HOT yield() { vPortYield(); }
|
||||
// Use xTaskGetTickCount() when tick rate is 1 kHz (ESPHome's default via sdkconfig),
|
||||
// falling back to esp_timer for non-standard rates. IRAM_ATTR is required because
|
||||
// Wiegand and ZyAura call millis() from IRAM_ATTR ISR handlers on ESP32.
|
||||
// xTaskGetTickCountFromISR() is used in ISR context to satisfy the FreeRTOS API contract.
|
||||
uint32_t IRAM_ATTR HOT millis() {
|
||||
#if CONFIG_FREERTOS_HZ == 1000
|
||||
if (xPortInIsrContext()) [[unlikely]] {
|
||||
return xTaskGetTickCountFromISR();
|
||||
}
|
||||
return xTaskGetTickCount();
|
||||
#else
|
||||
return micros_to_millis(static_cast<uint64_t>(esp_timer_get_time()));
|
||||
#endif
|
||||
}
|
||||
// millis_64() stays on esp_timer — a different clock from xTaskGetTickCount(). This is
|
||||
// safe because the two are never cross-compared: millis() values are only used for
|
||||
// millis()-vs-millis() deltas (feed_wdt, warn_blocking, component start time), while
|
||||
// millis_64() is used by the Scheduler and uptime sensors. On ESP32 (USE_NATIVE_64BIT_TIME),
|
||||
// Scheduler::millis_64_from_(now) discards the 32-bit now and calls millis_64() directly,
|
||||
// so the Scheduler is internally consistent on the esp_timer clock.
|
||||
uint64_t HOT millis_64() { return micros_to_millis<uint64_t>(static_cast<uint64_t>(esp_timer_get_time())); }
|
||||
void HOT delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); }
|
||||
uint32_t IRAM_ATTR HOT micros() { return (uint32_t) esp_timer_get_time(); }
|
||||
void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); }
|
||||
void arch_restart() {
|
||||
esp_restart();
|
||||
// restart() doesn't always end execution
|
||||
while (true) { // NOLINT(clang-diagnostic-unreachable-code)
|
||||
yield();
|
||||
}
|
||||
}
|
||||
|
||||
void arch_init() {
|
||||
#ifdef USE_ESP32_CRASH_HANDLER
|
||||
// Read crash data from previous boot before anything else
|
||||
esp32::crash_handler_read_and_clear();
|
||||
#endif
|
||||
|
||||
// Enable the task watchdog only on the loop task (from which we're currently running)
|
||||
esp_task_wdt_add(nullptr);
|
||||
|
||||
// Handle OTA rollback: mark partition valid immediately unless USE_OTA_ROLLBACK is enabled,
|
||||
// in which case safe_mode will mark it valid after confirming successful boot.
|
||||
#ifndef USE_OTA_ROLLBACK
|
||||
esp_ota_mark_app_valid_cancel_rollback();
|
||||
#endif
|
||||
}
|
||||
void HOT arch_feed_wdt() { esp_task_wdt_reset(); }
|
||||
|
||||
uint32_t arch_get_cpu_cycle_count() { return esp_cpu_get_cycle_count(); }
|
||||
uint32_t arch_get_cpu_freq_hz() {
|
||||
uint32_t freq = 0;
|
||||
esp_clk_tree_src_get_freq_hz(SOC_MOD_CLK_CPU, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &freq);
|
||||
return freq;
|
||||
}
|
||||
|
||||
TaskHandle_t loop_task_handle = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
static StaticTask_t loop_task_tcb; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
static StackType_t
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
#ifdef USE_ESP32
|
||||
|
||||
// defines.h must come before crash_handler.h so USE_ESP32_CRASH_HANDLER is set
|
||||
// before crash_handler.h's #ifdef-guarded namespace block is parsed.
|
||||
#include "esphome/core/defines.h"
|
||||
#include "crash_handler.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
#include <esp_clk_tree.h>
|
||||
#include <esp_ota_ops.h>
|
||||
#include <esp_system.h>
|
||||
#include <esp_task_wdt.h>
|
||||
#include <esp_timer.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
// Empty esp32 namespace block to satisfy ci-custom's lint_namespace check.
|
||||
// HAL functions live in namespace esphome (root) — they are not part of the
|
||||
// esp32 component's API.
|
||||
namespace esphome::esp32 {} // namespace esphome::esp32
|
||||
|
||||
namespace esphome {
|
||||
|
||||
// Use xTaskGetTickCount() when tick rate is 1 kHz (ESPHome's default via sdkconfig),
|
||||
// falling back to esp_timer for non-standard rates. IRAM_ATTR is required because
|
||||
// Wiegand and ZyAura call millis() from IRAM_ATTR ISR handlers on ESP32.
|
||||
// xTaskGetTickCountFromISR() is used in ISR context to satisfy the FreeRTOS API contract.
|
||||
uint32_t IRAM_ATTR HOT millis() {
|
||||
#if CONFIG_FREERTOS_HZ == 1000
|
||||
if (xPortInIsrContext()) [[unlikely]] {
|
||||
return xTaskGetTickCountFromISR();
|
||||
}
|
||||
return xTaskGetTickCount();
|
||||
#else
|
||||
return micros_to_millis(static_cast<uint64_t>(esp_timer_get_time()));
|
||||
#endif
|
||||
}
|
||||
|
||||
void arch_restart() {
|
||||
esp_restart();
|
||||
// restart() doesn't always end execution
|
||||
while (true) { // NOLINT(clang-diagnostic-unreachable-code)
|
||||
yield();
|
||||
}
|
||||
}
|
||||
|
||||
void arch_init() {
|
||||
#ifdef USE_ESP32_CRASH_HANDLER
|
||||
// Read crash data from previous boot before anything else
|
||||
esp32::crash_handler_read_and_clear();
|
||||
#endif
|
||||
|
||||
// Enable the task watchdog only on the loop task (from which we're currently running)
|
||||
esp_task_wdt_add(nullptr);
|
||||
|
||||
// Handle OTA rollback: mark partition valid immediately unless USE_OTA_ROLLBACK is enabled,
|
||||
// in which case safe_mode will mark it valid after confirming successful boot.
|
||||
#ifndef USE_OTA_ROLLBACK
|
||||
esp_ota_mark_app_valid_cancel_rollback();
|
||||
#endif
|
||||
}
|
||||
|
||||
uint32_t arch_get_cpu_freq_hz() {
|
||||
uint32_t freq = 0;
|
||||
esp_clk_tree_src_get_freq_hz(SOC_MOD_CLK_CPU, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &freq);
|
||||
return freq;
|
||||
}
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESP32
|
||||
@@ -1,52 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include <cstdint>
|
||||
#include <esp_attr.h>
|
||||
#include <esp_cpu.h>
|
||||
#include <esp_task_wdt.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
#include "esphome/core/time_conversion.h"
|
||||
|
||||
#ifndef PROGMEM
|
||||
#define PROGMEM
|
||||
#endif
|
||||
|
||||
namespace esphome::esp32 {}
|
||||
|
||||
namespace esphome {
|
||||
|
||||
// Forward decl from helpers.h (esphome/core/helpers.h) — kept here so this
|
||||
// header does not need to pull the rest of helpers.h.
|
||||
// NOLINTNEXTLINE(readability-redundant-declaration)
|
||||
void delay_microseconds_safe(uint32_t us);
|
||||
|
||||
/// Returns true when executing inside an interrupt handler.
|
||||
__attribute__((always_inline)) inline bool in_isr_context() { return xPortInIsrContext() != 0; }
|
||||
|
||||
// Forward decl from <esp_timer.h>.
|
||||
// NOLINTNEXTLINE(readability-redundant-declaration)
|
||||
extern "C" int64_t esp_timer_get_time(void);
|
||||
|
||||
__attribute__((always_inline)) inline void yield() { vPortYield(); }
|
||||
__attribute__((always_inline)) inline void delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); }
|
||||
__attribute__((always_inline)) inline uint32_t micros() { return static_cast<uint32_t>(esp_timer_get_time()); }
|
||||
uint32_t millis();
|
||||
__attribute__((always_inline)) inline uint64_t millis_64() {
|
||||
return micros_to_millis<uint64_t>(static_cast<uint64_t>(esp_timer_get_time()));
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||
__attribute__((always_inline)) inline void delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); }
|
||||
__attribute__((always_inline)) inline void arch_feed_wdt() { esp_task_wdt_reset(); }
|
||||
__attribute__((always_inline)) inline uint32_t arch_get_cpu_cycle_count() { return esp_cpu_get_cycle_count(); }
|
||||
|
||||
void arch_init();
|
||||
uint32_t arch_get_cpu_freq_hz();
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESP32
|
||||
@@ -18,12 +18,6 @@ struct NVSData {
|
||||
|
||||
static std::vector<NVSData> s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
// open() runs from app_main() before the logger is initialized, so any failure
|
||||
// must be deferred until after global_logger is set. This is emitted from the
|
||||
// first make_preference() call, which runs from the generated setup() after
|
||||
// log->pre_setup() has run at EARLY_INIT priority.
|
||||
static esp_err_t s_open_err = ESP_OK; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
bool ESP32PreferenceBackend::save(const uint8_t *data, size_t len) {
|
||||
// try find in pending saves and update that
|
||||
for (auto &obj : s_pending_save) {
|
||||
@@ -76,14 +70,12 @@ bool ESP32PreferenceBackend::load(uint8_t *data, size_t len) {
|
||||
}
|
||||
|
||||
void ESP32Preferences::open() {
|
||||
// Runs from app_main() before the logger is initialized; any logging here
|
||||
// must be deferred. See s_open_err and make_preference() below.
|
||||
nvs_flash_init();
|
||||
esp_err_t err = nvs_open("esphome", NVS_READWRITE, &this->nvs_handle);
|
||||
if (err == 0)
|
||||
return;
|
||||
|
||||
s_open_err = err;
|
||||
ESP_LOGW(TAG, "nvs_open failed: %s - erasing NVS", esp_err_to_name(err));
|
||||
nvs_flash_deinit();
|
||||
nvs_flash_erase();
|
||||
nvs_flash_init();
|
||||
@@ -95,14 +87,6 @@ void ESP32Preferences::open() {
|
||||
}
|
||||
|
||||
ESPPreferenceObject ESP32Preferences::make_preference(size_t length, uint32_t type) {
|
||||
if (s_open_err != ESP_OK) {
|
||||
if (this->nvs_handle == 0) {
|
||||
ESP_LOGW(TAG, "nvs_open failed: %s - NVS unavailable", esp_err_to_name(s_open_err));
|
||||
} else {
|
||||
ESP_LOGW(TAG, "nvs_open failed: %s - erased NVS", esp_err_to_name(s_open_err));
|
||||
}
|
||||
s_open_err = ESP_OK;
|
||||
}
|
||||
auto *pref = new ESP32PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory)
|
||||
pref->nvs_handle = this->nvs_handle;
|
||||
pref->key = type;
|
||||
|
||||
@@ -1,38 +1,32 @@
|
||||
/*
|
||||
* Linker wrap stubs for FILE*-based printf functions (newlib only).
|
||||
* Linker wrap stubs for FILE*-based printf functions.
|
||||
*
|
||||
* ESP-IDF SDK components (gpio driver, ringbuf, log_write) reference
|
||||
* fprintf(), printf(), vprintf(), and vfprintf(), which on newlib pull
|
||||
* in _vfprintf_r (~11 KB) — a separate implementation from the one used
|
||||
* by snprintf/vsnprintf that handles FILE* stream I/O with buffering.
|
||||
* fprintf(), printf(), vprintf(), and vfprintf() which pull in the full
|
||||
* printf implementation (~11 KB on newlib's _vfprintf_r, ~2.8 KB on
|
||||
* picolibc's vfprintf). This is a separate implementation from the one
|
||||
* used by snprintf/vsnprintf that handles FILE* stream I/O with buffering
|
||||
* and locking.
|
||||
*
|
||||
* ESPHome replaces the ESP-IDF log handler via esp_log_set_vprintf_(),
|
||||
* so the SDK's vprintf() path is dead code at runtime. The fprintf()
|
||||
* and printf() calls in SDK components are only in debug/assert paths
|
||||
* (gpio_dump_io_configuration, ringbuf diagnostics) that are either
|
||||
* GC'd or never called. Crash backtraces and panic output are
|
||||
* unaffected; they use esp_rom_printf() which is a ROM function and
|
||||
* does not go through libc.
|
||||
* unaffected — they use esp_rom_printf() which is a ROM function
|
||||
* and does not go through libc.
|
||||
*
|
||||
* This wrap is newlib-only. On picolibc, vsnprintf is implemented as
|
||||
* vfprintf into a string-output FILE, so vfprintf is unconditionally
|
||||
* linked in by any caller of snprintf/vsnprintf and the wrap can never
|
||||
* elide it — it just adds shim cost. Codegen forces USE_FULL_PRINTF
|
||||
* on picolibc builds (IDF 6.0+ on all variants) so this file compiles
|
||||
* to nothing there; the #error below catches a desynchronised gate.
|
||||
* These stubs redirect through vsnprintf() (which uses _svfprintf_r
|
||||
* already in the binary) and fwrite(), allowing the linker to
|
||||
* dead-code eliminate _vfprintf_r.
|
||||
*
|
||||
* Saves ~11 KB of flash on newlib.
|
||||
* Saves ~11 KB of flash.
|
||||
*
|
||||
* To disable this wrap on newlib, set enable_full_printf: true in the
|
||||
* esp32 advanced config section.
|
||||
* To disable these wraps, set enable_full_printf: true in the esp32
|
||||
* advanced config section.
|
||||
*/
|
||||
|
||||
#if defined(USE_ESP_IDF) && !defined(USE_FULL_PRINTF)
|
||||
|
||||
#ifdef __PICOLIBC__
|
||||
#error "printf wrap is net-negative on picolibc; codegen should set USE_FULL_PRINTF"
|
||||
#endif
|
||||
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
|
||||
@@ -40,9 +34,6 @@
|
||||
|
||||
namespace esphome::esp32 {}
|
||||
|
||||
// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
|
||||
extern "C" {
|
||||
|
||||
static constexpr size_t PRINTF_BUFFER_SIZE = 512;
|
||||
|
||||
// These stubs are essentially dead code at runtime — ESPHome replaces the
|
||||
@@ -64,16 +55,14 @@ static int write_printf_buffer(FILE *stream, char *buf, int len) {
|
||||
return len;
|
||||
}
|
||||
|
||||
// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
|
||||
extern "C" {
|
||||
|
||||
int __wrap_vprintf(const char *fmt, va_list ap) {
|
||||
char buf[PRINTF_BUFFER_SIZE];
|
||||
return write_printf_buffer(stdout, buf, vsnprintf(buf, sizeof(buf), fmt, ap));
|
||||
}
|
||||
|
||||
int __wrap_vfprintf(FILE *stream, const char *fmt, va_list ap) {
|
||||
char buf[PRINTF_BUFFER_SIZE];
|
||||
return write_printf_buffer(stream, buf, vsnprintf(buf, sizeof(buf), fmt, ap));
|
||||
}
|
||||
|
||||
int __wrap_printf(const char *fmt, ...) {
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
@@ -82,6 +71,11 @@ int __wrap_printf(const char *fmt, ...) {
|
||||
return len;
|
||||
}
|
||||
|
||||
int __wrap_vfprintf(FILE *stream, const char *fmt, va_list ap) {
|
||||
char buf[PRINTF_BUFFER_SIZE];
|
||||
return write_printf_buffer(stream, buf, vsnprintf(buf, sizeof(buf), fmt, ap));
|
||||
}
|
||||
|
||||
int __wrap_fprintf(FILE *stream, const char *fmt, ...) {
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
|
||||
@@ -104,7 +104,7 @@ ESPBTUUID ESPBTUUID::as_128bit() const {
|
||||
} else {
|
||||
uuid32 = this->uuid_.uuid.uuid16;
|
||||
}
|
||||
for (uint16_t i = 0; i < this->uuid_.len; i++) {
|
||||
for (uint8_t i = 0; i < this->uuid_.len; i++) {
|
||||
data[12 + i] = ((uuid32 >> i * 8) & 0xFF);
|
||||
}
|
||||
return ESPBTUUID::from_raw(data);
|
||||
|
||||
@@ -166,9 +166,8 @@ void ESP32BLETracker::loop() {
|
||||
ClientStateCounts counts = this->count_client_states_();
|
||||
if (counts != this->client_state_counts_) {
|
||||
this->client_state_counts_ = counts;
|
||||
ESP_LOGD(TAG, "connecting: %d, discovered: %d, disconnecting: %d, active: %d",
|
||||
this->client_state_counts_.connecting, this->client_state_counts_.discovered,
|
||||
this->client_state_counts_.disconnecting, this->client_state_counts_.active);
|
||||
ESP_LOGD(TAG, "connecting: %d, discovered: %d, disconnecting: %d", this->client_state_counts_.connecting,
|
||||
this->client_state_counts_.discovered, this->client_state_counts_.disconnecting);
|
||||
}
|
||||
|
||||
// Scanner failure: reached when set_scanner_state_(FAILED) or scan_set_param_failed_ set
|
||||
@@ -191,18 +190,10 @@ void ESP32BLETracker::loop() {
|
||||
*/
|
||||
|
||||
// Start scan: reached when scanner_state_ becomes IDLE (via set_scanner_state_()) and
|
||||
// no clients are in the transient CONNECTING / DISCOVERED / DISCONNECTING states
|
||||
// (their state changes increment version when they finish). CONNECTED / ESTABLISHED
|
||||
// clients do NOT block this branch — the coex revert below has its own active-count gate.
|
||||
// all clients are idle (their state changes increment version when they finish)
|
||||
if (this->scanner_state_ == ScannerState::IDLE && !counts.connecting && !counts.disconnecting && !counts.discovered) {
|
||||
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
|
||||
// Only revert to BALANCE when no connections are active. Established connections
|
||||
// continue to need PREFER_BT so peer GATT responses can reach us while WiFi traffic
|
||||
// (advertisement upload, log streaming) competes for the shared radio. Reverting too
|
||||
// early causes Bluedroid to time out at ~20s and synthesize status=133.
|
||||
if (!counts.active) {
|
||||
this->update_coex_preference_(false);
|
||||
}
|
||||
this->update_coex_preference_(false);
|
||||
#endif
|
||||
if (this->scan_continuous_) {
|
||||
this->start_scan_(false); // first = false
|
||||
@@ -710,10 +701,9 @@ void ESP32BLETracker::dump_config() {
|
||||
this->scan_active_ ? "ACTIVE" : "PASSIVE", YESNO(this->scan_continuous_));
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Scanner State: %s\n"
|
||||
" Connecting: %d, discovered: %d, disconnecting: %d, active: %d",
|
||||
" Connecting: %d, discovered: %d, disconnecting: %d",
|
||||
this->scanner_state_to_string_(this->scanner_state_), this->client_state_counts_.connecting,
|
||||
this->client_state_counts_.discovered, this->client_state_counts_.disconnecting,
|
||||
this->client_state_counts_.active);
|
||||
this->client_state_counts_.discovered, this->client_state_counts_.disconnecting);
|
||||
if (this->scan_start_fail_count_) {
|
||||
ESP_LOGCONFIG(TAG, " Scan Start Fail Count: %d", this->scan_start_fail_count_);
|
||||
}
|
||||
|
||||
@@ -160,13 +160,9 @@ struct ClientStateCounts {
|
||||
uint8_t connecting = 0;
|
||||
uint8_t discovered = 0;
|
||||
uint8_t disconnecting = 0;
|
||||
// CONNECTED + ESTABLISHED clients. Tracked so coex stays at PREFER_BT
|
||||
// while active connections may still need to send/receive GATT traffic.
|
||||
uint8_t active = 0;
|
||||
|
||||
bool operator==(const ClientStateCounts &other) const {
|
||||
return connecting == other.connecting && discovered == other.discovered && disconnecting == other.disconnecting &&
|
||||
active == other.active;
|
||||
return connecting == other.connecting && discovered == other.discovered && disconnecting == other.disconnecting;
|
||||
}
|
||||
|
||||
bool operator!=(const ClientStateCounts &other) const { return !(*this == other); }
|
||||
@@ -385,10 +381,6 @@ class ESP32BLETracker : public Component,
|
||||
case ClientState::CONNECTING:
|
||||
counts.connecting++;
|
||||
break;
|
||||
case ClientState::CONNECTED:
|
||||
case ClientState::ESTABLISHED:
|
||||
counts.active++;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -246,10 +246,9 @@ async def to_code(config):
|
||||
idf_ver = esp32.idf_version()
|
||||
os.environ["ESP_IDF_VERSION"] = f"{idf_ver.major}.{idf_ver.minor}"
|
||||
if idf_ver >= cv.Version(5, 5, 0):
|
||||
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.5.1")
|
||||
esp32.add_idf_component(name="espressif/wifi_remote_over_eppp", ref="0.3.2")
|
||||
esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.5")
|
||||
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.12.6")
|
||||
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.4.0")
|
||||
esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.4")
|
||||
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.12.1")
|
||||
else:
|
||||
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.13.0")
|
||||
esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0")
|
||||
|
||||
@@ -216,7 +216,6 @@ void ESP32TouchComponent::setup() {
|
||||
// Do initial oneshot scans to populate baseline values
|
||||
for (uint32_t i = 0; i < ONESHOT_SCAN_COUNT; i++) {
|
||||
err = touch_sensor_trigger_oneshot_scanning(this->sens_handle_, ONESHOT_SCAN_TIMEOUT_MS);
|
||||
App.feed_wdt(); // 3 scans with 2s timeout might exceed WDT, so feed it here to be safe
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Oneshot scan %" PRIu32 " failed: %s", i, esp_err_to_name(err));
|
||||
}
|
||||
|
||||
@@ -314,11 +314,6 @@ async def to_code(config):
|
||||
for symbol in ("vprintf", "printf", "fprintf"):
|
||||
cg.add_build_flag(f"-Wl,--wrap={symbol}")
|
||||
|
||||
# Wrap Arduino's millis() so all callers (including Arduino libraries and ISR
|
||||
# handlers) use our fast accumulator instead of the expensive 4x 64-bit multiply
|
||||
# implementation in the Arduino ESP8266 core.
|
||||
cg.add_build_flag("-Wl,--wrap=millis")
|
||||
|
||||
cg.add_platformio_option("board_build.flash_mode", config[CONF_BOARD_FLASH_MODE])
|
||||
|
||||
ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
|
||||
|
||||
@@ -3,12 +3,45 @@
|
||||
#include "core.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/time_64.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "preferences.h"
|
||||
#include <Arduino.h>
|
||||
#include <core_esp8266_features.h>
|
||||
|
||||
extern "C" {
|
||||
#include <user_interface.h>
|
||||
}
|
||||
|
||||
namespace esphome {
|
||||
|
||||
// HAL functions live in hal.cpp. This file keeps only the ESP8266-specific
|
||||
// firmware bootstrap (Tasmota OTA magic bytes, optional GPIO pre-init).
|
||||
void HOT yield() { ::yield(); }
|
||||
uint32_t IRAM_ATTR HOT millis() { return ::millis(); }
|
||||
uint64_t millis_64() { return Millis64Impl::compute(::millis()); }
|
||||
void HOT delay(uint32_t ms) { ::delay(ms); }
|
||||
uint32_t IRAM_ATTR HOT micros() { return ::micros(); }
|
||||
void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); }
|
||||
void arch_restart() {
|
||||
system_restart();
|
||||
// restart() doesn't always end execution
|
||||
while (true) { // NOLINT(clang-diagnostic-unreachable-code)
|
||||
yield();
|
||||
}
|
||||
}
|
||||
void arch_init() {}
|
||||
void HOT arch_feed_wdt() { system_soft_wdt_feed(); }
|
||||
|
||||
uint8_t progmem_read_byte(const uint8_t *addr) {
|
||||
return pgm_read_byte(addr); // NOLINT
|
||||
}
|
||||
const char *progmem_read_ptr(const char *const *addr) {
|
||||
return reinterpret_cast<const char *>(pgm_read_ptr(addr)); // NOLINT
|
||||
}
|
||||
uint16_t progmem_read_uint16(const uint16_t *addr) {
|
||||
return pgm_read_word(addr); // NOLINT
|
||||
}
|
||||
uint32_t IRAM_ATTR HOT arch_get_cpu_cycle_count() { return esp_get_cycle_count(); }
|
||||
uint32_t arch_get_cpu_freq_hz() { return F_CPU; }
|
||||
|
||||
void force_link_symbols() {
|
||||
// Tasmota uses magic bytes in the binary to check if an OTA firmware is compatible
|
||||
|
||||
@@ -140,7 +140,6 @@ void IRAM_ATTR ISRInternalGPIOPin::digital_write(bool value) {
|
||||
|
||||
void IRAM_ATTR ISRInternalGPIOPin::clear_interrupt() {
|
||||
auto *arg = reinterpret_cast<ISRPinArg *>(arg_);
|
||||
// NOLINTNEXTLINE(clang-analyzer-core.FixedAddressDereference) -- GPIO_REG_WRITE is MMIO at a fixed address
|
||||
GPIO_REG_WRITE(GPIO_STATUS_W1TC_ADDRESS, 1UL << arg->pin);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
#ifdef USE_ESP8266
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <core_esp8266_features.h>
|
||||
|
||||
extern "C" {
|
||||
#include <user_interface.h>
|
||||
}
|
||||
|
||||
// Empty esp8266 namespace block to satisfy ci-custom's lint_namespace check.
|
||||
// HAL functions live in namespace esphome (root) — they are not part of the
|
||||
// esp8266 component's API.
|
||||
namespace esphome::esp8266 {} // namespace esphome::esp8266
|
||||
|
||||
namespace esphome {
|
||||
|
||||
// yield(), micros(), millis_64(), delayMicroseconds(), arch_feed_wdt(),
|
||||
// progmem_read_*() are inlined in components/esp8266/hal.h.
|
||||
//
|
||||
// Fast accumulator replacement for Arduino's millis() (~3.3 μs via 4× 64-bit
|
||||
// multiplies on the LX106). Tracks a running ms counter from 32-bit
|
||||
// system_get_time() deltas using pure 32-bit ops. Installed as __wrap_millis
|
||||
// (via -Wl,--wrap=millis) so Arduino libs and IRAM_ATTR ISR handlers (e.g.
|
||||
// Wiegand, ZyAura) also get the fast version. xt_rsil(15) guards the static
|
||||
// state against ISR re-entry; the critical section is bounded (≤10 while-loop
|
||||
// iterations, ~100 ns on the common path, or a constant-time /1000 ~2.5 μs on
|
||||
// the rare path — well under WiFi's ~10 μs ISR latency budget). NMIs (level
|
||||
// >15) are not masked, but the ESP8266 SDK's NMI handlers don't call millis().
|
||||
//
|
||||
// system_get_time() wraps every ~71.6 min; unsigned (now_us - last_us) handles
|
||||
// one wrap. The main loop calls millis() at 60+ Hz, so delta stays tiny — a
|
||||
// >71 min block would trip the watchdog long before it could matter here.
|
||||
static constexpr uint32_t MILLIS_RARE_PATH_THRESHOLD_US = 10000;
|
||||
static constexpr uint32_t US_PER_MS = 1000;
|
||||
|
||||
uint32_t IRAM_ATTR HOT millis() {
|
||||
// Struct packs the three statics so the compiler loads one base address
|
||||
// instead of three separate literal pool entries (saves ~8 bytes IRAM).
|
||||
static struct {
|
||||
uint32_t cache;
|
||||
uint32_t remainder;
|
||||
uint32_t last_us;
|
||||
} state = {0, 0, 0};
|
||||
uint32_t ps = xt_rsil(15);
|
||||
uint32_t now_us = system_get_time();
|
||||
uint32_t delta = now_us - state.last_us;
|
||||
state.last_us = now_us;
|
||||
state.remainder += delta;
|
||||
if (state.remainder >= MILLIS_RARE_PATH_THRESHOLD_US) {
|
||||
// Rare path: large gap (WiFi scan, boot, long block). Constant-time
|
||||
// conversion keeps the critical section bounded.
|
||||
uint32_t ms = state.remainder / US_PER_MS;
|
||||
state.cache += ms;
|
||||
// Reuse ms instead of `remainder %= US_PER_MS` — `%` would compile to a
|
||||
// second __umodsi3 call on the LX106 (no hardware divide).
|
||||
state.remainder -= ms * US_PER_MS;
|
||||
} else {
|
||||
// Common path: small gap. At most ~10 iterations since remainder was
|
||||
// < threshold (10 ms) on entry and delta adds at most one more threshold
|
||||
// before exiting this branch.
|
||||
while (state.remainder >= US_PER_MS) {
|
||||
state.cache++;
|
||||
state.remainder -= US_PER_MS;
|
||||
}
|
||||
}
|
||||
uint32_t result = state.cache;
|
||||
xt_wsr_ps(ps);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Poll-based delay that avoids ::delay() — Arduino's __delay has an intra-object
|
||||
// call to the original millis() that --wrap can't intercept, so calling ::delay()
|
||||
// would keep the slow Arduino millis body alive in IRAM. optimistic_yield still
|
||||
// enters esp_schedule()/esp_suspend_within_cont() via yield(), so SDK tasks and
|
||||
// WiFi run correctly. Theoretically less power-efficient than Arduino's
|
||||
// os_timer-based delay() for long waits, but nearly all ESPHome delays are short
|
||||
// (sensor/I²C/SPI settling in the 1–100 ms range) where the difference is
|
||||
// negligible.
|
||||
void HOT delay(uint32_t ms) {
|
||||
if (ms == 0) {
|
||||
optimistic_yield(1000);
|
||||
return;
|
||||
}
|
||||
uint32_t start = millis();
|
||||
while (millis() - start < ms) {
|
||||
optimistic_yield(1000);
|
||||
}
|
||||
}
|
||||
|
||||
void arch_restart() {
|
||||
system_restart();
|
||||
// restart() doesn't always end execution
|
||||
while (true) { // NOLINT(clang-diagnostic-unreachable-code)
|
||||
yield();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
// Linker wrap: redirect all ::millis() calls (Arduino libs, ISRs) to our accumulator.
|
||||
// Requires -Wl,--wrap=millis in build flags (added by __init__.py).
|
||||
// NOLINTNEXTLINE(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
|
||||
extern "C" uint32_t IRAM_ATTR __wrap_millis() { return esphome::millis(); }
|
||||
// Note: Arduino's init() registers a 60-second overflow timer for micros64().
|
||||
// We leave it running — wrapping init() as a no-op would break micros64()'s
|
||||
// overflow tracking, and the timer's cost is negligible (~3 μs per 60 s).
|
||||
|
||||
#endif // USE_ESP8266
|
||||
@@ -1,73 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
|
||||
#include <c_types.h>
|
||||
#include <core_esp8266_features.h>
|
||||
#include <cstdint>
|
||||
#include <pgmspace.h>
|
||||
|
||||
#include "esphome/core/time_64.h"
|
||||
|
||||
#ifndef PROGMEM
|
||||
#define PROGMEM ICACHE_RODATA_ATTR
|
||||
#endif
|
||||
|
||||
// Forward decls from Arduino's <Arduino.h> for the inline wrappers below.
|
||||
// NOLINTBEGIN(google-runtime-int,readability-identifier-naming,readability-redundant-declaration)
|
||||
extern "C" void yield(void);
|
||||
extern "C" void delay(unsigned long ms);
|
||||
extern "C" unsigned long micros(void);
|
||||
extern "C" unsigned long millis(void);
|
||||
// NOLINTEND(google-runtime-int,readability-identifier-naming,readability-redundant-declaration)
|
||||
|
||||
// Forward decl from <user_interface.h> for arch_feed_wdt() inline below.
|
||||
// NOLINTNEXTLINE(readability-redundant-declaration)
|
||||
extern "C" void system_soft_wdt_feed(void);
|
||||
|
||||
namespace esphome::esp8266 {}
|
||||
|
||||
namespace esphome {
|
||||
|
||||
// Forward decl from helpers.h so this header stays cheap.
|
||||
// NOLINTNEXTLINE(readability-redundant-declaration)
|
||||
void delay_microseconds_safe(uint32_t us);
|
||||
|
||||
/// Returns true when executing inside an interrupt handler.
|
||||
/// ESP8266 has no reliable single-register ISR detection: PS.INTLEVEL is
|
||||
/// non-zero both in a real ISR and when user code masks interrupts. The
|
||||
/// ESP8266 wake path is context-agnostic (wake_loop_impl uses esp_schedule
|
||||
/// which is ISR-safe) so this helper is unused on this platform.
|
||||
__attribute__((always_inline)) inline bool in_isr_context() { return false; }
|
||||
|
||||
__attribute__((always_inline)) inline void yield() { ::yield(); }
|
||||
__attribute__((always_inline)) inline uint32_t micros() { return static_cast<uint32_t>(::micros()); }
|
||||
void delay(uint32_t ms);
|
||||
uint32_t millis();
|
||||
__attribute__((always_inline)) inline uint64_t millis_64() { return Millis64Impl::compute(millis()); }
|
||||
|
||||
// ESP8266: pgm_read_* does aligned 32-bit flash reads on Harvard architecture.
|
||||
// Inline-forward to the platform macros so the wrappers themselves don't
|
||||
// occupy IRAM/flash on every call site.
|
||||
__attribute__((always_inline)) inline uint8_t progmem_read_byte(const uint8_t *addr) {
|
||||
return pgm_read_byte(addr); // NOLINT
|
||||
}
|
||||
__attribute__((always_inline)) inline const char *progmem_read_ptr(const char *const *addr) {
|
||||
return reinterpret_cast<const char *>(pgm_read_ptr(addr)); // NOLINT
|
||||
}
|
||||
__attribute__((always_inline)) inline uint16_t progmem_read_uint16(const uint16_t *addr) {
|
||||
return pgm_read_word(addr); // NOLINT
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||
__attribute__((always_inline)) inline void delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); }
|
||||
__attribute__((always_inline)) inline void arch_feed_wdt() { system_soft_wdt_feed(); }
|
||||
__attribute__((always_inline)) inline void arch_init() {}
|
||||
// esp_get_cycle_count() declared in <core_esp8266_features.h>; F_CPU is a
|
||||
// compiler-driven macro from the ESP8266 Arduino board defs (-DF_CPU=...).
|
||||
__attribute__((always_inline)) inline uint32_t arch_get_cpu_cycle_count() { return esp_get_cycle_count(); }
|
||||
__attribute__((always_inline)) inline uint32_t arch_get_cpu_freq_hz() { return F_CPU; }
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESP8266
|
||||
@@ -51,7 +51,7 @@ static inline bool esp_rtc_user_mem_read(uint32_t index, uint32_t *dest) {
|
||||
if (index >= ESP_RTC_USER_MEM_SIZE_WORDS) {
|
||||
return false;
|
||||
}
|
||||
*dest = ESP_RTC_USER_MEM[index]; // NOLINT(performance-no-int-to-ptr,clang-analyzer-core.FixedAddressDereference)
|
||||
*dest = ESP_RTC_USER_MEM[index]; // NOLINT(performance-no-int-to-ptr)
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ static inline bool esp_rtc_user_mem_write(uint32_t index, uint32_t value) {
|
||||
}
|
||||
|
||||
auto *ptr = &ESP_RTC_USER_MEM[index]; // NOLINT(performance-no-int-to-ptr)
|
||||
*ptr = value; // NOLINT(clang-analyzer-core.FixedAddressDereference)
|
||||
*ptr = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -114,10 +114,8 @@ void ESPHomeOTAComponent::loop() {
|
||||
this->handle_handshake_();
|
||||
}
|
||||
|
||||
static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_COMPRESSION = 0x01;
|
||||
static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_SHA256_AUTH = 0x02;
|
||||
static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL = 0x04;
|
||||
static constexpr uint8_t SERVER_FEATURE_SUPPORTS_COMPRESSION = 0x01;
|
||||
static const uint8_t FEATURE_SUPPORTS_COMPRESSION = 0x01;
|
||||
static const uint8_t FEATURE_SUPPORTS_SHA256_AUTH = 0x02;
|
||||
|
||||
void ESPHomeOTAComponent::handle_handshake_() {
|
||||
/// Handle the OTA handshake and authentication.
|
||||
@@ -203,30 +201,16 @@ void ESPHomeOTAComponent::handle_handshake_() {
|
||||
this->ota_features_ = this->handshake_buf_[0];
|
||||
ESP_LOGV(TAG, "Features: 0x%02X", this->ota_features_);
|
||||
this->transition_ota_state_(OTAState::FEATURE_ACK);
|
||||
|
||||
const bool supports_compression =
|
||||
(this->ota_features_ & CLIENT_FEATURE_SUPPORTS_COMPRESSION) != 0 && this->backend_->supports_compression();
|
||||
|
||||
// Compose the feature-ack response. When the client negotiates the extended protocol we emit
|
||||
// a 2-byte response (marker + server feature flags); otherwise we emit the single-byte
|
||||
// legacy response.
|
||||
this->extended_proto_ = (this->ota_features_ & CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL) != 0;
|
||||
if (this->extended_proto_) {
|
||||
static_assert(HANDSHAKE_BUF_SIZE >= 2, "handshake_buf_ must hold the 2-byte extended-protocol feature ack");
|
||||
this->handshake_buf_[0] = ota::OTA_RESPONSE_FEATURE_FLAGS;
|
||||
this->handshake_buf_[1] = (supports_compression ? SERVER_FEATURE_SUPPORTS_COMPRESSION : 0);
|
||||
} else {
|
||||
this->handshake_buf_[0] =
|
||||
supports_compression ? ota::OTA_RESPONSE_SUPPORTS_COMPRESSION : ota::OTA_RESPONSE_HEADER_OK;
|
||||
}
|
||||
this->handshake_buf_[0] =
|
||||
((this->ota_features_ & FEATURE_SUPPORTS_COMPRESSION) != 0 && this->backend_->supports_compression())
|
||||
? ota::OTA_RESPONSE_SUPPORTS_COMPRESSION
|
||||
: ota::OTA_RESPONSE_HEADER_OK;
|
||||
[[fallthrough]];
|
||||
}
|
||||
|
||||
case OTAState::FEATURE_ACK: {
|
||||
static constexpr size_t STANDARD_PROTO_ACK_SIZE = 1;
|
||||
static constexpr size_t EXTENDED_PROTO_ACK_SIZE = 2;
|
||||
const size_t ack_size = this->extended_proto_ ? EXTENDED_PROTO_ACK_SIZE : STANDARD_PROTO_ACK_SIZE;
|
||||
if (!this->try_write_(ack_size, LOG_STR("ack feature"))) {
|
||||
// Acknowledge header - 1 byte
|
||||
if (!this->try_write_(1, LOG_STR("ack feature"))) {
|
||||
return;
|
||||
}
|
||||
#ifdef USE_OTA_PASSWORD
|
||||
@@ -308,11 +292,9 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
bool update_started = false;
|
||||
size_t total = 0;
|
||||
uint32_t last_progress = 0;
|
||||
uint32_t last_data_ms = 0;
|
||||
uint8_t buf[OTA_BUFFER_SIZE];
|
||||
char *sbuf = reinterpret_cast<char *>(buf);
|
||||
size_t ota_size;
|
||||
ota::OTAType ota_type = ota::OTA_TYPE_UPDATE_APP;
|
||||
#if USE_OTA_VERSION == 2
|
||||
size_t size_acknowledged = 0;
|
||||
#endif
|
||||
@@ -328,16 +310,6 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
// Acknowledge auth OK - 1 byte
|
||||
this->write_byte_(ota::OTA_RESPONSE_AUTH_OK);
|
||||
|
||||
if (this->extended_proto_) {
|
||||
// Read ota type, 1 byte
|
||||
if (!this->readall_(buf, 1)) {
|
||||
this->log_read_error_(LOG_STR("OTA type"));
|
||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
}
|
||||
ota_type = static_cast<ota::OTAType>(buf[0]);
|
||||
}
|
||||
ESP_LOGV(TAG, "OTA type is 0x%02x", ota_type);
|
||||
|
||||
// Read size, 4 bytes MSB first
|
||||
if (!this->readall_(buf, 4)) {
|
||||
this->log_read_error_(LOG_STR("size"));
|
||||
@@ -347,11 +319,6 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
(static_cast<size_t>(buf[2]) << 8) | buf[3];
|
||||
ESP_LOGV(TAG, "Size is %u bytes", ota_size);
|
||||
|
||||
if (ota_type != ota::OTA_TYPE_UPDATE_APP) {
|
||||
error_code = ota::OTA_RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE;
|
||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
}
|
||||
|
||||
// Now that we've passed authentication and are actually
|
||||
// starting the update, set the warning status and notify
|
||||
// listeners. This ensures that port scanners do not
|
||||
@@ -383,18 +350,8 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
// Acknowledge MD5 OK - 1 byte
|
||||
this->write_byte_(ota::OTA_RESPONSE_BIN_MD5_OK);
|
||||
|
||||
// Track when we last received data so a silently-vanished peer (no FIN/RST
|
||||
// delivered, e.g. uploader killed mid-transfer or NAT/router dropped state)
|
||||
// can't wedge the device indefinitely. Without this, the loop only exits
|
||||
// on actual data, EOF, or a non-EWOULDBLOCK error from read(), and lwIP
|
||||
// TCP keepalive isn't enabled here.
|
||||
last_data_ms = millis();
|
||||
while (total < ota_size) {
|
||||
if (millis() - last_data_ms > OTA_SOCKET_TIMEOUT_DATA) {
|
||||
ESP_LOGW(TAG, "No data received for %u ms", (unsigned) OTA_SOCKET_TIMEOUT_DATA);
|
||||
error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN;
|
||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
}
|
||||
// TODO: timeout check
|
||||
size_t remaining = ota_size - total;
|
||||
size_t requested = remaining < OTA_BUFFER_SIZE ? remaining : OTA_BUFFER_SIZE;
|
||||
ssize_t read = this->client_->read(buf, requested);
|
||||
@@ -412,7 +369,6 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
}
|
||||
|
||||
last_data_ms = millis();
|
||||
error_code = this->backend_->write(buf, read);
|
||||
if (error_code != ota::OTA_RESPONSE_OK) {
|
||||
ESP_LOGW(TAG, "Flash write err %d", error_code);
|
||||
@@ -648,7 +604,7 @@ void ESPHomeOTAComponent::yield_and_feed_watchdog_() {
|
||||
void ESPHomeOTAComponent::log_auth_warning_(const LogString *msg) { ESP_LOGW(TAG, "Auth: %s", LOG_STR_ARG(msg)); }
|
||||
|
||||
bool ESPHomeOTAComponent::select_auth_type_() {
|
||||
bool client_supports_sha256 = (this->ota_features_ & CLIENT_FEATURE_SUPPORTS_SHA256_AUTH) != 0;
|
||||
bool client_supports_sha256 = (this->ota_features_ & FEATURE_SUPPORTS_SHA256_AUTH) != 0;
|
||||
|
||||
// Require SHA256
|
||||
if (!client_supports_sha256) {
|
||||
|
||||
@@ -97,9 +97,8 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
|
||||
ota::OTABackendPtr backend_;
|
||||
|
||||
uint32_t client_connect_time_{0};
|
||||
static constexpr size_t HANDSHAKE_BUF_SIZE = 5;
|
||||
uint16_t port_;
|
||||
uint8_t handshake_buf_[HANDSHAKE_BUF_SIZE];
|
||||
uint8_t handshake_buf_[5];
|
||||
OTAState ota_state_{OTAState::IDLE};
|
||||
uint8_t handshake_buf_pos_{0};
|
||||
uint8_t ota_features_{0};
|
||||
@@ -107,7 +106,6 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
|
||||
uint8_t auth_buf_pos_{0};
|
||||
uint8_t auth_type_{0}; // Store auth type to know which hasher to use
|
||||
#endif // USE_OTA_PASSWORD
|
||||
bool extended_proto_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
@@ -26,9 +26,9 @@ espnow_ns = cg.esphome_ns.namespace("espnow")
|
||||
ESPNowComponent = espnow_ns.class_("ESPNowComponent", cg.Component)
|
||||
|
||||
# Handler interfaces that other components can use to register callbacks
|
||||
ESPNowReceivePacketHandler = espnow_ns.class_("ESPNowReceivePacketHandler")
|
||||
ESPNowReceivedPacketHandler = espnow_ns.class_("ESPNowReceivedPacketHandler")
|
||||
ESPNowUnknownPeerHandler = espnow_ns.class_("ESPNowUnknownPeerHandler")
|
||||
ESPNowBroadcastHandler = espnow_ns.class_("ESPNowBroadcastHandler")
|
||||
ESPNowBroadcastedHandler = espnow_ns.class_("ESPNowBroadcastedHandler")
|
||||
|
||||
ESPNowRecvInfo = espnow_ns.class_("ESPNowRecvInfo")
|
||||
ESPNowRecvInfoConstRef = ESPNowRecvInfo.operator("const").operator("ref")
|
||||
@@ -48,10 +48,10 @@ OnUnknownPeerTrigger = espnow_ns.class_(
|
||||
"OnUnknownPeerTrigger", ESPNowHandlerTrigger, ESPNowUnknownPeerHandler
|
||||
)
|
||||
OnReceiveTrigger = espnow_ns.class_(
|
||||
"OnReceiveTrigger", ESPNowHandlerTrigger, ESPNowReceivePacketHandler
|
||||
"OnReceiveTrigger", ESPNowHandlerTrigger, ESPNowReceivedPacketHandler
|
||||
)
|
||||
OnBroadcastTrigger = espnow_ns.class_(
|
||||
"OnBroadcastTrigger", ESPNowHandlerTrigger, ESPNowBroadcastHandler
|
||||
OnBroadcastedTrigger = espnow_ns.class_(
|
||||
"OnBroadcastedTrigger", ESPNowHandlerTrigger, ESPNowBroadcastedHandler
|
||||
)
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
),
|
||||
cv.Optional(CONF_ON_BROADCAST): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OnBroadcastTrigger),
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OnBroadcastedTrigger),
|
||||
cv.Optional(CONF_ADDRESS): cv.mac_address,
|
||||
}
|
||||
),
|
||||
@@ -140,11 +140,11 @@ async def to_code(config):
|
||||
|
||||
for on_receive in config.get(CONF_ON_RECEIVE, []):
|
||||
trigger = await _trigger_to_code(on_receive)
|
||||
cg.add(var.register_receive_handler(trigger))
|
||||
cg.add(var.register_received_handler(trigger))
|
||||
|
||||
for on_receive in config.get(CONF_ON_BROADCAST, []):
|
||||
trigger = await _trigger_to_code(on_receive)
|
||||
cg.add(var.register_broadcast_handler(trigger))
|
||||
cg.add(var.register_broadcasted_handler(trigger))
|
||||
|
||||
|
||||
# ========================================== A C T I O N S ================================================
|
||||
|
||||
@@ -67,7 +67,6 @@ template<typename... Ts> class SendAction : public Action<Ts...>, public Parente
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
void play(const Ts &...x) override { /* ignore - see play_complex */
|
||||
}
|
||||
|
||||
@@ -76,6 +75,7 @@ template<typename... Ts> class SendAction : public Action<Ts...>, public Parente
|
||||
this->error_.stop();
|
||||
}
|
||||
|
||||
protected:
|
||||
ActionList<Ts...> sent_;
|
||||
ActionList<Ts...> error_;
|
||||
|
||||
@@ -89,7 +89,7 @@ template<typename... Ts> class SendAction : public Action<Ts...>, public Parente
|
||||
template<typename... Ts> class AddPeerAction : public Action<Ts...>, public Parented<ESPNowComponent> {
|
||||
TEMPLATABLE_VALUE(peer_address_t, address);
|
||||
|
||||
protected:
|
||||
public:
|
||||
void play(const Ts &...x) override {
|
||||
peer_address_t address = this->address_.value(x...);
|
||||
this->parent_->add_peer(address.data());
|
||||
@@ -99,7 +99,7 @@ template<typename... Ts> class AddPeerAction : public Action<Ts...>, public Pare
|
||||
template<typename... Ts> class DeletePeerAction : public Action<Ts...>, public Parented<ESPNowComponent> {
|
||||
TEMPLATABLE_VALUE(peer_address_t, address);
|
||||
|
||||
protected:
|
||||
public:
|
||||
void play(const Ts &...x) override {
|
||||
peer_address_t address = this->address_.value(x...);
|
||||
this->parent_->del_peer(address.data());
|
||||
@@ -107,9 +107,8 @@ template<typename... Ts> class DeletePeerAction : public Action<Ts...>, public P
|
||||
};
|
||||
|
||||
template<typename... Ts> class SetChannelAction : public Action<Ts...>, public Parented<ESPNowComponent> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(uint8_t, channel)
|
||||
|
||||
protected:
|
||||
void play(const Ts &...x) override {
|
||||
if (this->parent_->is_wifi_enabled()) {
|
||||
return;
|
||||
@@ -126,9 +125,9 @@ class OnReceiveTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_t *,
|
||||
memcpy(this->address_, address.data(), ESP_NOW_ETH_ALEN);
|
||||
}
|
||||
|
||||
explicit OnReceiveTrigger() {}
|
||||
explicit OnReceiveTrigger() : has_address_(false) {}
|
||||
|
||||
bool on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override {
|
||||
bool on_received(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override {
|
||||
bool match = !this->has_address_ || (memcmp(this->address_, info.src_addr, ESP_NOW_ETH_ALEN) == 0);
|
||||
if (!match)
|
||||
return false;
|
||||
@@ -139,7 +138,7 @@ class OnReceiveTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_t *,
|
||||
|
||||
protected:
|
||||
bool has_address_{false};
|
||||
uint8_t address_[ESP_NOW_ETH_ALEN]{};
|
||||
uint8_t address_[ESP_NOW_ETH_ALEN];
|
||||
};
|
||||
class OnUnknownPeerTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_t *, uint8_t>,
|
||||
public ESPNowUnknownPeerHandler {
|
||||
@@ -149,15 +148,15 @@ class OnUnknownPeerTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_
|
||||
return false; // Return false to continue processing other internal handlers
|
||||
}
|
||||
};
|
||||
class OnBroadcastTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_t *, uint8_t>,
|
||||
public ESPNowBroadcastHandler {
|
||||
class OnBroadcastedTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_t *, uint8_t>,
|
||||
public ESPNowBroadcastedHandler {
|
||||
public:
|
||||
explicit OnBroadcastTrigger(std::array<uint8_t, ESP_NOW_ETH_ALEN> address) : has_address_(true) {
|
||||
explicit OnBroadcastedTrigger(std::array<uint8_t, ESP_NOW_ETH_ALEN> address) : has_address_(true) {
|
||||
memcpy(this->address_, address.data(), ESP_NOW_ETH_ALEN);
|
||||
}
|
||||
explicit OnBroadcastTrigger() {}
|
||||
explicit OnBroadcastedTrigger() : has_address_(false) {}
|
||||
|
||||
bool on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override {
|
||||
bool on_broadcasted(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override {
|
||||
bool match = !this->has_address_ || (memcmp(this->address_, info.src_addr, ESP_NOW_ETH_ALEN) == 0);
|
||||
if (!match)
|
||||
return false;
|
||||
@@ -168,7 +167,7 @@ class OnBroadcastTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_t
|
||||
|
||||
protected:
|
||||
bool has_address_{false};
|
||||
uint8_t address_[ESP_NOW_ETH_ALEN]{};
|
||||
uint8_t address_[ESP_NOW_ETH_ALEN];
|
||||
};
|
||||
|
||||
} // namespace esphome::espnow
|
||||
|
||||
@@ -299,13 +299,13 @@ void ESPNowComponent::loop() {
|
||||
format_hex_pretty_to(hex_buf, packet->packet_.receive.data, packet->packet_.receive.size));
|
||||
#endif
|
||||
if (memcmp(info.des_addr, ESPNOW_BROADCAST_ADDR, ESP_NOW_ETH_ALEN) == 0) {
|
||||
for (auto *handler : this->broadcast_handlers_) {
|
||||
if (handler->on_broadcast(info, packet->packet_.receive.data, packet->packet_.receive.size))
|
||||
for (auto *handler : this->broadcasted_handlers_) {
|
||||
if (handler->on_broadcasted(info, packet->packet_.receive.data, packet->packet_.receive.size))
|
||||
break; // If a handler returns true, stop processing further handlers
|
||||
}
|
||||
} else {
|
||||
for (auto *handler : this->receive_handlers_) {
|
||||
if (handler->on_receive(info, packet->packet_.receive.data, packet->packet_.receive.size))
|
||||
for (auto *handler : this->received_handlers_) {
|
||||
if (handler->on_received(info, packet->packet_.receive.data, packet->packet_.receive.size))
|
||||
break; // If a handler returns true, stop processing further handlers
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,8 +31,8 @@ using peer_address_t = std::array<uint8_t, ESP_NOW_ETH_ALEN>;
|
||||
enum class ESPNowTriggers : uint8_t {
|
||||
TRIGGER_NONE = 0,
|
||||
ON_NEW_PEER = 1,
|
||||
ON_RECEIVE = 2,
|
||||
ON_BROADCAST = 3,
|
||||
ON_RECEIVED = 2,
|
||||
ON_BROADCASTED = 3,
|
||||
ON_SUCCEED = 10,
|
||||
ON_FAILED = 11,
|
||||
};
|
||||
@@ -74,18 +74,18 @@ class ESPNowReceivedPacketHandler {
|
||||
/// @param data Pointer to the received data payload
|
||||
/// @param size Size of the received data in bytes
|
||||
/// @return true if the packet was handled, false otherwise
|
||||
virtual bool on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) = 0;
|
||||
virtual bool on_received(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) = 0;
|
||||
};
|
||||
/// Handler interface for receiving ESPNow broadcast packets
|
||||
/// Handler interface for receiving broadcasted ESPNow packets
|
||||
/// Components should inherit from this class to handle incoming ESPNow data
|
||||
class ESPNowBroadcastHandler {
|
||||
class ESPNowBroadcastedHandler {
|
||||
public:
|
||||
/// Called when an ESPNow broadcast packet is received
|
||||
/// Called when a broadcasted ESPNow packet is received
|
||||
/// @param info Information about the received packet (sender MAC, etc.)
|
||||
/// @param data Pointer to the received data payload
|
||||
/// @param size Size of the received data in bytes
|
||||
/// @return true if the packet was handled, false otherwise
|
||||
virtual bool on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) = 0;
|
||||
virtual bool on_broadcasted(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) = 0;
|
||||
};
|
||||
|
||||
class ESPNowComponent : public Component {
|
||||
@@ -136,11 +136,13 @@ class ESPNowComponent : public Component {
|
||||
esp_err_t send(const uint8_t *peer_address, const uint8_t *payload, size_t size,
|
||||
const send_callback_t &callback = nullptr);
|
||||
|
||||
void register_receive_handler(ESPNowReceivedPacketHandler *handler) { this->receive_handlers_.push_back(handler); }
|
||||
void register_received_handler(ESPNowReceivedPacketHandler *handler) { this->received_handlers_.push_back(handler); }
|
||||
void register_unknown_peer_handler(ESPNowUnknownPeerHandler *handler) {
|
||||
this->unknown_peer_handlers_.push_back(handler);
|
||||
}
|
||||
void register_broadcast_handler(ESPNowBroadcastHandler *handler) { this->broadcast_handlers_.push_back(handler); }
|
||||
void register_broadcasted_handler(ESPNowBroadcastedHandler *handler) {
|
||||
this->broadcasted_handlers_.push_back(handler);
|
||||
}
|
||||
|
||||
protected:
|
||||
friend void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int size);
|
||||
@@ -154,8 +156,8 @@ class ESPNowComponent : public Component {
|
||||
void send_();
|
||||
|
||||
std::vector<ESPNowUnknownPeerHandler *> unknown_peer_handlers_;
|
||||
std::vector<ESPNowReceivedPacketHandler *> receive_handlers_;
|
||||
std::vector<ESPNowBroadcastHandler *> broadcast_handlers_;
|
||||
std::vector<ESPNowReceivedPacketHandler *> received_handlers_;
|
||||
std::vector<ESPNowBroadcastedHandler *> broadcasted_handlers_;
|
||||
|
||||
std::vector<ESPNowPeer> peers_{};
|
||||
|
||||
|
||||
@@ -26,10 +26,10 @@ void ESPNowTransport::setup() {
|
||||
this->peer_address_[5]);
|
||||
|
||||
// Register received handler
|
||||
this->parent_->register_receive_handler(this);
|
||||
this->parent_->register_received_handler(this);
|
||||
|
||||
// Register broadcast handler
|
||||
this->parent_->register_broadcast_handler(this);
|
||||
// Register broadcasted handler
|
||||
this->parent_->register_broadcasted_handler(this);
|
||||
}
|
||||
|
||||
void ESPNowTransport::send_packet(const std::vector<uint8_t> &buf) const {
|
||||
@@ -56,7 +56,7 @@ void ESPNowTransport::send_packet(const std::vector<uint8_t> &buf) const {
|
||||
});
|
||||
}
|
||||
|
||||
bool ESPNowTransport::on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) {
|
||||
bool ESPNowTransport::on_received(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) {
|
||||
ESP_LOGV(TAG, "Received packet of size %u from %02X:%02X:%02X:%02X:%02X:%02X", size, info.src_addr[0],
|
||||
info.src_addr[1], info.src_addr[2], info.src_addr[3], info.src_addr[4], info.src_addr[5]);
|
||||
|
||||
@@ -71,7 +71,7 @@ bool ESPNowTransport::on_receive(const ESPNowRecvInfo &info, const uint8_t *data
|
||||
return false; // Allow other handlers to run
|
||||
}
|
||||
|
||||
bool ESPNowTransport::on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) {
|
||||
bool ESPNowTransport::on_broadcasted(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) {
|
||||
ESP_LOGV(TAG, "Received broadcast packet of size %u from %02X:%02X:%02X:%02X:%02X:%02X", size, info.src_addr[0],
|
||||
info.src_addr[1], info.src_addr[2], info.src_addr[3], info.src_addr[4], info.src_addr[5]);
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace espnow {
|
||||
class ESPNowTransport : public packet_transport::PacketTransport,
|
||||
public Parented<ESPNowComponent>,
|
||||
public ESPNowReceivedPacketHandler,
|
||||
public ESPNowBroadcastHandler {
|
||||
public ESPNowBroadcastedHandler {
|
||||
public:
|
||||
void setup() override;
|
||||
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
|
||||
@@ -25,8 +25,8 @@ class ESPNowTransport : public packet_transport::PacketTransport,
|
||||
}
|
||||
|
||||
// ESPNow handler interface
|
||||
bool on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override;
|
||||
bool on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override;
|
||||
bool on_received(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override;
|
||||
bool on_broadcasted(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override;
|
||||
|
||||
protected:
|
||||
void send_packet(const std::vector<uint8_t> &buf) const override;
|
||||
|
||||
@@ -35,7 +35,7 @@ void EZOSensor::update() {
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
auto ezo_command = make_unique<EzoCommand>();
|
||||
std::unique_ptr<EzoCommand> ezo_command(new EzoCommand);
|
||||
ezo_command->command = "R";
|
||||
ezo_command->command_type = EzoCommandType::EZO_READ;
|
||||
ezo_command->delay_ms = 900;
|
||||
@@ -162,7 +162,7 @@ void EZOSensor::loop() {
|
||||
}
|
||||
|
||||
void EZOSensor::add_command_(const char *command, EzoCommandType command_type, uint16_t delay_ms) {
|
||||
auto ezo_command = make_unique<EzoCommand>();
|
||||
std::unique_ptr<EzoCommand> ezo_command(new EzoCommand);
|
||||
ezo_command->command = command;
|
||||
ezo_command->command_type = command_type;
|
||||
ezo_command->delay_ms = delay_ms;
|
||||
|
||||
@@ -31,19 +31,17 @@ from esphome.const import (
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_WEB_SERVER,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, Lambda, coroutine_with_priority
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import (
|
||||
entity_duplicate_validator,
|
||||
queue_entity_register,
|
||||
setup_entity,
|
||||
)
|
||||
from esphome.cpp_generator import LambdaExpression
|
||||
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
|
||||
fan_ns = cg.esphome_ns.namespace("fan")
|
||||
Fan = fan_ns.class_("Fan", cg.EntityBase)
|
||||
FanCall = fan_ns.class_("FanCall")
|
||||
|
||||
FanDirection = fan_ns.enum("FanDirection", is_class=True)
|
||||
FAN_DIRECTION_ENUM = {
|
||||
@@ -349,38 +347,17 @@ async def fan_turn_off_to_code(config, action_id, template_arg, args):
|
||||
)
|
||||
async def fan_turn_on_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
|
||||
# All configured fields are folded into a single stateless lambda whose
|
||||
# constants live in flash; the action stores only a function pointer.
|
||||
FIELDS = (
|
||||
(CONF_OSCILLATING, "set_oscillating", cg.bool_),
|
||||
(CONF_SPEED, "set_speed", cg.int_),
|
||||
(CONF_DIRECTION, "set_direction", FanDirection),
|
||||
)
|
||||
|
||||
fwd_args = ", ".join(name for _, name in args)
|
||||
body_lines: list[str] = []
|
||||
for conf_key, setter, type_ in FIELDS:
|
||||
if (value := config.get(conf_key)) is None:
|
||||
continue
|
||||
if isinstance(value, Lambda):
|
||||
inner = await cg.process_lambda(value, args, return_type=type_)
|
||||
body_lines.append(f"call.{setter}(({inner})({fwd_args}));")
|
||||
else:
|
||||
body_lines.append(f"call.{setter}({cg.safe_exp(value)});")
|
||||
|
||||
# Match TurnOnAction::ApplyFn signature: const Ts &... for trigger args.
|
||||
apply_args = [
|
||||
(FanCall.operator("ref"), "call"),
|
||||
*((t.operator("const").operator("ref"), n) for t, n in args),
|
||||
]
|
||||
apply_lambda = LambdaExpression(
|
||||
["\n".join(body_lines)],
|
||||
apply_args,
|
||||
capture="",
|
||||
return_type=cg.void,
|
||||
)
|
||||
return cg.new_Pvariable(action_id, template_arg, paren, apply_lambda)
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
if (oscillating := config.get(CONF_OSCILLATING)) is not None:
|
||||
template_ = await cg.templatable(oscillating, args, cg.bool_)
|
||||
cg.add(var.set_oscillating(template_))
|
||||
if (speed := config.get(CONF_SPEED)) is not None:
|
||||
template_ = await cg.templatable(speed, args, cg.int_)
|
||||
cg.add(var.set_speed(template_))
|
||||
if (direction := config.get(CONF_DIRECTION)) is not None:
|
||||
template_ = await cg.templatable(direction, args, FanDirection)
|
||||
cg.add(var.set_direction(template_))
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
|
||||
@@ -7,24 +7,29 @@
|
||||
namespace esphome {
|
||||
namespace fan {
|
||||
|
||||
// All configured fields are baked into a single stateless lambda whose
|
||||
// constants live in flash. The action only stores one function pointer
|
||||
// plus one parent pointer, regardless of how many fields the user set.
|
||||
// Trigger args are forwarded to the apply function so user lambdas
|
||||
// (e.g. `speed: !lambda "return x;"`) keep working.
|
||||
template<typename... Ts> class TurnOnAction : public Action<Ts...> {
|
||||
public:
|
||||
using ApplyFn = void (*)(FanCall &, const Ts &...);
|
||||
TurnOnAction(Fan *state, ApplyFn apply) : state_(state), apply_(apply) {}
|
||||
explicit TurnOnAction(Fan *state) : state_(state) {}
|
||||
|
||||
TEMPLATABLE_VALUE(bool, oscillating)
|
||||
TEMPLATABLE_VALUE(int, speed)
|
||||
TEMPLATABLE_VALUE(FanDirection, direction)
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
auto call = this->state_->turn_on();
|
||||
this->apply_(call, x...);
|
||||
if (this->oscillating_.has_value()) {
|
||||
call.set_oscillating(this->oscillating_.value(x...));
|
||||
}
|
||||
if (this->speed_.has_value()) {
|
||||
call.set_speed(this->speed_.value(x...));
|
||||
}
|
||||
if (this->direction_.has_value()) {
|
||||
call.set_direction(this->direction_.value(x...));
|
||||
}
|
||||
call.perform();
|
||||
}
|
||||
|
||||
Fan *state_;
|
||||
ApplyFn apply_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class TurnOffAction : public Action<Ts...> {
|
||||
|
||||
@@ -3,12 +3,11 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/application.h"
|
||||
|
||||
namespace esphome::feedback {
|
||||
namespace esphome {
|
||||
namespace feedback {
|
||||
|
||||
static const char *const TAG = "feedback.cover";
|
||||
|
||||
static constexpr uint32_t DIRECTION_CHANGE_TIMEOUT_ID = 1;
|
||||
|
||||
using namespace esphome::cover;
|
||||
|
||||
void FeedbackCover::setup() {
|
||||
@@ -38,7 +37,7 @@ void FeedbackCover::setup() {
|
||||
}
|
||||
#endif
|
||||
|
||||
this->last_recompute_time_ = this->start_dir_time_ = App.get_loop_component_start_time();
|
||||
this->last_recompute_time_ = this->start_dir_time_ = millis();
|
||||
}
|
||||
|
||||
CoverTraits FeedbackCover::get_traits() {
|
||||
@@ -136,7 +135,7 @@ void FeedbackCover::set_close_endstop(binary_sensor::BinarySensor *close_endstop
|
||||
#endif
|
||||
|
||||
void FeedbackCover::endstop_reached_(bool open_endstop) {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
const uint32_t now = millis();
|
||||
|
||||
this->position = open_endstop ? COVER_OPEN : COVER_CLOSED;
|
||||
|
||||
@@ -175,7 +174,7 @@ void FeedbackCover::set_current_operation_(cover::CoverOperation operation, bool
|
||||
if (!is_triggered || (this->open_feedback_ == nullptr || this->close_feedback_ == nullptr))
|
||||
#endif
|
||||
{
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
auto now = millis();
|
||||
this->current_operation = operation;
|
||||
this->start_dir_time_ = this->last_recompute_time_ = now;
|
||||
this->publish_state();
|
||||
@@ -307,7 +306,7 @@ void FeedbackCover::control(const CoverCall &call) {
|
||||
|
||||
void FeedbackCover::stop_prev_trigger_() {
|
||||
if (this->direction_change_waittime_.has_value()) {
|
||||
this->cancel_timeout(DIRECTION_CHANGE_TIMEOUT_ID);
|
||||
this->cancel_timeout("direction_change");
|
||||
}
|
||||
if (this->prev_command_trigger_ != nullptr) {
|
||||
this->prev_command_trigger_->stop_action();
|
||||
@@ -375,10 +374,12 @@ void FeedbackCover::start_direction_(CoverOperation dir) {
|
||||
// check if we have a wait time
|
||||
if (this->direction_change_waittime_.has_value() && dir != COVER_OPERATION_IDLE &&
|
||||
this->current_operation != COVER_OPERATION_IDLE && dir != this->current_operation) {
|
||||
const uint32_t waittime = *this->direction_change_waittime_;
|
||||
ESP_LOGD(TAG, "'%s' - Reversing direction.", this->name_.c_str());
|
||||
this->start_direction_(COVER_OPERATION_IDLE);
|
||||
this->set_timeout(DIRECTION_CHANGE_TIMEOUT_ID, waittime, [this, dir]() { this->start_direction_(dir); });
|
||||
|
||||
this->set_timeout("direction_change", *this->direction_change_waittime_,
|
||||
[this, dir]() { this->start_direction_(dir); });
|
||||
|
||||
} else {
|
||||
this->set_current_operation_(dir, true);
|
||||
this->prev_command_trigger_ = trig;
|
||||
@@ -394,7 +395,7 @@ void FeedbackCover::recompute_position_() {
|
||||
if (this->current_operation == COVER_OPERATION_IDLE)
|
||||
return;
|
||||
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
const uint32_t now = millis();
|
||||
float dir;
|
||||
float action_dur;
|
||||
float min_pos;
|
||||
@@ -450,4 +451,5 @@ void FeedbackCover::recompute_position_() {
|
||||
this->last_recompute_time_ = now;
|
||||
}
|
||||
|
||||
} // namespace esphome::feedback
|
||||
} // namespace feedback
|
||||
} // namespace esphome
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
#endif
|
||||
#include "esphome/components/cover/cover.h"
|
||||
|
||||
namespace esphome::feedback {
|
||||
namespace esphome {
|
||||
namespace feedback {
|
||||
|
||||
class FeedbackCover : public cover::Cover, public Component {
|
||||
public:
|
||||
@@ -84,4 +85,5 @@ class FeedbackCover : public cover::Cover, public Component {
|
||||
uint32_t update_interval_{1000};
|
||||
};
|
||||
|
||||
} // namespace esphome::feedback
|
||||
} // namespace feedback
|
||||
} // namespace esphome
|
||||
|
||||
@@ -85,7 +85,7 @@ void HonClimate::set_horizontal_airflow(hon_protocol::HorizontalSwingMode direct
|
||||
this->force_send_control_ = true;
|
||||
}
|
||||
|
||||
const char *HonClimate::get_cleaning_status_text() const {
|
||||
std::string HonClimate::get_cleaning_status_text() const {
|
||||
switch (this->cleaning_status_) {
|
||||
case CleaningState::SELF_CLEAN:
|
||||
return "Self clean";
|
||||
@@ -134,22 +134,29 @@ haier_protocol::HandlerError HonClimate::get_device_version_answer_handler_(haie
|
||||
}
|
||||
// All OK
|
||||
hon_protocol::DeviceVersionAnswer *answr = (hon_protocol::DeviceVersionAnswer *) data;
|
||||
HardwareInfo info{}; // zero-init guarantees null-termination
|
||||
strncpy(info.protocol_version_, answr->protocol_version, HARDWARE_INFO_STR_SIZE - 1);
|
||||
strncpy(info.software_version_, answr->software_version, HARDWARE_INFO_STR_SIZE - 1);
|
||||
strncpy(info.hardware_version_, answr->hardware_version, HARDWARE_INFO_STR_SIZE - 1);
|
||||
strncpy(info.device_name_, answr->device_name, HARDWARE_INFO_STR_SIZE - 1);
|
||||
info.functions_[0] = (answr->functions[1] & 0x01) != 0; // interactive mode support
|
||||
info.functions_[1] = (answr->functions[1] & 0x02) != 0; // controller-device mode support
|
||||
info.functions_[2] = (answr->functions[1] & 0x04) != 0; // crc support
|
||||
info.functions_[3] = (answr->functions[1] & 0x08) != 0; // multiple AC support
|
||||
info.functions_[4] = (answr->functions[1] & 0x20) != 0; // roles support
|
||||
this->use_crc_ = info.functions_[2];
|
||||
char tmp[9];
|
||||
tmp[8] = 0;
|
||||
strncpy(tmp, answr->protocol_version, 8);
|
||||
this->hvac_hardware_info_ = HardwareInfo();
|
||||
this->hvac_hardware_info_.value().protocol_version_ = std::string(tmp);
|
||||
strncpy(tmp, answr->software_version, 8);
|
||||
this->hvac_hardware_info_.value().software_version_ = std::string(tmp);
|
||||
strncpy(tmp, answr->hardware_version, 8);
|
||||
this->hvac_hardware_info_.value().hardware_version_ = std::string(tmp);
|
||||
strncpy(tmp, answr->device_name, 8);
|
||||
this->hvac_hardware_info_.value().device_name_ = std::string(tmp);
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
this->update_sub_text_sensor_(SubTextSensorType::APPLIANCE_NAME, info.device_name_);
|
||||
this->update_sub_text_sensor_(SubTextSensorType::PROTOCOL_VERSION, info.protocol_version_);
|
||||
this->update_sub_text_sensor_(SubTextSensorType::APPLIANCE_NAME, this->hvac_hardware_info_.value().device_name_);
|
||||
this->update_sub_text_sensor_(SubTextSensorType::PROTOCOL_VERSION,
|
||||
this->hvac_hardware_info_.value().protocol_version_);
|
||||
#endif
|
||||
this->hvac_hardware_info_ = info;
|
||||
this->hvac_hardware_info_.value().functions_[0] = (answr->functions[1] & 0x01) != 0; // interactive mode support
|
||||
this->hvac_hardware_info_.value().functions_[1] =
|
||||
(answr->functions[1] & 0x02) != 0; // controller-device mode support
|
||||
this->hvac_hardware_info_.value().functions_[2] = (answr->functions[1] & 0x04) != 0; // crc support
|
||||
this->hvac_hardware_info_.value().functions_[3] = (answr->functions[1] & 0x08) != 0; // multiple AC support
|
||||
this->hvac_hardware_info_.value().functions_[4] = (answr->functions[1] & 0x20) != 0; // roles support
|
||||
this->use_crc_ = this->hvac_hardware_info_.value().functions_[2];
|
||||
this->set_phase(ProtocolPhases::SENDING_INIT_2);
|
||||
return result;
|
||||
} else {
|
||||
@@ -340,9 +347,10 @@ void HonClimate::dump_config() {
|
||||
" Device software version: %s\n"
|
||||
" Device hardware version: %s\n"
|
||||
" Device name: %s",
|
||||
this->hvac_hardware_info_.value().protocol_version_,
|
||||
this->hvac_hardware_info_.value().software_version_,
|
||||
this->hvac_hardware_info_.value().hardware_version_, this->hvac_hardware_info_.value().device_name_);
|
||||
this->hvac_hardware_info_.value().protocol_version_.c_str(),
|
||||
this->hvac_hardware_info_.value().software_version_.c_str(),
|
||||
this->hvac_hardware_info_.value().hardware_version_.c_str(),
|
||||
this->hvac_hardware_info_.value().device_name_.c_str());
|
||||
ESP_LOGCONFIG(TAG, " Device features:%s%s%s%s%s",
|
||||
(this->hvac_hardware_info_.value().functions_[0] ? " interactive" : ""),
|
||||
(this->hvac_hardware_info_.value().functions_[1] ? " controller-device" : ""),
|
||||
@@ -452,7 +460,7 @@ void HonClimate::process_phase(std::chrono::steady_clock::time_point now) {
|
||||
if (this->action_request_.has_value()) {
|
||||
if (this->action_request_.value().message.has_value()) {
|
||||
this->send_message_(this->action_request_.value().message.value(), this->use_crc_);
|
||||
this->action_request_.value().message.reset(); // NOLINT(bugprone-unchecked-optional-access)
|
||||
this->action_request_.value().message.reset();
|
||||
} else {
|
||||
// Message already sent, reseting request and return to idle
|
||||
this->action_request_.reset();
|
||||
@@ -788,7 +796,7 @@ void HonClimate::set_sub_text_sensor(SubTextSensorType type, text_sensor::TextSe
|
||||
}
|
||||
}
|
||||
|
||||
void HonClimate::update_sub_text_sensor_(SubTextSensorType type, const char *value) {
|
||||
void HonClimate::update_sub_text_sensor_(SubTextSensorType type, const std::string &value) {
|
||||
size_t index = (size_t) type;
|
||||
if (this->sub_text_sensors_[index] != nullptr)
|
||||
this->sub_text_sensors_[index]->publish_state(value);
|
||||
|
||||
@@ -90,7 +90,7 @@ class HonClimate : public HaierClimateBase {
|
||||
void set_sub_text_sensor(SubTextSensorType type, text_sensor::TextSensor *sens);
|
||||
|
||||
protected:
|
||||
void update_sub_text_sensor_(SubTextSensorType type, const char *value);
|
||||
void update_sub_text_sensor_(SubTextSensorType type, const std::string &value);
|
||||
text_sensor::TextSensor *sub_text_sensors_[(size_t) SubTextSensorType::SUB_TEXT_SENSOR_TYPE_COUNT]{nullptr};
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
@@ -116,7 +116,7 @@ class HonClimate : public HaierClimateBase {
|
||||
void set_vertical_airflow(hon_protocol::VerticalSwingMode direction);
|
||||
esphome::optional<hon_protocol::HorizontalSwingMode> get_horizontal_airflow() const;
|
||||
void set_horizontal_airflow(hon_protocol::HorizontalSwingMode direction);
|
||||
const char *get_cleaning_status_text() const;
|
||||
std::string get_cleaning_status_text() const;
|
||||
CleaningState get_cleaning_status() const;
|
||||
void start_self_cleaning();
|
||||
void start_steri_cleaning();
|
||||
@@ -166,12 +166,11 @@ class HonClimate : public HaierClimateBase {
|
||||
void fill_control_messages_queue_();
|
||||
void clear_control_messages_queue_();
|
||||
|
||||
static constexpr size_t HARDWARE_INFO_STR_SIZE = 9;
|
||||
struct HardwareInfo {
|
||||
char protocol_version_[HARDWARE_INFO_STR_SIZE];
|
||||
char software_version_[HARDWARE_INFO_STR_SIZE];
|
||||
char hardware_version_[HARDWARE_INFO_STR_SIZE];
|
||||
char device_name_[HARDWARE_INFO_STR_SIZE];
|
||||
std::string protocol_version_;
|
||||
std::string software_version_;
|
||||
std::string hardware_version_;
|
||||
std::string device_name_;
|
||||
bool functions_[5];
|
||||
};
|
||||
|
||||
|
||||
@@ -191,7 +191,7 @@ void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now)
|
||||
if (this->action_request_.has_value()) {
|
||||
if (this->action_request_.value().message.has_value()) {
|
||||
this->send_message_(this->action_request_.value().message.value(), this->use_crc_);
|
||||
this->action_request_.value().message.reset(); // NOLINT(bugprone-unchecked-optional-access)
|
||||
this->action_request_.value().message.reset();
|
||||
} else {
|
||||
// Message already sent, reseting request and return to idle
|
||||
this->action_request_.reset();
|
||||
@@ -210,9 +210,8 @@ void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now)
|
||||
#ifdef USE_WIFI
|
||||
else if (this->send_wifi_signal_ &&
|
||||
(std::chrono::duration_cast<std::chrono::milliseconds>(now - this->last_signal_request_).count() >
|
||||
SIGNAL_LEVEL_UPDATE_INTERVAL_MS)) {
|
||||
SIGNAL_LEVEL_UPDATE_INTERVAL_MS))
|
||||
this->set_phase(ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST);
|
||||
}
|
||||
#endif
|
||||
} break;
|
||||
default:
|
||||
|
||||
@@ -1,16 +1,74 @@
|
||||
#ifdef USE_HOST
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "preferences.h"
|
||||
|
||||
#include <csignal>
|
||||
#include <sched.h>
|
||||
#include <time.h>
|
||||
#include <cstdlib>
|
||||
|
||||
namespace {
|
||||
volatile sig_atomic_t s_signal_received = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
void signal_handler(int signal) { s_signal_received = signal; }
|
||||
} // namespace
|
||||
|
||||
// HAL functions live in hal.cpp.
|
||||
namespace esphome {
|
||||
|
||||
void HOT yield() { ::sched_yield(); }
|
||||
uint32_t IRAM_ATTR HOT millis() {
|
||||
struct timespec spec;
|
||||
clock_gettime(CLOCK_MONOTONIC, &spec);
|
||||
return static_cast<uint32_t>(spec.tv_sec * 1000ULL + spec.tv_nsec / 1000000);
|
||||
}
|
||||
uint64_t millis_64() {
|
||||
struct timespec spec;
|
||||
clock_gettime(CLOCK_MONOTONIC, &spec);
|
||||
return static_cast<uint64_t>(spec.tv_sec) * 1000ULL + static_cast<uint64_t>(spec.tv_nsec) / 1000000ULL;
|
||||
}
|
||||
void HOT delay(uint32_t ms) {
|
||||
struct timespec ts;
|
||||
ts.tv_sec = ms / 1000;
|
||||
ts.tv_nsec = (ms % 1000) * 1000000;
|
||||
int res;
|
||||
do {
|
||||
res = nanosleep(&ts, &ts);
|
||||
} while (res != 0 && errno == EINTR);
|
||||
}
|
||||
uint32_t IRAM_ATTR HOT micros() {
|
||||
struct timespec spec;
|
||||
clock_gettime(CLOCK_MONOTONIC, &spec);
|
||||
return static_cast<uint32_t>(spec.tv_sec * 1000000ULL + spec.tv_nsec / 1000);
|
||||
}
|
||||
void IRAM_ATTR HOT delayMicroseconds(uint32_t us) {
|
||||
struct timespec ts;
|
||||
ts.tv_sec = us / 1000000U;
|
||||
ts.tv_nsec = (us % 1000000U) * 1000U;
|
||||
int res;
|
||||
do {
|
||||
res = nanosleep(&ts, &ts);
|
||||
} while (res != 0 && errno == EINTR);
|
||||
}
|
||||
void arch_restart() { exit(0); }
|
||||
void arch_init() {
|
||||
// pass
|
||||
}
|
||||
void HOT arch_feed_wdt() {
|
||||
// pass
|
||||
}
|
||||
|
||||
uint32_t arch_get_cpu_cycle_count() {
|
||||
struct timespec spec;
|
||||
clock_gettime(CLOCK_MONOTONIC, &spec);
|
||||
time_t seconds = spec.tv_sec;
|
||||
uint32_t us = spec.tv_nsec;
|
||||
return ((uint32_t) seconds) * 1000000000U + us;
|
||||
}
|
||||
uint32_t arch_get_cpu_freq_hz() { return 1000000000U; }
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
void setup();
|
||||
void loop();
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
#ifdef USE_HOST
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <time.h>
|
||||
#include <cerrno>
|
||||
#include <cstdlib>
|
||||
|
||||
// Empty host namespace block to satisfy ci-custom's lint_namespace check.
|
||||
// HAL functions live in namespace esphome (root) — they are not part of the
|
||||
// host component's API.
|
||||
namespace esphome::host {} // namespace esphome::host
|
||||
|
||||
namespace esphome {
|
||||
|
||||
// yield(), arch_init(), arch_feed_wdt(), arch_get_cpu_freq_hz() inlined in
|
||||
// components/host/hal.h.
|
||||
|
||||
uint32_t IRAM_ATTR HOT millis() {
|
||||
struct timespec spec;
|
||||
clock_gettime(CLOCK_MONOTONIC, &spec);
|
||||
return static_cast<uint32_t>(spec.tv_sec * 1000ULL + spec.tv_nsec / 1000000);
|
||||
}
|
||||
uint64_t millis_64() {
|
||||
struct timespec spec;
|
||||
clock_gettime(CLOCK_MONOTONIC, &spec);
|
||||
return static_cast<uint64_t>(spec.tv_sec) * 1000ULL + static_cast<uint64_t>(spec.tv_nsec) / 1000000ULL;
|
||||
}
|
||||
void HOT delay(uint32_t ms) {
|
||||
struct timespec ts;
|
||||
ts.tv_sec = ms / 1000;
|
||||
ts.tv_nsec = (ms % 1000) * 1000000;
|
||||
int res;
|
||||
do {
|
||||
res = nanosleep(&ts, &ts);
|
||||
} while (res != 0 && errno == EINTR);
|
||||
}
|
||||
uint32_t IRAM_ATTR HOT micros() {
|
||||
struct timespec spec;
|
||||
clock_gettime(CLOCK_MONOTONIC, &spec);
|
||||
return static_cast<uint32_t>(spec.tv_sec * 1000000ULL + spec.tv_nsec / 1000);
|
||||
}
|
||||
void IRAM_ATTR HOT delayMicroseconds(uint32_t us) {
|
||||
struct timespec ts;
|
||||
ts.tv_sec = us / 1000000U;
|
||||
ts.tv_nsec = (us % 1000000U) * 1000U;
|
||||
int res;
|
||||
do {
|
||||
res = nanosleep(&ts, &ts);
|
||||
} while (res != 0 && errno == EINTR);
|
||||
}
|
||||
void arch_restart() { exit(0); }
|
||||
|
||||
uint32_t arch_get_cpu_cycle_count() {
|
||||
struct timespec spec;
|
||||
clock_gettime(CLOCK_MONOTONIC, &spec);
|
||||
time_t seconds = spec.tv_sec;
|
||||
uint32_t ns = static_cast<uint32_t>(spec.tv_nsec);
|
||||
return static_cast<uint32_t>(seconds) * 1000000000U + ns;
|
||||
}
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_HOST
|
||||
@@ -1,34 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_HOST
|
||||
|
||||
#include <cstdint>
|
||||
#include <sched.h>
|
||||
|
||||
#define IRAM_ATTR
|
||||
#define PROGMEM
|
||||
|
||||
namespace esphome::host {}
|
||||
|
||||
namespace esphome {
|
||||
|
||||
/// Returns true when executing inside an interrupt handler.
|
||||
/// Host has no ISR concept.
|
||||
__attribute__((always_inline)) inline bool in_isr_context() { return false; }
|
||||
|
||||
__attribute__((always_inline)) inline void yield() { ::sched_yield(); }
|
||||
|
||||
void delay(uint32_t ms);
|
||||
uint32_t micros();
|
||||
uint32_t millis();
|
||||
uint64_t millis_64();
|
||||
void delayMicroseconds(uint32_t us); // NOLINT(readability-identifier-naming)
|
||||
uint32_t arch_get_cpu_cycle_count();
|
||||
|
||||
__attribute__((always_inline)) inline void arch_init() {}
|
||||
__attribute__((always_inline)) inline void arch_feed_wdt() {}
|
||||
__attribute__((always_inline)) inline uint32_t arch_get_cpu_freq_hz() { return 1000000000U; }
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_HOST
|
||||
@@ -462,7 +462,7 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
|
||||
this->request_headers_.push_back({key, value});
|
||||
}
|
||||
|
||||
void add_collect_header(const char *value) { this->lower_case_collect_headers_.emplace_back(value); }
|
||||
void add_collect_header(const char *value) { this->lower_case_collect_headers_.push_back(value); }
|
||||
|
||||
void init_json(size_t count) { this->json_.init(count); }
|
||||
void add_json(const char *key, TemplatableValue<std::string, Ts...> value) { this->json_.push_back({key, value}); }
|
||||
|
||||
@@ -243,7 +243,7 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
|
||||
// Non-chunked path
|
||||
int available_data = stream_ptr->available();
|
||||
size_t remaining = (this->content_length > 0) ? (this->content_length - this->bytes_read_) : max_len;
|
||||
int bufsize = std::min({max_len, remaining, (size_t) available_data});
|
||||
int bufsize = std::min(max_len, std::min(remaining, (size_t) available_data));
|
||||
|
||||
if (bufsize == 0) {
|
||||
this->duration_ms += (millis() - start);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
#include "i2c_bus.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
struct device; // NOLINT(readability-identifier-naming) - forward decl of Zephyr's device type
|
||||
struct device;
|
||||
|
||||
namespace esphome::i2c {
|
||||
|
||||
|
||||
@@ -744,28 +744,21 @@ async def write_image(config, all_frames=False):
|
||||
if frame_count <= 1:
|
||||
_LOGGER.warning("Image file %s has no animation frames", path)
|
||||
|
||||
# Encode each frame with its own encoder and concatenate. This keeps every
|
||||
# frame self-contained on disk (e.g. RGB565+alpha emits [RGB plane | alpha plane]
|
||||
# per frame) so animation frame stepping in image.cpp / animation.cpp stays
|
||||
# correct without needing to know the total frame count.
|
||||
byte_order = config.get(CONF_BYTE_ORDER)
|
||||
combined_data: list[int] = []
|
||||
encoder: ImageEncoder | None = None
|
||||
total_rows = height * frame_count
|
||||
encoder = IMAGE_TYPE[type](width, total_rows, transparency, dither, invert_alpha)
|
||||
if byte_order := config.get(CONF_BYTE_ORDER):
|
||||
# Check for valid type has already been done in validate_settings
|
||||
encoder.set_big_endian(byte_order == "BIG_ENDIAN")
|
||||
for frame_index in range(frame_count):
|
||||
image.seek(frame_index)
|
||||
encoder = IMAGE_TYPE[type](width, height, transparency, dither, invert_alpha)
|
||||
if byte_order is not None:
|
||||
# Check for valid type has already been done in validate_settings
|
||||
encoder.set_big_endian(byte_order == "BIG_ENDIAN")
|
||||
pixels = encoder.convert(image.resize((width, height)), path).getdata()
|
||||
for row in range(height):
|
||||
for col in range(width):
|
||||
encoder.encode(pixels[row * width + col])
|
||||
encoder.end_row()
|
||||
encoder.end_image()
|
||||
combined_data.extend(encoder.data)
|
||||
encoder.end_image()
|
||||
|
||||
rhs = [HexInt(x) for x in combined_data]
|
||||
rhs = [HexInt(x) for x in encoder.data]
|
||||
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
|
||||
image_type = get_image_type_enum(type)
|
||||
trans_value = get_transparency_enum(encoder.transparency)
|
||||
|
||||
@@ -41,12 +41,12 @@ bool InkbirdIbstH1Mini::parse_device(const esp32_ble_tracker::ESPBTDevice &devic
|
||||
ESP_LOGVV(TAG, "parse_device(): service_data is expected to be empty");
|
||||
return false;
|
||||
}
|
||||
const auto &mnf_datas = device.get_manufacturer_datas();
|
||||
auto mnf_datas = device.get_manufacturer_datas();
|
||||
if (mnf_datas.size() != 1) {
|
||||
ESP_LOGVV(TAG, "parse_device(): manufacturer_datas is expected to have a single element");
|
||||
return false;
|
||||
}
|
||||
const auto &mnf_data = mnf_datas[0];
|
||||
auto mnf_data = mnf_datas[0];
|
||||
if (mnf_data.uuid.get_uuid().len != ESP_UUID_LEN_16) {
|
||||
ESP_LOGVV(TAG, "parse_device(): manufacturer data element is expected to have uuid of length 16");
|
||||
return false;
|
||||
|
||||
@@ -39,8 +39,7 @@ bool parse_json(const uint8_t *data, size_t len, const json_parse_t &f) {
|
||||
}
|
||||
|
||||
JsonDocument parse_json(const uint8_t *data, size_t len) {
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks,clang-analyzer-core.StackAddressEscape) false positives with
|
||||
// ArduinoJson
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
if (data == nullptr || len == 0) {
|
||||
ESP_LOGE(TAG, "No data to parse");
|
||||
return JsonObject(); // return unbound object
|
||||
@@ -64,7 +63,7 @@ JsonDocument parse_json(const uint8_t *data, size_t len) {
|
||||
}
|
||||
ESP_LOGE(TAG, "Parse error: %s", err.c_str());
|
||||
return JsonObject(); // return unbound object
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks,clang-analyzer-core.StackAddressEscape)
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
}
|
||||
|
||||
SerializationBuffer<> JsonBuilder::serialize() {
|
||||
|
||||
@@ -139,12 +139,12 @@ void KamstrupKMPComponent::clear_uart_rx_buffer_() {
|
||||
|
||||
void KamstrupKMPComponent::read_command_(uint16_t command) {
|
||||
uint8_t buffer[20] = {0};
|
||||
size_t buffer_len = 0;
|
||||
int buffer_len = 0;
|
||||
int data;
|
||||
int timeout = 250; // ms
|
||||
|
||||
// Read the data from the UART
|
||||
while (timeout > 0 && buffer_len < sizeof(buffer)) {
|
||||
while (timeout > 0 && buffer_len < static_cast<int>(sizeof(buffer))) {
|
||||
if (this->available()) {
|
||||
data = this->read();
|
||||
if (data > -1) {
|
||||
@@ -183,7 +183,7 @@ void KamstrupKMPComponent::read_command_(uint16_t command) {
|
||||
// Decode
|
||||
uint8_t msg[20] = {0};
|
||||
int msg_len = 0;
|
||||
for (size_t i = 1; i < buffer_len - 1; i++) {
|
||||
for (int i = 1; i < buffer_len - 1; i++) {
|
||||
if (buffer[i] == 0x1B) {
|
||||
msg[msg_len++] = buffer[i + 1] ^ 0xFF;
|
||||
i++;
|
||||
|
||||
@@ -37,7 +37,6 @@ from .const import (
|
||||
CONF_UART_PORT,
|
||||
FAMILIES,
|
||||
FAMILY_BK7231N,
|
||||
FAMILY_BK7238,
|
||||
FAMILY_COMPONENT,
|
||||
FAMILY_FRIENDLY,
|
||||
FAMILY_RTL8710B,
|
||||
@@ -57,22 +56,19 @@ CODEOWNERS = ["@kuba2k2"]
|
||||
AUTO_LOAD = ["preferences"]
|
||||
IS_TARGET_PLATFORM = True
|
||||
|
||||
# BLE 5.x BK SDK options to disable unused features.
|
||||
# BK7231N SDK options to disable unused features.
|
||||
# Disabling BLE saves ~21KB RAM and ~200KB Flash because BLE init code is
|
||||
# called unconditionally by the SDK. ESPHome doesn't use BLE on LibreTiny.
|
||||
#
|
||||
# This only works on BLE 5.x BK chips (BK7231N, BK7238). Other BK72XX chips
|
||||
# using BLE 4.2 (BK7231T, BK7231Q, BK7251; BK7252 boards use the BK7251 family)
|
||||
# have a bug where the BLE library still links and references undefined symbols
|
||||
# when CFG_SUPPORT_BLE=0.
|
||||
#
|
||||
# On BK7238 the SDK also hangs at WiFi STA enable when BLE init runs, so
|
||||
# disabling it is required for reliable boot, not just an optimization.
|
||||
# This only works on BK7231N (BLE 5.x). Other BK72XX chips using BLE 4.2
|
||||
# (BK7231T, BK7231Q, BK7251; BK7252 boards use the BK7251 family) have a bug
|
||||
# where the BLE library still links and references undefined symbols when
|
||||
# CFG_SUPPORT_BLE=0.
|
||||
#
|
||||
# Other options like CFG_TX_EVM_TEST, CFG_RX_SENSITIVITY_TEST, CFG_SUPPORT_BKREG,
|
||||
# CFG_SUPPORT_OTA_HTTP, and CFG_USE_SPI_SLAVE were evaluated but provide no # NOLINT
|
||||
# measurable benefit - the linker already strips unreferenced code via -gc-sections.
|
||||
_BLE5_BK_SYS_CONFIG_OPTIONS = [
|
||||
_BK7231N_SYS_CONFIG_OPTIONS = [
|
||||
"CFG_SUPPORT_BLE=0",
|
||||
]
|
||||
|
||||
@@ -553,9 +549,9 @@ async def component_to_code(config):
|
||||
cg.add_platformio_option("custom_fw_version", __version__)
|
||||
|
||||
# Apply chip-specific SDK options to save RAM/Flash
|
||||
if config[CONF_FAMILY] in (FAMILY_BK7231N, FAMILY_BK7238):
|
||||
if config[CONF_FAMILY] == FAMILY_BK7231N:
|
||||
cg.add_platformio_option(
|
||||
"custom_options.sys_config#h", _BLE5_BK_SYS_CONFIG_OPTIONS
|
||||
"custom_options.sys_config#h", _BK7231N_SYS_CONFIG_OPTIONS
|
||||
)
|
||||
|
||||
# Tune lwIP for ESPHome's actual needs.
|
||||
|
||||
@@ -1,6 +1,81 @@
|
||||
#ifdef USE_LIBRETINY
|
||||
|
||||
// HAL functions live in hal.cpp. core.cpp is intentionally empty for
|
||||
// libretiny — there is no extra component bootstrap to keep here.
|
||||
#include "core.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/time_64.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "preferences.h"
|
||||
|
||||
#include <FreeRTOS.h>
|
||||
#include <task.h>
|
||||
|
||||
void setup();
|
||||
void loop();
|
||||
|
||||
namespace esphome {
|
||||
|
||||
void HOT yield() { ::yield(); }
|
||||
// Inline the tick read so esphome::millis() matches MillisInternal::get()'s fast
|
||||
// path instead of going through the Arduino core's out-of-line ::millis() wrapper.
|
||||
//
|
||||
// RTL87xx / LN882x (1 kHz): xTaskGetTickCount() is already ms. IRAM_ATTR + ISR
|
||||
// dispatch are needed because ISR handlers (e.g. rotary_encoder) call millis().
|
||||
//
|
||||
// BK72xx (500 Hz): ticks * portTICK_PERIOD_MS (== 2). IRAM_ATTR and ISR dispatch
|
||||
// are both unnecessary — the SDK masks FIQ + IRQ during flash writes (see hal.h),
|
||||
// so no ISR runs while flash is stalled.
|
||||
#if defined(USE_RTL87XX) || defined(USE_LN882X)
|
||||
uint32_t IRAM_ATTR HOT millis() {
|
||||
static_assert(configTICK_RATE_HZ == 1000, "millis() fast path requires 1 kHz FreeRTOS tick");
|
||||
return in_isr_context() ? xTaskGetTickCountFromISR() : xTaskGetTickCount();
|
||||
}
|
||||
#elif defined(USE_BK72XX)
|
||||
uint32_t HOT millis() {
|
||||
static_assert(configTICK_RATE_HZ == 500, "BK72xx millis() fast path assumes 500 Hz FreeRTOS tick");
|
||||
return xTaskGetTickCount() * portTICK_PERIOD_MS;
|
||||
}
|
||||
#else
|
||||
uint32_t IRAM_ATTR HOT millis() { return ::millis(); }
|
||||
#endif
|
||||
uint64_t millis_64() { return Millis64Impl::compute(millis()); }
|
||||
uint32_t IRAM_ATTR HOT micros() { return ::micros(); }
|
||||
void HOT delay(uint32_t ms) { ::delay(ms); }
|
||||
void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { ::delayMicroseconds(us); }
|
||||
|
||||
void arch_init() {
|
||||
libretiny::setup_preferences();
|
||||
lt_wdt_enable(10000L);
|
||||
#ifdef USE_BK72XX
|
||||
// BK72xx SDK creates the main Arduino task at priority 3, which is lower than
|
||||
// all WiFi (4-5), LwIP (4), and TCP/IP (7) tasks. This causes ~100ms loop
|
||||
// stalls whenever WiFi background processing runs, because the main task
|
||||
// cannot resume until every higher-priority task finishes.
|
||||
//
|
||||
// By contrast, RTL87xx creates the main task at osPriorityRealtime (highest).
|
||||
//
|
||||
// Raise to priority 6: above WiFi/LwIP tasks (4-5) so they don't preempt the
|
||||
// main loop, but below the TCP/IP thread (7) so packet processing keeps priority.
|
||||
// This is safe because ESPHome yields voluntarily via wakeable_delay() and
|
||||
// the Arduino mainTask yield() after each loop() iteration.
|
||||
static constexpr UBaseType_t MAIN_TASK_PRIORITY = 6;
|
||||
static_assert(MAIN_TASK_PRIORITY < configMAX_PRIORITIES, "MAIN_TASK_PRIORITY must be less than configMAX_PRIORITIES");
|
||||
vTaskPrioritySet(nullptr, MAIN_TASK_PRIORITY);
|
||||
#endif
|
||||
#if LT_GPIO_RECOVER
|
||||
lt_gpio_recover();
|
||||
#endif
|
||||
}
|
||||
|
||||
void arch_restart() {
|
||||
lt_reboot();
|
||||
while (1) {
|
||||
}
|
||||
}
|
||||
void HOT arch_feed_wdt() { lt_wdt_feed(); }
|
||||
uint32_t arch_get_cpu_cycle_count() { return lt_cpu_get_cycle_count(); }
|
||||
uint32_t arch_get_cpu_freq_hz() { return lt_cpu_get_freq(); }
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_LIBRETINY
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
#ifdef USE_LIBRETINY
|
||||
|
||||
#include "core.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "preferences.h"
|
||||
|
||||
#include <FreeRTOS.h>
|
||||
#include <task.h>
|
||||
|
||||
// Empty libretiny namespace block to satisfy ci-custom's lint_namespace check.
|
||||
// HAL functions live in namespace esphome (root) — they are not part of the
|
||||
// libretiny component's API.
|
||||
namespace esphome::libretiny {} // namespace esphome::libretiny
|
||||
|
||||
namespace esphome {
|
||||
|
||||
// yield(), delay(), micros(), millis(), millis_64(), delayMicroseconds(),
|
||||
// arch_feed_wdt(), arch_get_cpu_cycle_count(), arch_get_cpu_freq_hz()
|
||||
// inlined in components/libretiny/hal.h.
|
||||
|
||||
void arch_init() {
|
||||
libretiny::setup_preferences();
|
||||
lt_wdt_enable(10000L);
|
||||
#ifdef USE_BK72XX
|
||||
// BK72xx SDK creates the main Arduino task at priority 3, which is lower than
|
||||
// all WiFi (4-5), LwIP (4), and TCP/IP (7) tasks. This causes ~100ms loop
|
||||
// stalls whenever WiFi background processing runs, because the main task
|
||||
// cannot resume until every higher-priority task finishes.
|
||||
//
|
||||
// By contrast, RTL87xx creates the main task at osPriorityRealtime (highest).
|
||||
//
|
||||
// Raise to priority 6: above WiFi/LwIP tasks (4-5) so they don't preempt the
|
||||
// main loop, but below the TCP/IP thread (7) so packet processing keeps priority.
|
||||
// This is safe because ESPHome yields voluntarily via wakeable_delay() and
|
||||
// the Arduino mainTask yield() after each loop() iteration.
|
||||
static constexpr UBaseType_t MAIN_TASK_PRIORITY = 6;
|
||||
static_assert(MAIN_TASK_PRIORITY < configMAX_PRIORITIES, "MAIN_TASK_PRIORITY must be less than configMAX_PRIORITIES");
|
||||
vTaskPrioritySet(nullptr, MAIN_TASK_PRIORITY);
|
||||
#endif
|
||||
#if LT_GPIO_RECOVER
|
||||
lt_gpio_recover();
|
||||
#endif
|
||||
}
|
||||
|
||||
void arch_restart() {
|
||||
lt_reboot();
|
||||
while (1) {
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_LIBRETINY
|
||||
@@ -1,111 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_LIBRETINY
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// For the inline millis() fast paths (xTaskGetTickCount, portTICK_PERIOD_MS).
|
||||
#include <FreeRTOS.h>
|
||||
#include <task.h>
|
||||
|
||||
#include "esphome/core/time_64.h"
|
||||
|
||||
// IRAM_ATTR places a function in executable RAM so it is callable from an
|
||||
// ISR even while flash is busy (XIP stall, OTA, logger flash write).
|
||||
// Each family uses a section its stock linker already routes to RAM:
|
||||
// RTL8710B → .image2.ram.text, RTL8720C → .sram.text. LN882H is the
|
||||
// exception: its stock linker has no matching glob, so patch_linker.py
|
||||
// injects KEEP(*(.sram.text*)) into .flash_copysection at pre-link.
|
||||
//
|
||||
// BK72xx (all variants) are left as a no-op: their SDK wraps flash
|
||||
// operations in GLOBAL_INT_DISABLE() which masks FIQ + IRQ at the CPU for
|
||||
// the duration of every write, so no ISR fires while flash is stalled and
|
||||
// the race IRAM_ATTR guards against cannot occur. The trade-off is that
|
||||
// interrupts are delayed (not dropped) by up to ~20 ms during a sector
|
||||
// erase, but that is an SDK-level choice and cannot be changed from this
|
||||
// layer.
|
||||
#if defined(USE_BK72XX)
|
||||
#define IRAM_ATTR
|
||||
#elif defined(USE_LIBRETINY_VARIANT_RTL8710B)
|
||||
// Stock linker consumes *(.image2.ram.text*) into .ram_image2.text (> BD_RAM).
|
||||
#define IRAM_ATTR __attribute__((noinline, section(".image2.ram.text")))
|
||||
#else
|
||||
// RTL8720C: stock linker consumes *(.sram.text*) into .ram.code_text.
|
||||
// LN882H: patch_linker.py.script injects *(.sram.text*) into
|
||||
// .flash_copysection (> RAM0 AT> FLASH).
|
||||
#define IRAM_ATTR __attribute__((noinline, section(".sram.text")))
|
||||
#endif
|
||||
#define PROGMEM
|
||||
|
||||
#ifdef USE_BK72XX
|
||||
// Declared in the Beken FreeRTOS port (portmacro.h) and built in ARM mode so
|
||||
// it is callable from Thumb code via interworking. The MRS CPSR instruction
|
||||
// is ARM-only and user code here may be built in Thumb, so in_isr_context()
|
||||
// defers to this port helper on BK72xx instead of reading CPSR inline.
|
||||
extern "C" uint32_t platform_is_in_interrupt_context(void);
|
||||
#endif
|
||||
|
||||
// Forward decls from Arduino's <Arduino.h> for the inline wrappers below.
|
||||
// NOLINTBEGIN(google-runtime-int,readability-identifier-naming,readability-redundant-declaration)
|
||||
extern "C" void yield(void);
|
||||
extern "C" void delay(unsigned long ms);
|
||||
extern "C" unsigned long micros(void);
|
||||
extern "C" unsigned long millis(void);
|
||||
extern "C" void delayMicroseconds(unsigned int us);
|
||||
// NOLINTEND(google-runtime-int,readability-identifier-naming,readability-redundant-declaration)
|
||||
|
||||
// Forward decls from libretiny's <lt_api.h> family for the inline arch_*
|
||||
// wrappers below. Pulling the full header would drag in the rest of the
|
||||
// LibreTiny C API.
|
||||
extern "C" void lt_wdt_feed(void);
|
||||
extern "C" uint32_t lt_cpu_get_cycle_count(void);
|
||||
extern "C" uint32_t lt_cpu_get_freq(void);
|
||||
|
||||
namespace esphome::libretiny {}
|
||||
|
||||
namespace esphome {
|
||||
|
||||
/// Returns true when executing inside an interrupt handler.
|
||||
__attribute__((always_inline)) inline bool in_isr_context() {
|
||||
#if defined(USE_BK72XX)
|
||||
// BK72xx is ARM968E-S (ARM9); see extern declaration above.
|
||||
return platform_is_in_interrupt_context() != 0;
|
||||
#else
|
||||
// Cortex-M (AmebaZ, AmebaZ2, LN882H). IPSR is the active exception number;
|
||||
// non-zero means we're in a handler.
|
||||
uint32_t ipsr;
|
||||
__asm__ volatile("mrs %0, ipsr" : "=r"(ipsr));
|
||||
return ipsr != 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
__attribute__((always_inline)) inline void yield() { ::yield(); }
|
||||
__attribute__((always_inline)) inline void delay(uint32_t ms) { ::delay(ms); }
|
||||
__attribute__((always_inline)) inline uint32_t micros() { return static_cast<uint32_t>(::micros()); }
|
||||
|
||||
// Per-variant millis() fast path — matches MillisInternal::get().
|
||||
#if defined(USE_RTL87XX) || defined(USE_LN882X)
|
||||
static_assert(configTICK_RATE_HZ == 1000, "millis() fast path requires 1 kHz FreeRTOS tick");
|
||||
__attribute__((always_inline)) inline uint32_t millis() {
|
||||
// xTaskGetTickCountFromISR is mandatory in interrupt context per the FreeRTOS API contract.
|
||||
return in_isr_context() ? xTaskGetTickCountFromISR() : xTaskGetTickCount();
|
||||
}
|
||||
#elif defined(USE_BK72XX)
|
||||
static_assert(configTICK_RATE_HZ == 500, "BK72xx millis() fast path assumes 500 Hz FreeRTOS tick");
|
||||
__attribute__((always_inline)) inline uint32_t millis() { return xTaskGetTickCount() * portTICK_PERIOD_MS; }
|
||||
#else
|
||||
__attribute__((always_inline)) inline uint32_t millis() { return static_cast<uint32_t>(::millis()); }
|
||||
#endif
|
||||
__attribute__((always_inline)) inline uint64_t millis_64() { return Millis64Impl::compute(millis()); }
|
||||
|
||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||
__attribute__((always_inline)) inline void delayMicroseconds(uint32_t us) { ::delayMicroseconds(us); }
|
||||
__attribute__((hot, always_inline)) inline void arch_feed_wdt() { lt_wdt_feed(); }
|
||||
__attribute__((always_inline)) inline uint32_t arch_get_cpu_cycle_count() { return lt_cpu_get_cycle_count(); }
|
||||
__attribute__((always_inline)) inline uint32_t arch_get_cpu_freq_hz() { return lt_cpu_get_freq(); }
|
||||
|
||||
void arch_init();
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_LIBRETINY
|
||||
@@ -8,59 +8,84 @@ namespace esphome::light {
|
||||
|
||||
enum class LimitMode { CLAMP, DO_NOTHING };
|
||||
|
||||
template<bool HasTransitionLength, typename... Ts> class ToggleAction : public Action<Ts...> {
|
||||
template<typename... Ts> class ToggleAction : public Action<Ts...> {
|
||||
public:
|
||||
explicit ToggleAction(LightState *state) : state_(state) {}
|
||||
|
||||
template<typename V> void set_transition_length(V value) requires(HasTransitionLength) {
|
||||
this->transition_length_ = value;
|
||||
}
|
||||
TEMPLATABLE_VALUE(uint32_t, transition_length)
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
auto call = this->state_->toggle();
|
||||
if constexpr (HasTransitionLength) {
|
||||
call.set_transition_length(this->transition_length_.optional_value(x...));
|
||||
}
|
||||
call.set_transition_length(this->transition_length_.optional_value(x...));
|
||||
call.perform();
|
||||
}
|
||||
|
||||
protected:
|
||||
LightState *state_;
|
||||
struct NoTransition {};
|
||||
[[no_unique_address]] std::conditional_t<HasTransitionLength, TemplatableFn<uint32_t, Ts...>, NoTransition>
|
||||
transition_length_{};
|
||||
};
|
||||
|
||||
// All configured fields are baked into a single stateless lambda whose
|
||||
// constants live in flash. The action only stores one function pointer
|
||||
// plus one parent pointer, regardless of how many fields the user set.
|
||||
// Trigger args are forwarded to the apply function so user lambdas
|
||||
// (e.g. `brightness: !lambda "return x;"`) keep working.
|
||||
template<typename... Ts> class LightControlAction : public Action<Ts...> {
|
||||
public:
|
||||
using ApplyFn = void (*)(LightState *, LightCall &, const Ts &...);
|
||||
LightControlAction(LightState *parent, ApplyFn apply) : parent_(parent), apply_(apply) {}
|
||||
explicit LightControlAction(LightState *parent) : parent_(parent) {}
|
||||
|
||||
TEMPLATABLE_VALUE(ColorMode, color_mode)
|
||||
TEMPLATABLE_VALUE(bool, state)
|
||||
TEMPLATABLE_VALUE(uint32_t, transition_length)
|
||||
TEMPLATABLE_VALUE(uint32_t, flash_length)
|
||||
TEMPLATABLE_VALUE(float, brightness)
|
||||
TEMPLATABLE_VALUE(float, color_brightness)
|
||||
TEMPLATABLE_VALUE(float, red)
|
||||
TEMPLATABLE_VALUE(float, green)
|
||||
TEMPLATABLE_VALUE(float, blue)
|
||||
TEMPLATABLE_VALUE(float, white)
|
||||
TEMPLATABLE_VALUE(float, color_temperature)
|
||||
TEMPLATABLE_VALUE(float, cold_white)
|
||||
TEMPLATABLE_VALUE(float, warm_white)
|
||||
TEMPLATABLE_VALUE(uint32_t, effect)
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
auto call = this->parent_->make_call();
|
||||
this->apply_(this->parent_, call, x...);
|
||||
if (this->color_mode_.has_value())
|
||||
call.set_color_mode(this->color_mode_.value(x...));
|
||||
if (this->state_.has_value())
|
||||
call.set_state(this->state_.value(x...));
|
||||
if (this->transition_length_.has_value())
|
||||
call.set_transition_length(this->transition_length_.value(x...));
|
||||
if (this->flash_length_.has_value())
|
||||
call.set_flash_length(this->flash_length_.value(x...));
|
||||
if (this->brightness_.has_value())
|
||||
call.set_brightness(this->brightness_.value(x...));
|
||||
if (this->color_brightness_.has_value())
|
||||
call.set_color_brightness(this->color_brightness_.value(x...));
|
||||
if (this->red_.has_value())
|
||||
call.set_red(this->red_.value(x...));
|
||||
if (this->green_.has_value())
|
||||
call.set_green(this->green_.value(x...));
|
||||
if (this->blue_.has_value())
|
||||
call.set_blue(this->blue_.value(x...));
|
||||
if (this->white_.has_value())
|
||||
call.set_white(this->white_.value(x...));
|
||||
if (this->color_temperature_.has_value())
|
||||
call.set_color_temperature(this->color_temperature_.value(x...));
|
||||
if (this->cold_white_.has_value())
|
||||
call.set_cold_white(this->cold_white_.value(x...));
|
||||
if (this->warm_white_.has_value())
|
||||
call.set_warm_white(this->warm_white_.value(x...));
|
||||
if (this->effect_.has_value())
|
||||
call.set_effect(this->effect_.value(x...));
|
||||
call.perform();
|
||||
}
|
||||
|
||||
protected:
|
||||
LightState *parent_;
|
||||
ApplyFn apply_;
|
||||
};
|
||||
|
||||
template<bool HasTransitionLength, typename... Ts> class DimRelativeAction : public Action<Ts...> {
|
||||
template<typename... Ts> class DimRelativeAction : public Action<Ts...> {
|
||||
public:
|
||||
explicit DimRelativeAction(LightState *parent) : parent_(parent) {}
|
||||
|
||||
TEMPLATABLE_VALUE(float, relative_brightness)
|
||||
|
||||
template<typename V> void set_transition_length(V value) requires(HasTransitionLength) {
|
||||
this->transition_length_ = value;
|
||||
}
|
||||
TEMPLATABLE_VALUE(uint32_t, transition_length)
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
auto call = this->parent_->make_call();
|
||||
@@ -74,9 +99,7 @@ template<bool HasTransitionLength, typename... Ts> class DimRelativeAction : pub
|
||||
call.set_state(new_brightness != 0.0f);
|
||||
call.set_brightness(new_brightness);
|
||||
|
||||
if constexpr (HasTransitionLength) {
|
||||
call.set_transition_length(this->transition_length_.optional_value(x...));
|
||||
}
|
||||
call.set_transition_length(this->transition_length_.optional_value(x...));
|
||||
call.perform();
|
||||
}
|
||||
|
||||
@@ -92,9 +115,6 @@ template<bool HasTransitionLength, typename... Ts> class DimRelativeAction : pub
|
||||
float min_brightness_{0.0};
|
||||
float max_brightness_{1.0};
|
||||
LimitMode limit_mode_{LimitMode::CLAMP};
|
||||
struct NoTransition {};
|
||||
[[no_unique_address]] std::conditional_t<HasTransitionLength, TemplatableFn<uint32_t, Ts...>, NoTransition>
|
||||
transition_length_{};
|
||||
};
|
||||
|
||||
template<typename... Ts> class LightIsOnCondition : public Condition<Ts...> {
|
||||
|
||||
@@ -37,7 +37,6 @@ from .types import (
|
||||
AddressableSet,
|
||||
ColorMode,
|
||||
DimRelativeAction,
|
||||
LightCall,
|
||||
LightControlAction,
|
||||
LightIsOffCondition,
|
||||
LightIsOnCondition,
|
||||
@@ -61,10 +60,8 @@ from .types import (
|
||||
)
|
||||
async def light_toggle_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
has_transition_length = CONF_TRANSITION_LENGTH in config
|
||||
toggle_template_arg = cg.TemplateArguments(has_transition_length, *template_arg)
|
||||
var = cg.new_Pvariable(action_id, toggle_template_arg, paren)
|
||||
if has_transition_length:
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
if CONF_TRANSITION_LENGTH in config:
|
||||
template_ = await cg.templatable(
|
||||
config[CONF_TRANSITION_LENGTH], args, cg.uint32
|
||||
)
|
||||
@@ -181,9 +178,9 @@ def _resolve_effect_index(config: ConfigType) -> int:
|
||||
)
|
||||
async def light_control_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
# All configured fields are folded into a single stateless lambda whose
|
||||
# constants live in flash; the action stores only a function pointer.
|
||||
# (config_key, setter_name, c++ type)
|
||||
FIELDS = (
|
||||
(CONF_COLOR_MODE, "set_color_mode", ColorMode),
|
||||
(CONF_STATE, "set_state", cg.bool_),
|
||||
@@ -199,50 +196,38 @@ async def light_control_to_code(config, action_id, template_arg, args):
|
||||
(CONF_COLD_WHITE, "set_cold_white", cg.float_),
|
||||
(CONF_WARM_WHITE, "set_warm_white", cg.float_),
|
||||
)
|
||||
|
||||
fwd_args = ", ".join(name for _, name in args)
|
||||
body_lines: list[str] = []
|
||||
|
||||
for conf_key, setter, type_ in FIELDS:
|
||||
if conf_key not in config:
|
||||
continue
|
||||
value = config[conf_key]
|
||||
if isinstance(value, Lambda):
|
||||
inner = await cg.process_lambda(value, args, return_type=type_)
|
||||
body_lines.append(f"call.{setter}(({inner})({fwd_args}));")
|
||||
else:
|
||||
body_lines.append(f"call.{setter}({cg.safe_exp(value)});")
|
||||
if conf_key in config:
|
||||
template_ = await cg.templatable(config[conf_key], args, type_)
|
||||
cg.add(getattr(var, setter)(template_))
|
||||
|
||||
if CONF_EFFECT in config:
|
||||
if isinstance(config[CONF_EFFECT], Lambda):
|
||||
# Lambda returns a string — wrap in a C++ lambda that resolves
|
||||
# the effect name to its uint32_t index at runtime
|
||||
inner_lambda = await cg.process_lambda(
|
||||
config[CONF_EFFECT], args, return_type=cg.std_string
|
||||
)
|
||||
body_lines.append(
|
||||
f"{{ auto __effect_s = ({inner_lambda})({fwd_args});\n"
|
||||
f"call.set_effect(parent->get_effect_index("
|
||||
f"__effect_s.c_str(), __effect_s.size())); }}"
|
||||
fwd_args = ", ".join(n for _, n in args)
|
||||
# capture="" is correct: paren is a global variable name
|
||||
# string-interpolated into the body at codegen time, not a
|
||||
# C++ runtime capture.
|
||||
wrapper = LambdaExpression(
|
||||
f"auto __effect_s = ({inner_lambda})({fwd_args});\n"
|
||||
f"return {paren}->get_effect_index("
|
||||
f"__effect_s.c_str(), __effect_s.size());",
|
||||
args,
|
||||
capture="",
|
||||
return_type=cg.uint32,
|
||||
)
|
||||
cg.add(var.set_effect(wrapper))
|
||||
else:
|
||||
# Cast disambiguates between set_effect(uint32_t) and
|
||||
# set_effect(optional<uint32_t>) when the literal is an int.
|
||||
body_lines.append(
|
||||
f"call.set_effect(static_cast<uint32_t>({_resolve_effect_index(config)}));"
|
||||
# Static string — resolve effect name to index at codegen time
|
||||
template_ = await cg.templatable(
|
||||
_resolve_effect_index(config), args, cg.uint32
|
||||
)
|
||||
|
||||
# Match LightControlAction::ApplyFn signature: const Ts &... for trigger args.
|
||||
apply_args = [
|
||||
(LightState.operator("ptr"), "parent"),
|
||||
(LightCall.operator("ref"), "call"),
|
||||
*((t.operator("const").operator("ref"), n) for t, n in args),
|
||||
]
|
||||
apply_lambda = LambdaExpression(
|
||||
["\n".join(body_lines)],
|
||||
apply_args,
|
||||
capture="",
|
||||
return_type=cg.void,
|
||||
)
|
||||
return cg.new_Pvariable(action_id, template_arg, paren, apply_lambda)
|
||||
cg.add(var.set_effect(template_))
|
||||
return var
|
||||
|
||||
|
||||
CONF_RELATIVE_BRIGHTNESS = "relative_brightness"
|
||||
@@ -276,12 +261,10 @@ LIGHT_DIM_RELATIVE_ACTION_SCHEMA = cv.Schema(
|
||||
)
|
||||
async def light_dim_relative_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
has_transition_length = CONF_TRANSITION_LENGTH in config
|
||||
dim_template_arg = cg.TemplateArguments(has_transition_length, *template_arg)
|
||||
var = cg.new_Pvariable(action_id, dim_template_arg, paren)
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
templ = await cg.templatable(config[CONF_RELATIVE_BRIGHTNESS], args, cg.float_)
|
||||
cg.add(var.set_relative_brightness(templ))
|
||||
if has_transition_length:
|
||||
if CONF_TRANSITION_LENGTH in config:
|
||||
templ = await cg.templatable(config[CONF_TRANSITION_LENGTH], args, cg.uint32)
|
||||
cg.add(var.set_transition_length(templ))
|
||||
if conf := config.get(CONF_BRIGHTNESS_LIMITS):
|
||||
|
||||
@@ -13,7 +13,6 @@ Color = cg.esphome_ns.class_("Color")
|
||||
LightColorValues = light_ns.class_("LightColorValues")
|
||||
|
||||
LightStateRTCState = light_ns.struct("LightStateRTCState")
|
||||
LightCall = light_ns.class_("LightCall")
|
||||
|
||||
# Color modes
|
||||
ColorMode = light_ns.enum("ColorMode", is_class=True)
|
||||
|
||||
@@ -4,25 +4,15 @@ from esphome.components.binary_sensor import (
|
||||
new_binary_sensor,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_STATE
|
||||
|
||||
from ..defines import CONF_WIDGET, LV_OBJ_FLAG, LvConstant
|
||||
from ..lvcode import EVENT_ARG, UPDATE_EVENT, LambdaContext, LvContext, lvgl_static
|
||||
from ..types import LV_EVENT, LV_STATE, lv_pseudo_button_t
|
||||
from ..defines import CONF_WIDGET
|
||||
from ..lvcode import EVENT_ARG, LambdaContext, LvContext, lvgl_static
|
||||
from ..types import LV_EVENT, lv_pseudo_button_t
|
||||
from ..widgets import Widget, get_widgets, wait_for_widgets
|
||||
|
||||
STATE_PRESSED = "PRESSED"
|
||||
STATE_CHECKED = "CHECKED"
|
||||
|
||||
BS_STATE = LvConstant(
|
||||
"LV_STATE_",
|
||||
STATE_PRESSED,
|
||||
STATE_CHECKED,
|
||||
)
|
||||
CONFIG_SCHEMA = binary_sensor_schema(BinarySensor).extend(
|
||||
{
|
||||
cv.Required(CONF_WIDGET): cv.use_id(lv_pseudo_button_t),
|
||||
cv.Optional(CONF_STATE, default=STATE_PRESSED): BS_STATE.one_of,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -32,23 +22,16 @@ async def to_code(config):
|
||||
widget = await get_widgets(config, CONF_WIDGET)
|
||||
widget = widget[0]
|
||||
assert isinstance(widget, Widget)
|
||||
state = await BS_STATE.process(config[CONF_STATE])
|
||||
await wait_for_widgets()
|
||||
is_pressed = str(state) == str(LV_STATE.PRESSED)
|
||||
test_expr = widget.is_pressed() if is_pressed else widget.is_checked()
|
||||
async with LambdaContext(EVENT_ARG) as test_ctx:
|
||||
test_ctx.add(sensor.publish_state(test_expr))
|
||||
async with LambdaContext(EVENT_ARG) as pressed_ctx:
|
||||
pressed_ctx.add(sensor.publish_state(widget.is_pressed()))
|
||||
async with LvContext() as ctx:
|
||||
ctx.add(sensor.publish_initial_state(test_expr))
|
||||
if is_pressed:
|
||||
events = [LV_EVENT.PRESSED, LV_EVENT.RELEASED]
|
||||
widget.add_flag(LV_OBJ_FLAG.CLICKABLE)
|
||||
else:
|
||||
events = [LV_EVENT.VALUE_CHANGED, UPDATE_EVENT]
|
||||
ctx.add(sensor.publish_initial_state(widget.is_pressed()))
|
||||
ctx.add(
|
||||
lvgl_static.add_event_cb(
|
||||
widget.obj,
|
||||
await test_ctx.get_lambda(),
|
||||
*events,
|
||||
await pressed_ctx.get_lambda(),
|
||||
LV_EVENT.PRESSED,
|
||||
LV_EVENT.RELEASED,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from operator import itemgetter
|
||||
|
||||
from esphome import config_validation as cv
|
||||
import esphome.codegen as cg
|
||||
from esphome.const import (
|
||||
@@ -13,7 +11,6 @@ from esphome.core import ID
|
||||
from esphome.cpp_generator import MockObj
|
||||
|
||||
from .defines import CONF_GRADIENTS, CONF_OPA, LV_DITHER, add_define, add_warning
|
||||
from .helpers import add_lv_use
|
||||
from .lv_validation import lv_color, lv_percentage, opacity
|
||||
from .lvcode import lv
|
||||
from .types import lv_color_t, lv_gradient_t, lv_opa_t
|
||||
@@ -53,7 +50,6 @@ GRADIENT_SCHEMA = cv.ensure_list(
|
||||
|
||||
|
||||
async def gradients_to_code(config):
|
||||
add_lv_use("gradient")
|
||||
max_stops = 2
|
||||
if any(CONF_DITHER in x for x in config.get(CONF_GRADIENTS, ())):
|
||||
add_warning(
|
||||
@@ -62,7 +58,7 @@ async def gradients_to_code(config):
|
||||
for gradient in config.get(CONF_GRADIENTS, ()):
|
||||
var = MockObj(cg.new_Pvariable(gradient[CONF_ID]), "->")
|
||||
idbase = gradient[CONF_ID].id
|
||||
stops = sorted(gradient[CONF_STOPS], key=itemgetter(CONF_POSITION))
|
||||
stops = gradient[CONF_STOPS]
|
||||
max_stops = max(max_stops, len(stops))
|
||||
if gradient[CONF_DIRECTION].startswith("VER"):
|
||||
lv.grad_vertical_init(var)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import math
|
||||
import re
|
||||
import textwrap
|
||||
|
||||
@@ -86,22 +85,6 @@ def grid_free_space(value):
|
||||
|
||||
grid_spec = cv.Any(size, LvConstant("LV_GRID_", "CONTENT").one_of, grid_free_space)
|
||||
|
||||
|
||||
def grid_dimension(value):
|
||||
"""
|
||||
Validator for a grid `rows` or `columns` value.
|
||||
Accepts either a positive integer (interpreted as that many cells of equal
|
||||
`LV_GRID_FR(1)` size) or a non-empty list of grid specs.
|
||||
"""
|
||||
if isinstance(value, int):
|
||||
value = cv.int_range(min=1)(value)
|
||||
return ["LV_GRID_FR(1)"] * value
|
||||
result = cv.Schema([grid_spec])(value)
|
||||
if not result:
|
||||
raise cv.Invalid("Grid dimension list must contain at least one entry")
|
||||
return result
|
||||
|
||||
|
||||
GRID_CELL_SCHEMA = {
|
||||
cv.Optional(CONF_GRID_CELL_ROW_POS): cv.positive_int,
|
||||
cv.Optional(CONF_GRID_CELL_COLUMN_POS): cv.positive_int,
|
||||
@@ -201,16 +184,7 @@ class DirectionalLayout(FlexLayout):
|
||||
|
||||
|
||||
class GridLayout(Layout):
|
||||
# Match shorthand grid layout strings: "NxM", "Nx" or "xM".
|
||||
# At least one of the two numbers must be present; this is enforced after matching.
|
||||
_GRID_LAYOUT_REGEX = re.compile(r"^\s*(\d+)?\s*x\s*(\d+)?\s*$")
|
||||
|
||||
@staticmethod
|
||||
def _match_shorthand(layout):
|
||||
match = GridLayout._GRID_LAYOUT_REGEX.match(layout)
|
||||
if match is None or (match.group(1) is None and match.group(2) is None):
|
||||
return None
|
||||
return match
|
||||
_GRID_LAYOUT_REGEX = re.compile(r"^\s*(\d+)\s*x\s*(\d+)\s*$")
|
||||
|
||||
def get_type(self):
|
||||
return TYPE_GRID
|
||||
@@ -218,7 +192,7 @@ class GridLayout(Layout):
|
||||
def get_layout_schemas(self, config: dict) -> tuple:
|
||||
layout = config.get(CONF_LAYOUT)
|
||||
if isinstance(layout, str):
|
||||
if GridLayout._match_shorthand(layout):
|
||||
if GridLayout._GRID_LAYOUT_REGEX.match(layout):
|
||||
return (
|
||||
cv.string,
|
||||
{
|
||||
@@ -239,107 +213,59 @@ class GridLayout(Layout):
|
||||
|
||||
if not isinstance(layout, dict) or layout.get(CONF_TYPE).lower() != TYPE_GRID:
|
||||
return None, {}
|
||||
x_default = (
|
||||
"center" if isinstance(layout.get(CONF_GRID_ROWS), int) else cv.UNDEFINED
|
||||
)
|
||||
y_default = (
|
||||
"center" if isinstance(layout.get(CONF_GRID_COLUMNS), int) else cv.UNDEFINED
|
||||
)
|
||||
x_align = layout.get(CONF_GRID_CELL_X_ALIGN, x_default)
|
||||
y_align = layout.get(CONF_GRID_CELL_Y_ALIGN, y_default)
|
||||
return (
|
||||
{
|
||||
cv.Required(CONF_TYPE): cv.one_of(TYPE_GRID, lower=True),
|
||||
cv.Optional(CONF_GRID_ROWS): grid_dimension,
|
||||
cv.Optional(CONF_GRID_COLUMNS): grid_dimension,
|
||||
cv.Required(CONF_GRID_ROWS): [grid_spec],
|
||||
cv.Required(CONF_GRID_COLUMNS): [grid_spec],
|
||||
cv.Optional(CONF_GRID_COLUMN_ALIGN): grid_alignments,
|
||||
cv.Optional(CONF_GRID_ROW_ALIGN): grid_alignments,
|
||||
cv.Optional(CONF_PAD_ROW): padding,
|
||||
cv.Optional(CONF_PAD_COLUMN): padding,
|
||||
cv.Optional(CONF_MULTIPLE_WIDGETS_PER_CELL, default=False): cv.boolean,
|
||||
cv.Optional(CONF_GRID_CELL_X_ALIGN): grid_alignments,
|
||||
cv.Optional(CONF_GRID_CELL_Y_ALIGN): grid_alignments,
|
||||
},
|
||||
{
|
||||
cv.Optional(CONF_GRID_CELL_ROW_POS): cv.positive_int,
|
||||
cv.Optional(CONF_GRID_CELL_COLUMN_POS): cv.positive_int,
|
||||
cv.Optional(CONF_GRID_CELL_ROW_SPAN): cv.int_range(min=1),
|
||||
cv.Optional(CONF_GRID_CELL_COLUMN_SPAN): cv.int_range(min=1),
|
||||
cv.Optional(CONF_GRID_CELL_X_ALIGN, default=x_align): grid_alignments,
|
||||
cv.Optional(CONF_GRID_CELL_Y_ALIGN, default=y_align): grid_alignments,
|
||||
cv.Optional(CONF_GRID_CELL_X_ALIGN): grid_alignments,
|
||||
cv.Optional(CONF_GRID_CELL_Y_ALIGN): grid_alignments,
|
||||
},
|
||||
)
|
||||
|
||||
def validate(self, config: dict):
|
||||
"""
|
||||
Validate the grid layout.
|
||||
The `layout:` key may be a dictionary with `rows` and/or `columns` keys, or a
|
||||
shorthand string in the format "<rows>x<columns>", "<rows>x" or "x<columns>".
|
||||
Either dimension may be omitted, in which case it will be calculated from the
|
||||
other dimension and the number of configured widgets.
|
||||
The `layout:` key may be a dictionary with `rows` and `columns` keys, or a string in the format "rows x columns".
|
||||
Either all cells must have a row and column,
|
||||
or none, in which case the grid layout is auto-generated.
|
||||
:param config:
|
||||
:return: The config updated with auto-generated values
|
||||
"""
|
||||
layout = config.get(CONF_LAYOUT)
|
||||
widgets = config.get(CONF_WIDGETS, [])
|
||||
num_widgets = len(widgets)
|
||||
if isinstance(layout, str):
|
||||
# Shorthand string: "<rows>x<columns>", "<rows>x" or "x<columns>".
|
||||
# Each dimension defaults to LV_GRID_FR(1). A missing dimension is
|
||||
# calculated from the other dimension and the number of widgets.
|
||||
# If the layout is a string, assume it is in the format "rows x columns", implying
|
||||
# a grid layout with the specified number of rows and columns each with CONTENT sizing.
|
||||
layout = layout.strip()
|
||||
match = GridLayout._match_shorthand(layout)
|
||||
if not match:
|
||||
match = GridLayout._GRID_LAYOUT_REGEX.match(layout)
|
||||
if match:
|
||||
rows = int(match.group(1))
|
||||
cols = int(match.group(2))
|
||||
layout = {
|
||||
CONF_TYPE: TYPE_GRID,
|
||||
CONF_GRID_ROWS: ["LV_GRID_FR(1)"] * rows,
|
||||
CONF_GRID_COLUMNS: ["LV_GRID_FR(1)"] * cols,
|
||||
}
|
||||
config[CONF_LAYOUT] = layout
|
||||
else:
|
||||
raise cv.Invalid(
|
||||
f"Invalid grid layout format: {layout!r}, expected "
|
||||
"'<rows>x<columns>', '<rows>x' or 'x<columns>'",
|
||||
f"Invalid grid layout format: {config}, expected 'rows x columns'",
|
||||
[CONF_LAYOUT],
|
||||
)
|
||||
rows_int = int(match.group(1)) if match.group(1) is not None else None
|
||||
cols_int = int(match.group(2)) if match.group(2) is not None else None
|
||||
for label, val in (("row", rows_int), ("column", cols_int)):
|
||||
if val is not None and val < 1:
|
||||
raise cv.Invalid(
|
||||
f"Invalid grid layout {layout!r}: {label} count must be "
|
||||
"at least 1",
|
||||
[CONF_LAYOUT],
|
||||
)
|
||||
if rows_int is not None and cols_int is not None:
|
||||
rows = rows_int
|
||||
cols = cols_int
|
||||
elif rows_int is not None:
|
||||
rows = rows_int
|
||||
cols = max(1, math.ceil(num_widgets / rows)) if num_widgets else 1
|
||||
else:
|
||||
cols = cols_int
|
||||
rows = max(1, math.ceil(num_widgets / cols)) if num_widgets else 1
|
||||
layout = {
|
||||
CONF_TYPE: TYPE_GRID,
|
||||
CONF_GRID_ROWS: ["LV_GRID_FR(1)"] * rows,
|
||||
CONF_GRID_COLUMNS: ["LV_GRID_FR(1)"] * cols,
|
||||
}
|
||||
config[CONF_LAYOUT] = layout
|
||||
# should be guaranteed to be a dict at this point
|
||||
assert isinstance(layout, dict)
|
||||
assert layout.get(CONF_TYPE).lower() == TYPE_GRID
|
||||
rows_list = layout.get(CONF_GRID_ROWS)
|
||||
cols_list = layout.get(CONF_GRID_COLUMNS)
|
||||
if rows_list is None and cols_list is None:
|
||||
raise cv.Invalid(
|
||||
"Grid layout requires at least one of 'rows' or 'columns' to be "
|
||||
"specified",
|
||||
[CONF_LAYOUT],
|
||||
)
|
||||
if rows_list is None:
|
||||
cols = len(cols_list)
|
||||
rows = max(1, math.ceil(num_widgets / cols)) if num_widgets else 1
|
||||
layout[CONF_GRID_ROWS] = ["LV_GRID_FR(1)"] * rows
|
||||
elif cols_list is None:
|
||||
rows = len(rows_list)
|
||||
cols = max(1, math.ceil(num_widgets / rows)) if num_widgets else 1
|
||||
layout[CONF_GRID_COLUMNS] = ["LV_GRID_FR(1)"] * cols
|
||||
allow_multiple = layout.get(CONF_MULTIPLE_WIDGETS_PER_CELL, False)
|
||||
rows = len(layout[CONF_GRID_ROWS])
|
||||
columns = len(layout[CONF_GRID_COLUMNS])
|
||||
@@ -453,8 +379,7 @@ def append_layout_schema(schema, config: dict):
|
||||
textwrap.dedent(
|
||||
"""
|
||||
Invalid 'layout' value
|
||||
layout choices are 'horizontal', 'vertical',
|
||||
'<rows>x<cols>', '<rows>x', 'x<cols>',
|
||||
layout choices are 'horizontal', 'vertical', '<rows>x<cols>',
|
||||
or a dictionary with a 'type' key
|
||||
"""
|
||||
),
|
||||
|
||||
@@ -454,12 +454,10 @@ void LVTouchListener::update(const touchscreen::TouchPoints_t &tpoints) {
|
||||
|
||||
#ifdef USE_LVGL_METER
|
||||
|
||||
int16_t lv_get_needle_angle_for_value(lv_obj_t *obj, int32_t value) {
|
||||
int16_t lv_get_needle_angle_for_value(lv_obj_t *obj, int value) {
|
||||
auto *scale = lv_obj_get_parent(obj);
|
||||
auto min_value = lv_scale_get_range_min_value(scale);
|
||||
auto max_value = lv_scale_get_range_max_value(scale);
|
||||
value = clamp(value, min_value, max_value);
|
||||
return ((value - min_value) * lv_scale_get_angle_range(scale) / (max_value - min_value) +
|
||||
return ((value - min_value) * lv_scale_get_angle_range(scale) / (lv_scale_get_range_max_value(scale) - min_value) +
|
||||
lv_scale_get_rotation((scale))) %
|
||||
360;
|
||||
}
|
||||
@@ -866,32 +864,6 @@ void lv_scale_draw_event_cb(lv_event_t *e, int16_t range_start, int16_t range_en
|
||||
}
|
||||
#endif // USE_LVGL_SCALE
|
||||
|
||||
#ifdef USE_LVGL_GRADIENT
|
||||
/**
|
||||
*
|
||||
* @param dsc The gradient descriptor containing the color stops
|
||||
* @param pos The current position to calculate the color for
|
||||
* @return The color for the given position
|
||||
*/
|
||||
|
||||
lv_color_t lv_grad_calculate_color(const lv_grad_dsc_t *dsc, int32_t pos) {
|
||||
if (dsc->stops_count == 0)
|
||||
return lv_color_black();
|
||||
if (dsc->stops_count == 1 || pos <= dsc->stops[0].frac)
|
||||
return dsc->stops[0].color;
|
||||
if (pos >= dsc->stops[dsc->stops_count - 1].frac)
|
||||
return dsc->stops[dsc->stops_count - 1].color;
|
||||
int i = 1;
|
||||
while (i < dsc->stops_count && dsc->stops[i].frac < pos)
|
||||
i++;
|
||||
auto *stop1 = &dsc->stops[i - 1];
|
||||
auto *stop2 = &dsc->stops[i];
|
||||
int32_t range = stop2->frac - stop1->frac;
|
||||
int32_t offset = pos - stop1->frac;
|
||||
return lv_color_mix(stop2->color, stop1->color, range == 0 ? 0 : (offset * 255) / range);
|
||||
}
|
||||
#endif
|
||||
|
||||
static void lv_container_constructor(const lv_obj_class_t *class_p, lv_obj_t *obj) {
|
||||
LV_TRACE_OBJ_CREATE("begin");
|
||||
LV_UNUSED(class_p);
|
||||
|
||||
@@ -112,19 +112,9 @@ inline void lv_animimg_set_src(lv_obj_t *img, std::vector<image::Image *> images
|
||||
#endif // USE_LVGL_ANIMIMG
|
||||
|
||||
#ifdef USE_LVGL_METER
|
||||
int16_t lv_get_needle_angle_for_value(lv_obj_t *obj, int32_t value);
|
||||
int16_t lv_get_needle_angle_for_value(lv_obj_t *obj, int value);
|
||||
#endif
|
||||
|
||||
#ifdef USE_LVGL_GRADIENT
|
||||
/**
|
||||
*
|
||||
* @param dsc The gradient descriptor containing the color stops
|
||||
* @param pos The current position to calculate the color for
|
||||
* @return The color for the given position
|
||||
*/
|
||||
|
||||
lv_color_t lv_grad_calculate_color(const lv_grad_dsc_t *dsc, int32_t pos);
|
||||
#endif
|
||||
// Parent class for things that wrap an LVGL object
|
||||
class LvCompound {
|
||||
public:
|
||||
|
||||
@@ -1,27 +1,18 @@
|
||||
from collections.abc import Callable
|
||||
import difflib
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const import KEY_METADATA
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_FROM, CONF_ID, CONF_TO
|
||||
from esphome.core import CORE, ID
|
||||
from esphome.cpp_generator import (
|
||||
MockObj,
|
||||
MockObjClass,
|
||||
VariableDeclarationExpression,
|
||||
add_global,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.cpp_generator import MockObj, VariableDeclarationExpression, add_global
|
||||
from esphome.loader import get_component
|
||||
|
||||
CODEOWNERS = ["@clydebarrow"]
|
||||
MULTI_CONF = True
|
||||
DOMAIN = "mapping"
|
||||
|
||||
mapping_ns = cg.esphome_ns.namespace("mapping")
|
||||
mapping_class = mapping_ns.class_("Mapping")
|
||||
|
||||
CONF_DEFAULT_VALUE = "default_value"
|
||||
CONF_ENTRIES = "entries"
|
||||
CONF_CLASS = "class"
|
||||
|
||||
@@ -31,18 +22,11 @@ class IndexType:
|
||||
Represents a type of index in a map.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, validator: Callable, data_type: MockObj, conversion: Callable = None
|
||||
) -> None:
|
||||
def __init__(self, validator, data_type, conversion):
|
||||
self.validator = validator
|
||||
self.data_type = data_type
|
||||
self.conversion = conversion
|
||||
|
||||
async def convert_value(self, value):
|
||||
if self.conversion:
|
||||
return self.conversion(value)
|
||||
return await cg.get_variable(value)
|
||||
|
||||
|
||||
INDEX_TYPES = {
|
||||
"int": IndexType(cv.int_, cg.int_, int),
|
||||
@@ -54,12 +38,6 @@ INDEX_TYPES = {
|
||||
}
|
||||
|
||||
|
||||
class MappingMetaData:
|
||||
def __init__(self, from_: IndexType, to_: IndexType) -> None:
|
||||
self.from_ = from_
|
||||
self.to_ = to_
|
||||
|
||||
|
||||
def to_schema(value):
|
||||
"""
|
||||
Generate a schema for the 'to' field of a map. This can be either one of the index types or a class name.
|
||||
@@ -82,7 +60,7 @@ BASE_SCHEMA = cv.Schema(
|
||||
)
|
||||
|
||||
|
||||
def get_object_type(to_) -> MockObjClass | None:
|
||||
def get_object_type(to_):
|
||||
"""
|
||||
Get the object type from a string. Possible formats:
|
||||
xxx The name of a component which defines INSTANCE_TYPE
|
||||
@@ -103,60 +81,25 @@ def get_object_type(to_) -> MockObjClass | None:
|
||||
return None
|
||||
|
||||
|
||||
def get_all_mapping_metadata() -> dict[str, MappingMetaData]:
|
||||
"""Get all mapping metadata."""
|
||||
return CORE.data.setdefault(DOMAIN, {}).setdefault(KEY_METADATA, {})
|
||||
|
||||
|
||||
def get_mapping_metadata(mapping_id: str) -> MappingMetaData:
|
||||
"""Get mapping metadata by ID for use by other components."""
|
||||
return get_all_mapping_metadata()[mapping_id]
|
||||
|
||||
|
||||
def add_metadata(
|
||||
mapping_id: ID,
|
||||
from_: IndexType,
|
||||
to_: IndexType,
|
||||
) -> None:
|
||||
get_all_mapping_metadata()[mapping_id.id] = MappingMetaData(from_, to_)
|
||||
|
||||
|
||||
def map_schema(config):
|
||||
config = BASE_SCHEMA(config)
|
||||
if CONF_ENTRIES not in config or not isinstance(config[CONF_ENTRIES], dict):
|
||||
raise cv.Invalid("an entries dictionary is required for a mapping")
|
||||
raise cv.Invalid("an entries list is required for a map")
|
||||
entries = config[CONF_ENTRIES]
|
||||
if len(entries) == 0:
|
||||
raise cv.Invalid("A mapping must have at least one entry")
|
||||
raise cv.Invalid("Map must have at least one entry")
|
||||
to_ = config[CONF_TO]
|
||||
if to_ in INDEX_TYPES:
|
||||
value_type = INDEX_TYPES[to_]
|
||||
value_type = INDEX_TYPES[to_].validator
|
||||
else:
|
||||
object_type = get_object_type(to_)
|
||||
if object_type is None:
|
||||
value_type = get_object_type(to_)
|
||||
if value_type is None:
|
||||
matches = difflib.get_close_matches(to_, CORE.id_classes)
|
||||
raise cv.Invalid(
|
||||
f"No known mappable class name matches '{to_}'; did you mean one of {', '.join(matches)}?"
|
||||
)
|
||||
validator = cv.use_id(object_type)
|
||||
value_type = IndexType(validator, object_type)
|
||||
config[CONF_ENTRIES] = {k: value_type.validator(v) for k, v in entries.items()}
|
||||
if (default_value := config.get(CONF_DEFAULT_VALUE)) is not None:
|
||||
config[CONF_DEFAULT_VALUE] = value_type.validator(default_value)
|
||||
unexpected_keys = config.keys() - {
|
||||
CONF_ENTRIES,
|
||||
CONF_TO,
|
||||
CONF_FROM,
|
||||
CONF_ID,
|
||||
CONF_DEFAULT_VALUE,
|
||||
}
|
||||
if unexpected_keys:
|
||||
errors = [
|
||||
cv.Invalid(f"Unexpected key '{k}'", path=[k]) for k in unexpected_keys
|
||||
]
|
||||
raise cv.MultipleInvalid(errors)
|
||||
|
||||
add_metadata(config[CONF_ID], INDEX_TYPES[config[CONF_FROM]], value_type)
|
||||
value_type = cv.use_id(value_type)
|
||||
config[CONF_ENTRIES] = {k: value_type(v) for k, v in entries.items()}
|
||||
return config
|
||||
|
||||
|
||||
@@ -164,19 +107,29 @@ CONFIG_SCHEMA = map_schema
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
entries = config[CONF_ENTRIES]
|
||||
from_ = config[CONF_FROM]
|
||||
to_ = config[CONF_TO]
|
||||
index_conversion = INDEX_TYPES[from_].conversion
|
||||
index_type = INDEX_TYPES[from_].data_type
|
||||
if to_ in INDEX_TYPES:
|
||||
value_conversion = INDEX_TYPES[to_].conversion
|
||||
value_type = INDEX_TYPES[to_].data_type
|
||||
entries = {
|
||||
index_conversion(key): value_conversion(value)
|
||||
for key, value in entries.items()
|
||||
}
|
||||
else:
|
||||
entries = {
|
||||
index_conversion(key): await cg.get_variable(value)
|
||||
for key, value in entries.items()
|
||||
}
|
||||
value_type = get_object_type(to_)
|
||||
if list(entries.values())[0].op != ".":
|
||||
value_type = value_type.operator("ptr")
|
||||
varid = config[CONF_ID]
|
||||
metadata = get_mapping_metadata(varid.id)
|
||||
entries = {
|
||||
metadata.from_.conversion(key): await metadata.to_.convert_value(value)
|
||||
for key, value in config[CONF_ENTRIES].items()
|
||||
}
|
||||
value_type = metadata.to_.data_type
|
||||
# entries guaranteed to be non-empty here.
|
||||
value_0 = list(entries.values())[0]
|
||||
if isinstance(value_0, MockObj) and value_0.op != ".":
|
||||
value_type = value_type.operator("ptr")
|
||||
varid.type = mapping_class.template(
|
||||
metadata.from_.data_type,
|
||||
index_type,
|
||||
value_type,
|
||||
)
|
||||
var = MockObj(varid, ".")
|
||||
@@ -186,6 +139,4 @@ async def to_code(config):
|
||||
|
||||
for key, value in entries.items():
|
||||
cg.add(var.set(key, value))
|
||||
if (default_value := config.get(CONF_DEFAULT_VALUE)) is not None:
|
||||
cg.add(var.set_default_value(await metadata.to_.convert_value(default_value)))
|
||||
return var
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user