Compare commits

..

101 Commits

Author SHA1 Message Date
Petr Kejval
80265a6bd2 [sensor] Add optimistic option to heartbeat filter (#10993)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-10-21 09:17:07 -04:00
J. Nick Koston
87e9a7a1bd [climate] Remove unnecessary vector allocations in state save/restore (#11445) 2025-10-21 04:35:18 -05:00
J. Nick Koston
3aedfe8be3 [binary_sensor] Optimize AutorepeatFilter with FixedVector (#11444) 2025-10-21 04:30:13 -05:00
J. Nick Koston
7f2cc47ed6 [binary_sensor] Add compile test for auto repeat (#11443) 2025-10-21 04:25:59 -05:00
J. Nick Koston
a5542e0d2b [sensor] Optimize calibration and Or filters with FixedVector (#11437) 2025-10-20 21:38:05 -10:00
Keith Burzinski
66afe4a9be [climate] Add some integration tests (#11439) 2025-10-21 02:26:18 -05:00
J. Nick Koston
0ae9009e41 [ci] Fix clang-tidy split mode for core file changes (#11434) 2025-10-20 20:39:50 -10:00
J. Nick Koston
0b2f5fcd7e Add additional sensor filter tests (#11438) 2025-10-20 20:39:21 -10:00
J. Nick Koston
7a2887e2ed [analyze-memory] Improve symbol categorization accuracy (#11440) 2025-10-20 20:39:05 -10:00
J. Nick Koston
cd2d3f061d [espnow] Fix compilation error with initializer_list after #11433 (#11436) 2025-10-20 19:58:24 -10:00
J. Nick Koston
73f5d01c2d [core] Optimize automation actions memory usage with std::initializer_list (#11433) 2025-10-21 04:32:58 +00:00
Jesse Hills
0938609f7a [improv] Put next_url behind defines to save flash (#11420)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-10-21 16:58:26 +13:00
J. Nick Koston
77203f0cb4 [text_sensor] Optimize filters with FixedVector (1.6KB flash savings) (#11423)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-10-21 03:24:51 +00:00
J. Nick Koston
040130e357 [ci] Fix memory impact workflow for new components (#11421) 2025-10-21 16:02:07 +13:00
J. Nick Koston
85959e3004 [sensor,text_sensor,binary_sensor] Optimize filter parameters with std::initializer_list (#11426) 2025-10-21 15:47:13 +13:00
Jonathan Swoboda
a809a13729 [core] Add support for extern "C" includes (#11422) 2025-10-21 15:46:50 +13:00
J. Nick Koston
3b6ff615e8 [ci] Fix clang-tidy split decision to account for component dependencies (#11430) 2025-10-21 15:39:15 +13:00
J. Nick Koston
05216db5f0 ESP8266: Complete testing mode memory patches with DRAM and Flash (#11427) 2025-10-21 15:26:49 +13:00
J. Nick Koston
9f668b0c4b Add basic text_sensor tests (#11424) 2025-10-21 15:26:41 +13:00
J. Nick Koston
6a239f4d1c [ci] Prefer platform-specific tests for memory impact analysis (#11398) 2025-10-21 10:25:33 +13:00
J. Nick Koston
ffb0e854b6 [ci] Optimize clang-tidy for small PRs by avoiding unnecessary job spitting (#11402)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-21 10:24:46 +13:00
Jonathan Swoboda
6fbd0e3385 [esp32_hosted] Bump esp hosted (#11414) 2025-10-20 11:12:07 -10:00
dependabot[bot]
426511e78d Bump actions/download-artifact from 4.3.0 to 5.0.0 (#11419)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-20 11:11:15 -10:00
dependabot[bot]
97d91fee85 Bump pylint from 4.0.1 to 4.0.2 (#11418)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-20 11:10:33 -10:00
J. Nick Koston
0f4b54aa82 [esp32_improv, improv_base] Reduce flash usage by 352 bytes (#11406) 2025-10-20 11:07:39 -10:00
J. Nick Koston
1706a69fad [sensor] Optimize filter memory usage with ValueListFilter base class (#11407)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-21 09:38:49 +13:00
J. Nick Koston
e23d66a8cf [esp32] Automatic CONFIG_LWIP_MAX_SOCKETS configuration based on component needs (#11378) 2025-10-21 09:38:34 +13:00
J. Nick Koston
46101fd830 Add tests for FilterOutValueFilter and ThrottleWithPriorityFilter (#11408)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-20 09:25:03 -10:00
J. Nick Koston
e988905c2f [json] Add basic compile tests (#11409)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-21 07:31:59 +13:00
Patrick
abb57f08f5 [pipsolar] cleanup / refactoring (#10291)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-10-20 17:08:31 +00:00
EasilyBoredEngineer
ca2fe994a1 [espnow] Add transport platform for packet_transport (#11025)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-10-20 10:44:20 -04:00
Peter Zich
03def13917 [hdc1080] Make HDC1080_CMD_CONFIGURATION failure a warning (and log it) (#11355)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-10-20 09:13:13 -04:00
Keith Burzinski
63f100a8ca [bang_bang] Various clean-up (#11356) 2025-10-19 22:56:25 -10:00
Juan Antonio Aldea
ea4e5fd7bd [climate] Migrate components to the new API (#11369)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
2025-10-19 22:20:39 -10:00
Enrico Galli
12e9c5e60e [epaper_spi] Fix busy pin logic (#11349)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-10-20 19:11:09 +13:00
Aman kumar
3d82c5baf7 [esp32_improv]: add next_url support for WiFi provisioning (#10757)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-10-20 19:10:38 +13:00
Keith Burzinski
6f5e36ffc3 [climate] First pass at some optimization (#11366)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-10-19 23:42:54 -05:00
Grant Le Roux
118b1d8593 MQTT Light - Min/Max Color Temperature (#11103)
Co-authored-by: Cram42 <5396871+cram42@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-10-20 17:05:05 +13:00
Jesse Hills
319ba4a504 [cover] Clean up deprecated functions from 2021.9 (#11391) 2025-10-20 04:03:09 +00:00
J. Nick Koston
ae8336c268 [esp32][ci] Fix IRAM overflow in grouped component tests for ESP32-IDF (#11386) 2025-10-20 03:58:03 +00:00
J. Nick Koston
1b38518c63 [tests] Fix flaky test_noise_corrupt_encrypted_frame integration test (#11405) 2025-10-20 03:45:44 +00:00
J. Nick Koston
c00977df54 [climate] Add basic compile tests for climate component (#11404) 2025-10-20 03:27:04 +00:00
J. Nick Koston
255b5a3abd [ci] Skip memory analysis when only Python/config files change in core (#11397) 2025-10-20 16:13:08 +13:00
Clyde Stubbs
dd732dd155 [mipi_rgb] Add Waveshare 5" 1024x600 (#11206) 2025-10-20 14:09:36 +11:00
Jesse Hills
22fec4329f [fan] Clean up deprecated code from 2022.2 (#11392) 2025-10-20 03:02:03 +00:00
Stefan Rado
8f1c4634ec [uponor_smatrix] Use combined 32 bit addresses instead of separate 16 bit system and device addresses (#11066)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-10-19 16:49:06 -10:00
tomaszduda23
c15f1a9be8 [nrf52] add missing defines for tests (#11384)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-10-19 16:11:44 -10:00
J. Nick Koston
11b53096a6 [ci] Fix fork PR workflow failing to find PRs from forks (#11396) 2025-10-19 15:58:05 -10:00
J. Nick Koston
6a18367949 [cli] Add analyze-memory command (#11395)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-20 14:26:37 +13:00
Javier Peletier
a59b1494d8 [substitutions] Recursive substitutions and better jinja error handling and debug help (#10806) 2025-10-20 14:17:16 +13:00
Jesse Hills
e6ce5c58d1 Merge branch 'release' into dev 2025-10-20 13:43:31 +13:00
Jesse Hills
ebc0f5f7c9 Merge pull request #11387 from esphome/bump-2025.10.2
2025.10.2
2025-10-20 13:42:48 +13:00
Juan Antonio Aldea
0f87e7508b remove hexencode due 2022.1 deprecation (#11383) 2025-10-19 13:09:28 -10:00
J. Nick Koston
862bbb7fe1 [ci] Fix memory impact analysis failing on fork PRs (#11380)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-10-19 13:09:09 -10:00
Jesse Hills
020cea80b2 [nextion] Clean up deprecated code from 1.20 (#11393) 2025-10-19 22:16:50 +00:00
Jesse Hills
9c146a7070 [climate] Clean up deprecated functions from 1.20 (#11388) 2025-10-19 22:11:35 +00:00
Jesse Hills
afbd3f77af [light] Clean up deprecated functions from 1.21 (#11389) 2025-10-19 22:08:30 +00:00
Javier Peletier
1e1fefbd0a [substitutions] !extend and !remove now support substitutions and jinja (#11203) 2025-10-20 10:31:25 +13:00
Juan Antonio Aldea
1a2057df30 Migrate from hexencode() to format_hex_pretty() in Kuntze component (#11372) 2025-10-20 10:15:17 +13:00
J. Nick Koston
87ca8784ef [openthread] Backport address resolution support to prevent OTA crash (#11312)
Co-authored-by: Daniel Stiner <danstiner@gmail.com>
2025-10-20 10:12:56 +13:00
Jesse Hills
a186c1062f Bump version to 2025.10.2 2025-10-20 10:06:43 +13:00
Jonathan Swoboda
ea38237f29 [esp32] Fix OTA rollback (#11300)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-10-20 10:06:43 +13:00
J. Nick Koston
6aff1394ad [core] Fix IndexError when OTA devices cannot be resolved (#11311) 2025-10-20 10:06:43 +13:00
Spectre5
0e34d1b64d Change all temperature offsets to temperature_delta (#11347) 2025-10-20 10:06:43 +13:00
tomaszduda23
1483cee0fb [dashboard] fix migration to Path (#11342)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-10-20 10:06:43 +13:00
J. Nick Koston
8c1bd2fd85 [dashboard] Fix binary download with packages using secrets after Path migration (#11313) 2025-10-20 10:06:43 +13:00
Daniel Stiner
ea609dc0f6 [const] Add CONF_OPENTHREAD (#11318) 2025-10-20 10:06:42 +13:00
Jonathan Swoboda
913095f6be [esp32] Reduce tx power on Arduino (#11304) 2025-10-20 10:06:42 +13:00
Jonathan Swoboda
bb24ad4a30 [htu21d] Revert register address change (#11291) 2025-10-20 10:06:42 +13:00
Jonathan Swoboda
0d612fecfc [core] Add ESP32 ROM functions to reserved ids (#11293) 2025-10-20 10:06:42 +13:00
J. Nick Koston
9c235b4140 [datetime] Fix DateTimeStateTrigger compilation when time component is not used (#11287) 2025-10-20 10:06:42 +13:00
J. Nick Koston
70cb1793f3 [wifi] Optimize WiFi scan results with in-place construction (#11330) 2025-10-19 19:53:05 +00:00
J. Nick Koston
3bdd351d49 [wifi] Convert fast_connect to compile-time define, save 156-1024 bytes flash (#11328) 2025-10-19 19:52:33 +00:00
Jonathan Swoboda
b0ea3f57de [esp32] Fix OTA rollback (#11300)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-10-19 09:49:05 -10:00
J. Nick Koston
c9312d5c27 [script] Fix unbounded queue growth, optimize queued mode (default max_runs=5) (#11308)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-10-19 09:42:17 -10:00
J. Nick Koston
33fea90c19 [wifi] Optimize WiFi scanning to reduce copies and heap allocations (#11323) 2025-10-19 19:26:18 +00:00
J. Nick Koston
25f3b6a959 [mqtt] Reduce flash usage by optimizing ArduinoJson assignments (#11340) 2025-10-19 19:17:33 +00:00
J. Nick Koston
e993312640 [core] Fix IndexError when OTA devices cannot be resolved (#11311) 2025-10-20 08:15:47 +13:00
J. Nick Koston
85babe85e4 [sensor] Optimize sliding window filters to eliminate heap fragmentation (#11282) 2025-10-20 07:59:47 +13:00
J. Nick Koston
0266c897c9 [mdns] Use std::unique_ptr for TXT records to reduce ESP32 flash usage (#11362) 2025-10-20 07:53:00 +13:00
J. Nick Koston
bda7676e3a [bluetooth_proxy] Merge duplicate loops in get_connection_() (#11359) 2025-10-20 07:51:41 +13:00
J. Nick Koston
57e98ec3fc [wifi] Replace std::vector with std::unique_ptr for WiFi scan buffer (#11364) 2025-10-20 07:49:58 +13:00
J. Nick Koston
09b2ad071b [esp32_ble_client] Remove duplicate MAC address extraction in set_address() (#11358) 2025-10-20 07:49:13 +13:00
J. Nick Koston
fdecda3d65 [light] Use bitmask instead of std::set for color modes (#11348) 2025-10-20 07:48:14 +13:00
J. Nick Koston
a0922bc8b0 [ci] Add automated memory impact analysis for pull requests (#11242)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-10-20 07:43:38 +13:00
J. Nick Koston
f25af18655 [scheduler] Replace defer queue deque with vector to avoid 512-byte upfront allocation (#11305) 2025-10-20 07:34:34 +13:00
J. Nick Koston
5db07c2d70 [api][time] Refactor timezone update logic for cleaner code (#11327) 2025-10-20 07:31:40 +13:00
Juan Antonio Aldea
40823df7bc make types sensors_t and sensor_type_t internal to StatsdComponent. (#11345) 2025-10-19 07:47:31 -10:00
tomaszduda23
5e1019a6fa [nrf52, ble_nus] add logging over BLE (#9846) 2025-10-19 07:41:19 -10:00
tomaszduda23
f3cdbd0a05 [nrf52] fix task names in logs (#11367) 2025-10-19 07:39:48 -10:00
Keith Burzinski
ddf1b67e49 [prometheus] Update to use new climate API (#11361) 2025-10-18 22:11:44 -10:00
Keith Burzinski
b4d9fddd07 [mqtt] Update to use new climate API (#11360) 2025-10-18 22:11:10 -10:00
Keith Burzinski
25f03074ab [web_server] Update to use new climate API (#11363) 2025-10-18 22:10:07 -10:00
Keith Burzinski
590f6ff70b [api] Update to use new climate API (#11357) 2025-10-19 06:20:11 +00:00
Keith Burzinski
a33ed5e47b [thermostat] Add humidity support (#11286) 2025-10-18 17:25:53 -10:00
Spectre5
c11a9bb97f Change all temperature offsets to temperature_delta (#11347) 2025-10-18 21:13:57 -04:00
dependabot[bot]
acef2085d9 Bump aioesphomeapi from 42.1.0 to 42.2.0 (#11352)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-18 23:11:36 +00:00
dependabot[bot]
865663ce5f Bump aioesphomeapi from 42.0.0 to 42.1.0 (#11350)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-18 11:48:25 -10:00
tomaszduda23
ae010fd6f1 [dashboard] fix migration to Path (#11342)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-10-18 17:32:12 +00:00
J. Nick Koston
91a10d0e36 [total_daily_energy] Fix ID conflicts in component test configuration (#11337) 2025-10-18 08:36:30 -04:00
Juan Antonio Aldea
d5c36eaf2a [tests] Remove superfluous else-blocks from lambdas (#11322)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-10-17 21:40:54 -10:00
217 changed files with 9308 additions and 2770 deletions

View File

@@ -0,0 +1,111 @@
---
name: Memory Impact Comment (Forks)
on:
workflow_run:
workflows: ["CI"]
types: [completed]
permissions:
contents: read
pull-requests: write
actions: read
jobs:
memory-impact-comment:
name: Post memory impact comment (fork PRs only)
runs-on: ubuntu-24.04
# Only run for PRs from forks that had successful CI runs
if: >
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.head_repository.full_name != github.repository
env:
GH_TOKEN: ${{ github.token }}
steps:
- name: Get PR details
id: pr
run: |
# Get PR details by searching for PR with matching head SHA
# The workflow_run.pull_requests field is often empty for forks
# Use paginate to handle repos with many open PRs
head_sha="${{ github.event.workflow_run.head_sha }}"
pr_data=$(gh api --paginate "/repos/${{ github.repository }}/pulls" \
--jq ".[] | select(.head.sha == \"$head_sha\") | {number: .number, base_ref: .base.ref}" \
| head -n 1)
if [ -z "$pr_data" ]; then
echo "No PR found for SHA $head_sha, skipping"
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
pr_number=$(echo "$pr_data" | jq -r '.number')
base_ref=$(echo "$pr_data" | jq -r '.base_ref')
echo "pr_number=$pr_number" >> "$GITHUB_OUTPUT"
echo "base_ref=$base_ref" >> "$GITHUB_OUTPUT"
echo "Found PR #$pr_number targeting base branch: $base_ref"
- name: Check out code from base repository
if: steps.pr.outputs.skip != 'true'
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
# Always check out from the base repository (esphome/esphome), never from forks
# Use the PR's target branch to ensure we run trusted code from the main repo
repository: ${{ github.repository }}
ref: ${{ steps.pr.outputs.base_ref }}
- name: Restore Python
if: steps.pr.outputs.skip != 'true'
uses: ./.github/actions/restore-python
with:
python-version: "3.11"
cache-key: ${{ hashFiles('.cache-key') }}
- name: Download memory analysis artifacts
if: steps.pr.outputs.skip != 'true'
run: |
run_id="${{ github.event.workflow_run.id }}"
echo "Downloading artifacts from workflow run $run_id"
mkdir -p memory-analysis
# Download target analysis artifact
if gh run download --name "memory-analysis-target" --dir memory-analysis --repo "${{ github.repository }}" "$run_id"; then
echo "Downloaded memory-analysis-target artifact."
else
echo "No memory-analysis-target artifact found."
fi
# Download PR analysis artifact
if gh run download --name "memory-analysis-pr" --dir memory-analysis --repo "${{ github.repository }}" "$run_id"; then
echo "Downloaded memory-analysis-pr artifact."
else
echo "No memory-analysis-pr artifact found."
fi
- name: Check if artifacts exist
id: check
if: steps.pr.outputs.skip != 'true'
run: |
if [ -f ./memory-analysis/memory-analysis-target.json ] && [ -f ./memory-analysis/memory-analysis-pr.json ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "Memory analysis artifacts not found, skipping comment"
fi
- name: Post or update PR comment
if: steps.pr.outputs.skip != 'true' && steps.check.outputs.found == 'true'
env:
PR_NUMBER: ${{ steps.pr.outputs.pr_number }}
run: |
. venv/bin/activate
# Pass PR number and JSON file paths directly to Python script
# Let Python parse the JSON to avoid shell injection risks
# The script will validate and sanitize all inputs
python script/ci_memory_impact_comment.py \
--pr-number "$PR_NUMBER" \
--target-json ./memory-analysis/memory-analysis-target.json \
--pr-json ./memory-analysis/memory-analysis-pr.json

View File

@@ -170,11 +170,13 @@ jobs:
outputs:
integration-tests: ${{ steps.determine.outputs.integration-tests }}
clang-tidy: ${{ steps.determine.outputs.clang-tidy }}
clang-tidy-mode: ${{ steps.determine.outputs.clang-tidy-mode }}
python-linters: ${{ steps.determine.outputs.python-linters }}
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 }}
component-test-count: ${{ steps.determine.outputs.component-test-count }}
changed-cpp-file-count: ${{ steps.determine.outputs.changed-cpp-file-count }}
memory_impact: ${{ steps.determine.outputs.memory-impact }}
steps:
- name: Check out code from GitHub
@@ -200,11 +202,13 @@ jobs:
# Extract individual fields
echo "integration-tests=$(echo "$output" | jq -r '.integration_tests')" >> $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 "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
echo "component-test-count=$(echo "$output" | jq -r '.component_test_count')" >> $GITHUB_OUTPUT
echo "changed-cpp-file-count=$(echo "$output" | jq -r '.changed_cpp_file_count')" >> $GITHUB_OUTPUT
echo "memory-impact=$(echo "$output" | jq -c '.memory_impact')" >> $GITHUB_OUTPUT
integration-tests:
@@ -243,7 +247,7 @@ jobs:
. venv/bin/activate
pytest -vv --no-cov --tb=native -n auto tests/integration/
clang-tidy:
clang-tidy-single:
name: ${{ matrix.name }}
runs-on: ubuntu-24.04
needs:
@@ -261,22 +265,6 @@ jobs:
name: Run script/clang-tidy for ESP8266
options: --environment esp8266-arduino-tidy --grep USE_ESP8266
pio_cache_key: tidyesp8266
- id: clang-tidy
name: Run script/clang-tidy for ESP32 Arduino 1/4
options: --environment esp32-arduino-tidy --split-num 4 --split-at 1
pio_cache_key: tidyesp32
- id: clang-tidy
name: Run script/clang-tidy for ESP32 Arduino 2/4
options: --environment esp32-arduino-tidy --split-num 4 --split-at 2
pio_cache_key: tidyesp32
- id: clang-tidy
name: Run script/clang-tidy for ESP32 Arduino 3/4
options: --environment esp32-arduino-tidy --split-num 4 --split-at 3
pio_cache_key: tidyesp32
- id: clang-tidy
name: Run script/clang-tidy for ESP32 Arduino 4/4
options: --environment esp32-arduino-tidy --split-num 4 --split-at 4
pio_cache_key: tidyesp32
- id: clang-tidy
name: Run script/clang-tidy for ESP32 IDF
options: --environment esp32-idf-tidy --grep USE_ESP_IDF
@@ -357,6 +345,166 @@ jobs:
# yamllint disable-line rule:line-length
if: always()
clang-tidy-nosplit:
name: Run script/clang-tidy for ESP32 Arduino
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
if: needs.determine-jobs.outputs.clang-tidy-mode == 'nosplit'
env:
GH_TOKEN: ${{ github.token }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache platformio
if: github.ref == 'refs/heads/dev'
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
- name: Cache platformio
if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
- name: Register problem matchers
run: |
echo "::add-matcher::.github/workflows/matchers/gcc.json"
echo "::add-matcher::.github/workflows/matchers/clang-tidy.json"
- name: Check if full clang-tidy scan needed
id: check_full_scan
run: |
. venv/bin/activate
if python script/clang_tidy_hash.py --check; then
echo "full_scan=true" >> $GITHUB_OUTPUT
echo "reason=hash_changed" >> $GITHUB_OUTPUT
else
echo "full_scan=false" >> $GITHUB_OUTPUT
echo "reason=normal" >> $GITHUB_OUTPUT
fi
- name: Run clang-tidy
run: |
. venv/bin/activate
if [ "${{ steps.check_full_scan.outputs.full_scan }}" = "true" ]; then
echo "Running FULL clang-tidy scan (hash changed)"
script/clang-tidy --all-headers --fix --environment esp32-arduino-tidy
else
echo "Running clang-tidy on changed files only"
script/clang-tidy --all-headers --fix --changed --environment esp32-arduino-tidy
fi
env:
# Also cache libdeps, store them in a ~/.platformio subfolder
PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps
- name: Suggested changes
run: script/ci-suggest-changes
if: always()
clang-tidy-split:
name: ${{ matrix.name }}
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
if: needs.determine-jobs.outputs.clang-tidy-mode == 'split'
env:
GH_TOKEN: ${{ github.token }}
strategy:
fail-fast: false
max-parallel: 1
matrix:
include:
- id: clang-tidy
name: Run script/clang-tidy for ESP32 Arduino 1/4
options: --environment esp32-arduino-tidy --split-num 4 --split-at 1
- id: clang-tidy
name: Run script/clang-tidy for ESP32 Arduino 2/4
options: --environment esp32-arduino-tidy --split-num 4 --split-at 2
- id: clang-tidy
name: Run script/clang-tidy for ESP32 Arduino 3/4
options: --environment esp32-arduino-tidy --split-num 4 --split-at 3
- id: clang-tidy
name: Run script/clang-tidy for ESP32 Arduino 4/4
options: --environment esp32-arduino-tidy --split-num 4 --split-at 4
steps:
- name: Check out code from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache platformio
if: github.ref == 'refs/heads/dev'
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
- name: Cache platformio
if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
- name: Register problem matchers
run: |
echo "::add-matcher::.github/workflows/matchers/gcc.json"
echo "::add-matcher::.github/workflows/matchers/clang-tidy.json"
- name: Check if full clang-tidy scan needed
id: check_full_scan
run: |
. venv/bin/activate
if python script/clang_tidy_hash.py --check; then
echo "full_scan=true" >> $GITHUB_OUTPUT
echo "reason=hash_changed" >> $GITHUB_OUTPUT
else
echo "full_scan=false" >> $GITHUB_OUTPUT
echo "reason=normal" >> $GITHUB_OUTPUT
fi
- name: Run clang-tidy
run: |
. venv/bin/activate
if [ "${{ steps.check_full_scan.outputs.full_scan }}" = "true" ]; then
echo "Running FULL clang-tidy scan (hash changed)"
script/clang-tidy --all-headers --fix ${{ matrix.options }}
else
echo "Running clang-tidy on changed files only"
script/clang-tidy --all-headers --fix --changed ${{ matrix.options }}
fi
env:
# Also cache libdeps, store them in a ~/.platformio subfolder
PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps
- name: Suggested changes
run: script/ci-suggest-changes
if: always()
test-build-components-splitter:
name: Split components for intelligent grouping (40 weighted per batch)
runs-on: ubuntu-24.04
@@ -641,6 +789,12 @@ jobs:
--output-env \
--output-json memory-analysis-target.json
# Add metadata to JSON before caching
python script/ci_add_metadata_to_json.py \
--json-file memory-analysis-target.json \
--components "$components" \
--platform "$platform"
- name: Save memory analysis to cache
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success'
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
@@ -720,6 +874,13 @@ jobs:
python script/ci_memory_impact_extract.py \
--output-env \
--output-json memory-analysis-pr.json
# Add metadata to JSON (components and platform are in shell variables above)
python script/ci_add_metadata_to_json.py \
--json-file memory-analysis-pr.json \
--components "$components" \
--platform "$platform"
- name: Upload memory analysis JSON
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
@@ -736,10 +897,12 @@ jobs:
- determine-jobs
- memory-impact-target-branch
- memory-impact-pr-branch
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.memory_impact).should_run == 'true' && needs.memory-impact-target-branch.outputs.skip != 'true'
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && fromJSON(needs.determine-jobs.outputs.memory_impact).should_run == 'true' && needs.memory-impact-target-branch.outputs.skip != 'true'
permissions:
contents: read
pull-requests: write
env:
GH_TOKEN: ${{ github.token }}
steps:
- name: Check out code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
@@ -749,65 +912,29 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Download target analysis JSON
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
name: memory-analysis-target
path: ./memory-analysis
continue-on-error: true
- name: Download PR analysis JSON
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
name: memory-analysis-pr
path: ./memory-analysis
continue-on-error: true
- name: Post or update PR comment
env:
GH_TOKEN: ${{ github.token }}
COMPONENTS: ${{ toJSON(fromJSON(needs.determine-jobs.outputs.memory_impact).components) }}
PLATFORM: ${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}
TARGET_RAM: ${{ needs.memory-impact-target-branch.outputs.ram_usage }}
TARGET_FLASH: ${{ needs.memory-impact-target-branch.outputs.flash_usage }}
PR_RAM: ${{ needs.memory-impact-pr-branch.outputs.ram_usage }}
PR_FLASH: ${{ needs.memory-impact-pr-branch.outputs.flash_usage }}
TARGET_CACHE_HIT: ${{ needs.memory-impact-target-branch.outputs.cache_hit }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
. venv/bin/activate
# Check if analysis JSON files exist
target_json_arg=""
pr_json_arg=""
if [ -f ./memory-analysis/memory-analysis-target.json ]; then
echo "Found target analysis JSON"
target_json_arg="--target-json ./memory-analysis/memory-analysis-target.json"
else
echo "No target analysis JSON found"
fi
if [ -f ./memory-analysis/memory-analysis-pr.json ]; then
echo "Found PR analysis JSON"
pr_json_arg="--pr-json ./memory-analysis/memory-analysis-pr.json"
else
echo "No PR analysis JSON found"
fi
# Add cache flag if target was cached
cache_flag=""
if [ "$TARGET_CACHE_HIT" == "true" ]; then
cache_flag="--target-cache-hit"
fi
# Pass JSON file paths directly to Python script
# All data is extracted from JSON files for security
python script/ci_memory_impact_comment.py \
--pr-number "${{ github.event.pull_request.number }}" \
--components "$COMPONENTS" \
--platform "$PLATFORM" \
--target-ram "$TARGET_RAM" \
--target-flash "$TARGET_FLASH" \
--pr-ram "$PR_RAM" \
--pr-flash "$PR_FLASH" \
$target_json_arg \
$pr_json_arg \
$cache_flag
--pr-number "$PR_NUMBER" \
--target-json ./memory-analysis/memory-analysis-target.json \
--pr-json ./memory-analysis/memory-analysis-pr.json
ci-status:
name: CI Status
@@ -818,7 +945,9 @@ jobs:
- pylint
- pytest
- integration-tests
- clang-tidy
- clang-tidy-single
- clang-tidy-nosplit
- clang-tidy-split
- determine-jobs
- test-build-components-splitter
- test-build-components-split

View File

@@ -70,6 +70,7 @@ esphome/components/bl0939/* @ziceva
esphome/components/bl0940/* @dan-s-github @tobias-
esphome/components/bl0942/* @dbuezas @dwmw2
esphome/components/ble_client/* @buxtronix @clydebarrow
esphome/components/ble_nus/* @tomaszduda23
esphome/components/bluetooth_proxy/* @bdraco @jesserockz
esphome/components/bme280_base/* @esphome/core
esphome/components/bme280_spi/* @apbodrov
@@ -160,6 +161,7 @@ esphome/components/esp32_rmt_led_strip/* @jesserockz
esphome/components/esp8266/* @esphome/core
esphome/components/esp_ldo/* @clydebarrow
esphome/components/espnow/* @jesserockz
esphome/components/espnow/packet_transport/* @EasilyBoredEngineer
esphome/components/ethernet_info/* @gtjadsonsantos
esphome/components/event/* @nohat
esphome/components/exposure_notifications/* @OttoWinter

View File

@@ -62,6 +62,40 @@ from esphome.util import (
_LOGGER = logging.getLogger(__name__)
# Special non-component keys that appear in configs
_NON_COMPONENT_KEYS = frozenset(
{
CONF_ESPHOME,
"substitutions",
"packages",
"globals",
"external_components",
"<<",
}
)
def detect_external_components(config: ConfigType) -> set[str]:
"""Detect external/custom components in the configuration.
External components are those that appear in the config but are not
part of ESPHome's built-in components and are not special config keys.
Args:
config: The ESPHome configuration dictionary
Returns:
A set of external component names
"""
from esphome.analyze_memory.helpers import get_esphome_components
builtin_components = get_esphome_components()
return {
key
for key in config
if key not in builtin_components and key not in _NON_COMPONENT_KEYS
}
class ArgsProtocol(Protocol):
device: list[str] | None
@@ -185,7 +219,9 @@ def choose_upload_log_host(
else:
resolved.append(device)
if not resolved:
_LOGGER.error("All specified devices: %s could not be resolved.", defaults)
raise EsphomeError(
f"All specified devices {defaults} could not be resolved. Is the device connected to the network?"
)
return resolved
# No devices specified, show interactive chooser
@@ -890,6 +926,54 @@ def command_idedata(args: ArgsProtocol, config: ConfigType) -> int:
return 0
def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
"""Analyze memory usage by component.
This command compiles the configuration and performs memory analysis.
Compilation is fast if sources haven't changed (just relinking).
"""
from esphome import platformio_api
from esphome.analyze_memory.cli import MemoryAnalyzerCLI
# Always compile to ensure fresh data (fast if no changes - just relinks)
exit_code = write_cpp(config)
if exit_code != 0:
return exit_code
exit_code = compile_program(args, config)
if exit_code != 0:
return exit_code
_LOGGER.info("Successfully compiled program.")
# Get idedata for analysis
idedata = platformio_api.get_idedata(config)
if idedata is None:
_LOGGER.error("Failed to get IDE data for memory analysis")
return 1
firmware_elf = Path(idedata.firmware_elf_path)
# Extract external components from config
external_components = detect_external_components(config)
_LOGGER.debug("Detected external components: %s", external_components)
# Perform memory analysis
_LOGGER.info("Analyzing memory usage...")
analyzer = MemoryAnalyzerCLI(
str(firmware_elf),
idedata.objdump_path,
idedata.readelf_path,
external_components,
)
analyzer.analyze()
# Generate and display report
report = analyzer.generate_report()
print()
print(report)
return 0
def command_rename(args: ArgsProtocol, config: ConfigType) -> int | None:
new_name = args.name
for c in new_name:
@@ -1005,6 +1089,7 @@ POST_CONFIG_ACTIONS = {
"idedata": command_idedata,
"rename": command_rename,
"discover": command_discover,
"analyze-memory": command_analyze_memory,
}
SIMPLE_CONFIG_ACTIONS = [
@@ -1290,6 +1375,14 @@ def parse_args(argv):
)
parser_rename.add_argument("name", help="The new name for the device.", type=str)
parser_analyze_memory = subparsers.add_parser(
"analyze-memory",
help="Analyze memory usage by component.",
)
parser_analyze_memory.add_argument(
"configuration", help="Your YAML configuration file(s).", nargs="+"
)
# Keep backward compatibility with the old command line format of
# esphome <config> <command>.
#

View File

@@ -231,9 +231,22 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
api_component = (name, mem)
break
# Combine all components to analyze: top ESPHome + all external + API if not already included
components_to_analyze = list(top_esphome_components) + list(
top_external_components
# Also include wifi_stack and other important system components if they exist
system_components_to_include = [
# Empty list - we've finished debugging symbol categorization
# Add component names here if you need to debug their symbols
]
system_components = [
(name, mem)
for name, mem in components
if name in system_components_to_include
]
# Combine all components to analyze: top ESPHome + all external + API if not already included + system components
components_to_analyze = (
list(top_esphome_components)
+ list(top_external_components)
+ system_components
)
if api_component and api_component not in components_to_analyze:
components_to_analyze.append(api_component)

View File

@@ -127,40 +127,39 @@ SYMBOL_PATTERNS = {
"tryget_socket_unconn",
"cs_create_ctrl_sock",
"netbuf_alloc",
"tcp_", # TCP protocol functions
"udp_", # UDP protocol functions
"lwip_", # LwIP stack functions
"eagle_lwip", # ESP-specific LwIP functions
"new_linkoutput", # Link output function
"acd_", # Address Conflict Detection (ACD)
"eth_", # Ethernet functions
"mac_enable_bb", # MAC baseband enable
"reassemble_and_dispatch", # Packet reassembly
],
# dhcp must come before libc to avoid "dhcp_select" matching "select" pattern
"dhcp": ["dhcp", "handle_dhcp"],
"ipv6_stack": ["nd6_", "ip6_", "mld6_", "icmp6_", "icmp6_input"],
"wifi_stack": [
"ieee80211",
"hostap",
"sta_",
"ap_",
"scan_",
"wifi_",
"wpa_",
"wps_",
"esp_wifi",
"cnx_",
"wpa3_",
"sae_",
"wDev_",
"ic_",
"mac_",
"esf_buf",
"gWpaSm",
"sm_WPA",
"eapol_",
"owe_",
"wifiLowLevelInit",
"s_do_mapping",
"gScanStruct",
"ppSearchTxframe",
"ppMapWaitTxq",
"ppFillAMPDUBar",
"ppCheckTxConnTrafficIdle",
"ppCalTkipMic",
# Order matters! More specific categories must come before general ones.
# mdns must come before bluetooth to avoid "_mdns_disable_pcb" matching "ble_" pattern
"mdns_lib": ["mdns"],
# memory_mgmt must come before wifi_stack to catch mmu_hal_* symbols
"memory_mgmt": [
"mem_",
"memory_",
"tlsf_",
"memp_",
"pbuf_",
"pbuf_alloc",
"pbuf_copy_partial_pbuf",
"esp_mmu_map",
"mmu_hal_",
"s_do_mapping", # Memory mapping function, not WiFi
"hash_map_", # Hash map data structure
"umm_assimilate", # UMM malloc assimilation
],
"bluetooth": ["bt_", "ble_", "l2c_", "gatt_", "gap_", "hci_", "BT_init"],
"wifi_bt_coex": ["coex"],
# Bluetooth categories must come BEFORE wifi_stack to avoid misclassification
# Many BLE symbols contain patterns like "ble_" that would otherwise match wifi patterns
"bluetooth_rom": ["r_ble", "r_lld", "r_llc", "r_llm"],
"bluedroid_bt": [
"bluedroid",
@@ -207,6 +206,61 @@ SYMBOL_PATTERNS = {
"copy_extra_byte_in_db",
"parse_read_local_supported_commands_response",
],
"bluetooth": [
"bt_",
"_ble_", # More specific than "ble_" to avoid matching "able_", "enable_", "disable_"
"l2c_",
"l2ble_", # L2CAP for BLE
"gatt_",
"gap_",
"hci_",
"btsnd_hcic_", # Bluetooth HCI command send functions
"BT_init",
"BT_tx_", # Bluetooth transmit functions
"esp_ble_", # Catch esp_ble_* functions
],
"bluetooth_ll": [
"llm_", # Link layer manager
"llc_", # Link layer control
"lld_", # Link layer driver
"ld_acl_", # Link layer ACL (Asynchronous Connection-Oriented)
"llcp_", # Link layer control protocol
"lmp_", # Link manager protocol
],
"wifi_bt_coex": ["coex"],
"wifi_stack": [
"ieee80211",
"hostap",
"sta_",
"wifi_ap_", # More specific than "ap_" to avoid matching "cap_", "map_"
"wifi_scan_", # More specific than "scan_" to avoid matching "_scan_" in other contexts
"wifi_",
"wpa_",
"wps_",
"esp_wifi",
"cnx_",
"wpa3_",
"sae_",
"wDev_",
"ic_mac_", # More specific than "mac_" to avoid matching emac_
"esf_buf",
"gWpaSm",
"sm_WPA",
"eapol_",
"owe_",
"wifiLowLevelInit",
# Removed "s_do_mapping" - this is memory management, not WiFi
"gScanStruct",
"ppSearchTxframe",
"ppMapWaitTxq",
"ppFillAMPDUBar",
"ppCheckTxConnTrafficIdle",
"ppCalTkipMic",
"phy_force_wifi",
"phy_unforce_wifi",
"write_wifi_chan",
"wifi_track_pll",
],
"crypto_math": [
"ecp_",
"bignum_",
@@ -231,13 +285,36 @@ SYMBOL_PATTERNS = {
"p_256_init_curve",
"shift_sub_rows",
"rshift",
"rijndaelEncrypt", # AES Rijndael encryption
],
# System and Arduino core functions must come before libc
"esp_system": [
"system_", # ESP system functions
"postmortem_", # Postmortem reporting
],
"arduino_core": [
"pinMode",
"resetPins",
"millis",
"micros",
"delay(", # More specific - Arduino delay function with parenthesis
"delayMicroseconds",
"digitalWrite",
"digitalRead",
],
"sntp": ["sntp_", "sntp_recv"],
"scheduler": [
"run_scheduled_",
"compute_scheduled_",
"event_TaskQueue",
],
"hw_crypto": ["esp_aes", "esp_sha", "esp_rsa", "esp_bignum", "esp_mpi"],
"libc": [
"printf",
"scanf",
"malloc",
"free",
"_free", # More specific than "free" to match _free, __free_r, etc. but not arbitrary "free" substring
"umm_free", # UMM malloc free function
"memcpy",
"memset",
"strcpy",
@@ -259,7 +336,7 @@ SYMBOL_PATTERNS = {
"_setenv_r",
"_tzset_unlocked_r",
"__tzcalc_limits",
"select",
"_select", # More specific than "select" to avoid matching "dhcp_select", etc.
"scalbnf",
"strtof",
"strtof_l",
@@ -316,8 +393,24 @@ SYMBOL_PATTERNS = {
"CSWTCH$",
"dst$",
"sulp",
"_strtol_l", # String to long with locale
"__cvt", # Convert
"__utoa", # Unsigned to ASCII
"__global_locale", # Global locale
"_ctype_", # Character type
"impure_data", # Impure data
],
"string_ops": [
"strcmp",
"strncmp",
"strchr",
"strstr",
"strtok",
"strdup",
"strncasecmp_P", # String compare (case insensitive, from program memory)
"strnlen_P", # String length (from program memory)
"strncat_P", # String concatenate (from program memory)
],
"string_ops": ["strcmp", "strncmp", "strchr", "strstr", "strtok", "strdup"],
"memory_alloc": ["malloc", "calloc", "realloc", "free", "_sbrk"],
"file_io": [
"fread",
@@ -338,10 +431,26 @@ SYMBOL_PATTERNS = {
"vsscanf",
],
"cpp_anonymous": ["_GLOBAL__N_", "n$"],
"cpp_runtime": ["__cxx", "_ZN", "_ZL", "_ZSt", "__gxx_personality", "_Z16"],
"exception_handling": ["__cxa_", "_Unwind_", "__gcc_personality", "uw_frame_state"],
# Plain C patterns only - C++ symbols will be categorized via DEMANGLED_PATTERNS
"nvs": ["nvs_"], # Plain C NVS functions
"ota": ["ota_", "OTA", "esp_ota", "app_desc"],
# cpp_runtime: Removed _ZN, _ZL to let DEMANGLED_PATTERNS categorize C++ symbols properly
# Only keep patterns that are truly runtime-specific and not categorizable by namespace
"cpp_runtime": ["__cxx", "_ZSt", "__gxx_personality", "_Z16"],
"exception_handling": [
"__cxa_",
"_Unwind_",
"__gcc_personality",
"uw_frame_state",
"search_object", # Search for exception handling object
"get_cie_encoding", # Get CIE encoding
"add_fdes", # Add frame description entries
"fde_unencoded_compare", # Compare FDEs
"fde_mixed_encoding_compare", # Compare mixed encoding FDEs
"frame_downheap", # Frame heap operations
"frame_heapsort", # Frame heap sorting
],
"static_init": ["_GLOBAL__sub_I_"],
"mdns_lib": ["mdns"],
"phy_radio": [
"phy_",
"rf_",
@@ -394,10 +503,47 @@ SYMBOL_PATTERNS = {
"txcal_debuge_mode",
"ant_wifitx_cfg",
"reg_init_begin",
"tx_cap_init", # TX capacitance init
"ram_set_txcap", # RAM TX capacitance setting
"tx_atten_", # TX attenuation
"txiq_", # TX I/Q calibration
"ram_cal_", # RAM calibration
"ram_rxiq_", # RAM RX I/Q
"readvdd33", # Read VDD33
"test_tout", # Test timeout
"tsen_meas", # Temperature sensor measurement
"bbpll_cal", # Baseband PLL calibration
"set_cal_", # Set calibration
"set_rfanagain_", # Set RF analog gain
"set_txdc_", # Set TX DC
"get_vdd33_", # Get VDD33
"gen_rx_gain_table", # Generate RX gain table
"ram_ana_inf_gating_en", # RAM analog interface gating enable
"tx_cont_en", # TX continuous enable
"tx_delay_cfg", # TX delay configuration
"tx_gain_table_set", # TX gain table set
"check_and_reset_hw_deadlock", # Hardware deadlock check
"s_config", # System/hardware config
"chan14_mic_cfg", # Channel 14 MIC config
],
"wifi_phy_pp": [
"pp_",
"ppT",
"ppR",
"ppP",
"ppInstall",
"ppCalTxAMPDULength",
"ppCheckTx", # Packet processor TX check
"ppCal", # Packet processor calibration
"HdlAllBuffedEb", # Handle buffered EB
],
"wifi_phy_pp": ["pp_", "ppT", "ppR", "ppP", "ppInstall", "ppCalTxAMPDULength"],
"wifi_lmac": ["lmac"],
"wifi_device": ["wdev", "wDev_"],
"wifi_device": [
"wdev",
"wDev_",
"ic_set_sta", # Set station mode
"ic_set_vif", # Set virtual interface
],
"power_mgmt": [
"pm_",
"sleep",
@@ -406,15 +552,7 @@ SYMBOL_PATTERNS = {
"deep_sleep",
"power_down",
"g_pm",
],
"memory_mgmt": [
"mem_",
"memory_",
"tlsf_",
"memp_",
"pbuf_",
"pbuf_alloc",
"pbuf_copy_partial_pbuf",
"pmc", # Power Management Controller
],
"hal_layer": ["hal_"],
"clock_mgmt": [
@@ -439,7 +577,6 @@ SYMBOL_PATTERNS = {
"error_handling": ["panic", "abort", "assert", "error_", "fault"],
"authentication": ["auth"],
"ppp_protocol": ["ppp", "ipcp_", "lcp_", "chap_", "LcpEchoCheck"],
"dhcp": ["dhcp", "handle_dhcp"],
"ethernet_phy": [
"emac_",
"eth_phy_",
@@ -618,7 +755,15 @@ SYMBOL_PATTERNS = {
"ampdu_dispatch_upto",
],
"ieee802_11": ["ieee802_11_", "ieee802_11_parse_elems"],
"rate_control": ["rssi_margin", "rcGetSched", "get_rate_fcc_index"],
"rate_control": [
"rssi_margin",
"rcGetSched",
"get_rate_fcc_index",
"rcGetRate", # Get rate
"rc_get_", # Rate control getters
"rc_set_", # Rate control setters
"rc_enable_", # Rate control enable functions
],
"nan": ["nan_dp_", "nan_dp_post_tx", "nan_dp_delete_peer"],
"channel_mgmt": ["chm_init", "chm_set_current_channel"],
"trace": ["trc_init", "trc_onAmpduOp"],
@@ -799,31 +944,18 @@ SYMBOL_PATTERNS = {
"supports_interlaced_inquiry_scan",
"supports_reading_remote_extended_features",
],
"bluetooth_ll": [
"lld_pdu_",
"ld_acl_",
"lld_stop_ind_handler",
"lld_evt_winsize_change",
"config_lld_evt_funcs_reset",
"config_lld_funcs_reset",
"config_llm_funcs_reset",
"llm_set_long_adv_data",
"lld_retry_tx_prog",
"llc_link_sup_to_ind_handler",
"config_llc_funcs_reset",
"lld_evt_rxwin_compute",
"config_btdm_funcs_reset",
"config_ea_funcs_reset",
"llc_defalut_state_tab_reset",
"config_rwip_funcs_reset",
"ke_lmp_rx_flooding_detect",
],
}
# Demangled patterns: patterns found in demangled C++ names
DEMANGLED_PATTERNS = {
"gpio_driver": ["GPIO"],
"uart_driver": ["UART"],
# mdns_lib must come before network_stack to avoid "udp" matching "_udpReadBuffer" in MDNSResponder
"mdns_lib": [
"MDNSResponder",
"MDNSImplementation",
"MDNS",
],
"network_stack": [
"lwip",
"tcp",
@@ -836,6 +968,24 @@ DEMANGLED_PATTERNS = {
"ethernet",
"ppp",
"slip",
"UdpContext", # UDP context class
"DhcpServer", # DHCP server class
],
"arduino_core": [
"String::", # Arduino String class
"Print::", # Arduino Print class
"HardwareSerial::", # Serial class
"IPAddress::", # IP address class
"EspClass::", # ESP class
"experimental::_SPI", # Experimental SPI
],
"ota": [
"UpdaterClass",
"Updater::",
],
"wifi": [
"ESP8266WiFi",
"WiFi::",
],
"wifi_stack": ["NetworkInterface"],
"nimble_bt": [
@@ -854,7 +1004,6 @@ DEMANGLED_PATTERNS = {
"rtti": ["__type_info", "__class_type_info"],
"web_server_lib": ["AsyncWebServer", "AsyncWebHandler", "WebServer"],
"async_tcp": ["AsyncClient", "AsyncServer"],
"mdns_lib": ["mdns"],
"json_lib": [
"ArduinoJson",
"JsonDocument",

View File

@@ -28,7 +28,7 @@ class Anova : public climate::Climate, public esphome::ble_client::BLEClientNode
void dump_config() override;
climate::ClimateTraits traits() override {
auto traits = climate::ClimateTraits();
traits.set_supports_current_temperature(true);
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::ClimateMode::CLIMATE_MODE_HEAT});
traits.set_visual_min_temperature(25.0);
traits.set_visual_max_temperature(100.0);

View File

@@ -155,6 +155,17 @@ def _validate_api_config(config: ConfigType) -> ConfigType:
return config
def _consume_api_sockets(config: ConfigType) -> ConfigType:
"""Register socket needs for API component."""
from esphome.components import socket
# API needs 1 listening socket + typically 3 concurrent client connections
# (not max_connections, which is the upper limit rarely reached)
sockets_needed = 1 + 3
socket.consume_sockets(sockets_needed, "api")(config)
return config
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
@@ -222,6 +233,7 @@ CONFIG_SCHEMA = cv.All(
).extend(cv.COMPONENT_SCHEMA),
cv.rename_key(CONF_SERVICES, CONF_ACTIONS),
_validate_api_config,
_consume_api_sockets,
)

View File

@@ -506,7 +506,7 @@ message ListEntitiesLightResponse {
string name = 3;
reserved 4; // Deprecated: was string unique_id
repeated ColorMode supported_color_modes = 12 [(container_pointer) = "std::set<light::ColorMode>"];
repeated ColorMode supported_color_modes = 12 [(container_pointer_no_template) = "light::ColorModeMask"];
// next four supports_* are for legacy clients, newer clients should use color modes
// Deprecated in API version 1.6
bool legacy_supports_brightness = 5 [deprecated=true];

View File

@@ -453,7 +453,6 @@ uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection *
bool is_single) {
auto *light = static_cast<light::LightState *>(entity);
LightStateResponse resp;
auto traits = light->get_traits();
auto values = light->remote_values;
auto color_mode = values.get_color_mode();
resp.state = values.is_on();
@@ -477,7 +476,8 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c
auto *light = static_cast<light::LightState *>(entity);
ListEntitiesLightResponse msg;
auto traits = light->get_traits();
msg.supported_color_modes = &traits.get_supported_color_modes_for_api_();
// Pass pointer to ColorModeMask so the iterator can encode actual ColorMode enum values
msg.supported_color_modes = &traits.get_supported_color_modes();
if (traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) ||
traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE)) {
msg.min_mireds = traits.get_min_mireds();
@@ -661,11 +661,12 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection
ListEntitiesClimateResponse msg;
auto traits = climate->get_traits();
// Flags set for backward compatibility, deprecated in 2025.11.0
msg.supports_current_temperature = traits.get_supports_current_temperature();
msg.supports_current_humidity = traits.get_supports_current_humidity();
msg.supports_two_point_target_temperature = traits.get_supports_two_point_target_temperature();
msg.supports_target_humidity = traits.get_supports_target_humidity();
msg.supports_action = traits.get_supports_action();
msg.supports_current_temperature = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
msg.supports_current_humidity = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY);
msg.supports_two_point_target_temperature = traits.has_feature_flags(
climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE);
msg.supports_target_humidity = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY);
msg.supports_action = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION);
// Current feature flags and other supported parameters
msg.feature_flags = traits.get_feature_flags();
msg.supported_modes = &traits.get_supported_modes_for_api_();
@@ -1081,13 +1082,8 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) {
homeassistant::global_homeassistant_time->set_epoch_time(value.epoch_seconds);
#ifdef USE_TIME_TIMEZONE
if (value.timezone_len > 0) {
const std::string &current_tz = homeassistant::global_homeassistant_time->get_timezone();
// Compare without allocating a string
if (current_tz.length() != value.timezone_len ||
memcmp(current_tz.c_str(), value.timezone, value.timezone_len) != 0) {
homeassistant::global_homeassistant_time->set_timezone(
std::string(reinterpret_cast<const char *>(value.timezone), value.timezone_len));
}
homeassistant::global_homeassistant_time->set_timezone(reinterpret_cast<const char *>(value.timezone),
value.timezone_len);
}
#endif
}

View File

@@ -70,4 +70,14 @@ extend google.protobuf.FieldOptions {
// init(size) before adding elements. This eliminates std::vector template overhead
// and is ideal when the exact size is known before populating the array.
optional bool fixed_vector = 50013 [default=false];
// container_pointer_no_template: Use a non-template container type for repeated fields
// Similar to container_pointer, but for containers that don't take template parameters.
// The container type is used as-is without appending element type.
// The container must have:
// - begin() and end() methods returning iterators
// - empty() method
// Example: [(container_pointer_no_template) = "light::ColorModeMask"]
// generates: const light::ColorModeMask *supported_color_modes{};
optional string container_pointer_no_template = 50014;
}

View File

@@ -790,7 +790,7 @@ class ListEntitiesLightResponse final : public InfoResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_light_response"; }
#endif
const std::set<light::ColorMode> *supported_color_modes{};
const light::ColorModeMask *supported_color_modes{};
float min_mireds{0.0f};
float max_mireds{0.0f};
std::vector<std::string> effects{};

View File

@@ -6,6 +6,9 @@ namespace bang_bang {
static const char *const TAG = "bang_bang.climate";
BangBangClimate::BangBangClimate()
: idle_trigger_(new Trigger<>()), cool_trigger_(new Trigger<>()), heat_trigger_(new Trigger<>()) {}
void BangBangClimate::setup() {
this->sensor_->add_on_state_callback([this](float state) {
this->current_temperature = state;
@@ -31,53 +34,63 @@ void BangBangClimate::setup() {
restore->to_call(this).perform();
} else {
// restore from defaults, change_away handles those for us
if (supports_cool_ && supports_heat_) {
if (this->supports_cool_ && this->supports_heat_) {
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
} else if (supports_cool_) {
} else if (this->supports_cool_) {
this->mode = climate::CLIMATE_MODE_COOL;
} else if (supports_heat_) {
} else if (this->supports_heat_) {
this->mode = climate::CLIMATE_MODE_HEAT;
}
this->change_away_(false);
}
}
void BangBangClimate::control(const climate::ClimateCall &call) {
if (call.get_mode().has_value())
if (call.get_mode().has_value()) {
this->mode = *call.get_mode();
if (call.get_target_temperature_low().has_value())
}
if (call.get_target_temperature_low().has_value()) {
this->target_temperature_low = *call.get_target_temperature_low();
if (call.get_target_temperature_high().has_value())
}
if (call.get_target_temperature_high().has_value()) {
this->target_temperature_high = *call.get_target_temperature_high();
if (call.get_preset().has_value())
}
if (call.get_preset().has_value()) {
this->change_away_(*call.get_preset() == climate::CLIMATE_PRESET_AWAY);
}
this->compute_state_();
this->publish_state();
}
climate::ClimateTraits BangBangClimate::traits() {
auto traits = climate::ClimateTraits();
traits.set_supports_current_temperature(true);
if (this->humidity_sensor_ != nullptr)
traits.set_supports_current_humidity(true);
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE |
climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE | climate::CLIMATE_SUPPORTS_ACTION);
if (this->humidity_sensor_ != nullptr) {
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY);
}
traits.set_supported_modes({
climate::CLIMATE_MODE_OFF,
});
if (supports_cool_)
if (this->supports_cool_) {
traits.add_supported_mode(climate::CLIMATE_MODE_COOL);
if (supports_heat_)
}
if (this->supports_heat_) {
traits.add_supported_mode(climate::CLIMATE_MODE_HEAT);
if (supports_cool_ && supports_heat_)
}
if (this->supports_cool_ && this->supports_heat_) {
traits.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL);
traits.set_supports_two_point_target_temperature(true);
if (supports_away_) {
}
if (this->supports_away_) {
traits.set_supported_presets({
climate::CLIMATE_PRESET_HOME,
climate::CLIMATE_PRESET_AWAY,
});
}
traits.set_supports_action(true);
return traits;
}
void BangBangClimate::compute_state_() {
if (this->mode == climate::CLIMATE_MODE_OFF) {
this->switch_to_action_(climate::CLIMATE_ACTION_OFF);
@@ -122,6 +135,7 @@ void BangBangClimate::compute_state_() {
this->switch_to_action_(target_action);
}
void BangBangClimate::switch_to_action_(climate::ClimateAction action) {
if (action == this->action) {
// already in target mode
@@ -166,6 +180,7 @@ void BangBangClimate::switch_to_action_(climate::ClimateAction action) {
this->prev_trigger_ = trig;
this->publish_state();
}
void BangBangClimate::change_away_(bool away) {
if (!away) {
this->target_temperature_low = this->normal_config_.default_temperature_low;
@@ -176,22 +191,26 @@ void BangBangClimate::change_away_(bool away) {
}
this->preset = away ? climate::CLIMATE_PRESET_AWAY : climate::CLIMATE_PRESET_HOME;
}
void BangBangClimate::set_normal_config(const BangBangClimateTargetTempConfig &normal_config) {
this->normal_config_ = normal_config;
}
void BangBangClimate::set_away_config(const BangBangClimateTargetTempConfig &away_config) {
this->supports_away_ = true;
this->away_config_ = away_config;
}
BangBangClimate::BangBangClimate()
: idle_trigger_(new Trigger<>()), cool_trigger_(new Trigger<>()), heat_trigger_(new Trigger<>()) {}
void BangBangClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; }
void BangBangClimate::set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
Trigger<> *BangBangClimate::get_idle_trigger() const { return this->idle_trigger_; }
Trigger<> *BangBangClimate::get_cool_trigger() const { return this->cool_trigger_; }
void BangBangClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
Trigger<> *BangBangClimate::get_heat_trigger() const { return this->heat_trigger_; }
void BangBangClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
void BangBangClimate::set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; }
void BangBangClimate::dump_config() {
LOG_CLIMATE("", "Bang Bang Climate", this);
ESP_LOGCONFIG(TAG,

View File

@@ -25,14 +25,15 @@ class BangBangClimate : public climate::Climate, public Component {
void set_sensor(sensor::Sensor *sensor);
void set_humidity_sensor(sensor::Sensor *humidity_sensor);
Trigger<> *get_idle_trigger() const;
Trigger<> *get_cool_trigger() const;
void set_supports_cool(bool supports_cool);
Trigger<> *get_heat_trigger() const;
void set_supports_heat(bool supports_heat);
void set_normal_config(const BangBangClimateTargetTempConfig &normal_config);
void set_away_config(const BangBangClimateTargetTempConfig &away_config);
Trigger<> *get_idle_trigger() const;
Trigger<> *get_cool_trigger() const;
Trigger<> *get_heat_trigger() const;
protected:
/// Override control to change settings of the climate device.
void control(const climate::ClimateCall &call) override;
@@ -56,16 +57,10 @@ class BangBangClimate : public climate::Climate, public Component {
*
* In idle mode, the controller is assumed to have both heating and cooling disabled.
*/
Trigger<> *idle_trigger_;
Trigger<> *idle_trigger_{nullptr};
/** The trigger to call when the controller should switch to cooling mode.
*/
Trigger<> *cool_trigger_;
/** Whether the controller supports cooling.
*
* A false value for this attribute means that the controller has no cooling action
* (for example a thermostat, where only heating and not-heating is possible).
*/
bool supports_cool_{false};
Trigger<> *cool_trigger_{nullptr};
/** The trigger to call when the controller should switch to heating mode.
*
* A null value for this attribute means that the controller has no heating action
@@ -73,15 +68,23 @@ class BangBangClimate : public climate::Climate, public Component {
* (blinds open) is possible.
*/
Trigger<> *heat_trigger_{nullptr};
bool supports_heat_{false};
/** A reference to the trigger that was previously active.
*
* This is so that the previous trigger can be stopped before enabling a new one.
*/
Trigger<> *prev_trigger_{nullptr};
BangBangClimateTargetTempConfig normal_config_{};
/** Whether the controller supports cooling/heating
*
* A false value for this attribute means that the controller has no respective action
* (for example a thermostat, where only heating and not-heating is possible).
*/
bool supports_cool_{false};
bool supports_heat_{false};
bool supports_away_{false};
BangBangClimateTargetTempConfig normal_config_{};
BangBangClimateTargetTempConfig away_config_{};
};

View File

@@ -33,8 +33,7 @@ class BedJetClimate : public climate::Climate, public BedJetClient, public Polli
climate::ClimateTraits traits() override {
auto traits = climate::ClimateTraits();
traits.set_supports_action(true);
traits.set_supports_current_temperature(true);
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_ACTION | climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
traits.set_supported_modes({
climate::CLIMATE_MODE_OFF,
climate::CLIMATE_MODE_HEAT,

View File

@@ -264,20 +264,31 @@ async def delayed_off_filter_to_code(config, filter_id):
),
)
async def autorepeat_filter_to_code(config, filter_id):
timings = []
if len(config) > 0:
timings.extend(
(conf[CONF_DELAY], conf[CONF_TIME_OFF], conf[CONF_TIME_ON])
for conf in config
)
else:
timings.append(
(
cv.time_period_str_unit(DEFAULT_DELAY).total_milliseconds,
cv.time_period_str_unit(DEFAULT_TIME_OFF).total_milliseconds,
cv.time_period_str_unit(DEFAULT_TIME_ON).total_milliseconds,
timings = [
cg.StructInitializer(
cg.MockObj("AutorepeatFilterTiming", "esphome::binary_sensor::"),
("delay", conf[CONF_DELAY]),
("time_off", conf[CONF_TIME_OFF]),
("time_on", conf[CONF_TIME_ON]),
)
)
for conf in config
]
else:
timings = [
cg.StructInitializer(
cg.MockObj("AutorepeatFilterTiming", "esphome::binary_sensor::"),
("delay", cv.time_period_str_unit(DEFAULT_DELAY).total_milliseconds),
(
"time_off",
cv.time_period_str_unit(DEFAULT_TIME_OFF).total_milliseconds,
),
(
"time_on",
cv.time_period_str_unit(DEFAULT_TIME_ON).total_milliseconds,
),
)
]
var = cg.new_Pvariable(filter_id, timings)
await cg.register_component(var, {})
return var

View File

@@ -51,7 +51,7 @@ void BinarySensor::add_filter(Filter *filter) {
last_filter->next_ = filter;
}
}
void BinarySensor::add_filters(const std::vector<Filter *> &filters) {
void BinarySensor::add_filters(std::initializer_list<Filter *> filters) {
for (Filter *filter : filters) {
this->add_filter(filter);
}

View File

@@ -4,7 +4,7 @@
#include "esphome/core/helpers.h"
#include "esphome/components/binary_sensor/filter.h"
#include <vector>
#include <initializer_list>
namespace esphome {
@@ -48,7 +48,7 @@ class BinarySensor : public StatefulEntityBase<bool>, public EntityBase_DeviceCl
void publish_initial_state(bool new_state);
void add_filter(Filter *filter);
void add_filters(const std::vector<Filter *> &filters);
void add_filters(std::initializer_list<Filter *> filters);
// ========== INTERNAL METHODS ==========
// (In most use cases you won't need these)

View File

@@ -1,7 +1,6 @@
#include "filter.h"
#include "binary_sensor.h"
#include <utility>
namespace esphome {
@@ -68,7 +67,7 @@ float DelayedOffFilter::get_setup_priority() const { return setup_priority::HARD
optional<bool> InvertFilter::new_value(bool value) { return !value; }
AutorepeatFilter::AutorepeatFilter(std::vector<AutorepeatFilterTiming> timings) : timings_(std::move(timings)) {}
AutorepeatFilter::AutorepeatFilter(std::initializer_list<AutorepeatFilterTiming> timings) : timings_(timings) {}
optional<bool> AutorepeatFilter::new_value(bool value) {
if (value) {

View File

@@ -4,8 +4,6 @@
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include <vector>
namespace esphome {
namespace binary_sensor {
@@ -82,11 +80,6 @@ class InvertFilter : public Filter {
};
struct AutorepeatFilterTiming {
AutorepeatFilterTiming(uint32_t delay, uint32_t off, uint32_t on) {
this->delay = delay;
this->time_off = off;
this->time_on = on;
}
uint32_t delay;
uint32_t time_off;
uint32_t time_on;
@@ -94,7 +87,7 @@ struct AutorepeatFilterTiming {
class AutorepeatFilter : public Filter, public Component {
public:
explicit AutorepeatFilter(std::vector<AutorepeatFilterTiming> timings);
explicit AutorepeatFilter(std::initializer_list<AutorepeatFilterTiming> timings);
optional<bool> new_value(bool value) override;
@@ -104,7 +97,7 @@ class AutorepeatFilter : public Filter, public Component {
void next_timing_();
void next_value_(bool val);
std::vector<AutorepeatFilterTiming> timings_;
FixedVector<AutorepeatFilterTiming> timings_;
uint8_t active_timing_{0};
};

View File

@@ -0,0 +1,29 @@
import esphome.codegen as cg
from esphome.components.zephyr import zephyr_add_prj_conf
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_LOGS, CONF_TYPE
AUTO_LOAD = ["zephyr_ble_server"]
CODEOWNERS = ["@tomaszduda23"]
ble_nus_ns = cg.esphome_ns.namespace("ble_nus")
BLENUS = ble_nus_ns.class_("BLENUS", cg.Component)
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(BLENUS),
cv.Optional(CONF_TYPE, default=CONF_LOGS): cv.one_of(
*[CONF_LOGS], lower=True
),
}
).extend(cv.COMPONENT_SCHEMA),
cv.only_with_framework("zephyr"),
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
zephyr_add_prj_conf("BT_NUS", True)
cg.add(var.set_expose_log(config[CONF_TYPE] == CONF_LOGS))
await cg.register_component(var, config)

View File

@@ -0,0 +1,157 @@
#ifdef USE_ZEPHYR
#include "ble_nus.h"
#include <zephyr/kernel.h>
#include <bluetooth/services/nus.h>
#include "esphome/core/log.h"
#ifdef USE_LOGGER
#include "esphome/components/logger/logger.h"
#include "esphome/core/application.h"
#endif
#include <zephyr/sys/ring_buffer.h>
namespace esphome::ble_nus {
constexpr size_t BLE_TX_BUF_SIZE = 2048;
// NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables)
BLENUS *global_ble_nus;
RING_BUF_DECLARE(global_ble_tx_ring_buf, BLE_TX_BUF_SIZE);
// NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables)
static const char *const TAG = "ble_nus";
size_t BLENUS::write_array(const uint8_t *data, size_t len) {
if (atomic_get(&this->tx_status_) == TX_DISABLED) {
return 0;
}
return ring_buf_put(&global_ble_tx_ring_buf, data, len);
}
void BLENUS::connected(bt_conn *conn, uint8_t err) {
if (err == 0) {
global_ble_nus->conn_.store(bt_conn_ref(conn));
}
}
void BLENUS::disconnected(bt_conn *conn, uint8_t reason) {
if (global_ble_nus->conn_) {
bt_conn_unref(global_ble_nus->conn_.load());
// Connection array is global static.
// Reference can be kept even if disconnected.
}
}
void BLENUS::tx_callback(bt_conn *conn) {
atomic_cas(&global_ble_nus->tx_status_, TX_BUSY, TX_ENABLED);
ESP_LOGVV(TAG, "Sent operation completed");
}
void BLENUS::send_enabled_callback(bt_nus_send_status status) {
switch (status) {
case BT_NUS_SEND_STATUS_ENABLED:
atomic_set(&global_ble_nus->tx_status_, TX_ENABLED);
#ifdef USE_LOGGER
if (global_ble_nus->expose_log_) {
App.schedule_dump_config();
}
#endif
ESP_LOGD(TAG, "NUS notification has been enabled");
break;
case BT_NUS_SEND_STATUS_DISABLED:
atomic_set(&global_ble_nus->tx_status_, TX_DISABLED);
ESP_LOGD(TAG, "NUS notification has been disabled");
break;
}
}
void BLENUS::rx_callback(bt_conn *conn, const uint8_t *const data, uint16_t len) {
ESP_LOGD(TAG, "Received %d bytes.", len);
}
void BLENUS::setup() {
bt_nus_cb callbacks = {
.received = rx_callback,
.sent = tx_callback,
.send_enabled = send_enabled_callback,
};
bt_nus_init(&callbacks);
static bt_conn_cb conn_callbacks = {
.connected = BLENUS::connected,
.disconnected = BLENUS::disconnected,
};
bt_conn_cb_register(&conn_callbacks);
global_ble_nus = this;
#ifdef USE_LOGGER
if (logger::global_logger != nullptr && this->expose_log_) {
logger::global_logger->add_on_log_callback(
[this](int level, const char *tag, const char *message, size_t message_len) {
this->write_array(reinterpret_cast<const uint8_t *>(message), message_len);
const char c = '\n';
this->write_array(reinterpret_cast<const uint8_t *>(&c), 1);
});
}
#endif
}
void BLENUS::dump_config() {
ESP_LOGCONFIG(TAG, "ble nus:");
ESP_LOGCONFIG(TAG, " log: %s", YESNO(this->expose_log_));
uint32_t mtu = 0;
bt_conn *conn = this->conn_.load();
if (conn) {
mtu = bt_nus_get_mtu(conn);
}
ESP_LOGCONFIG(TAG, " MTU: %u", mtu);
}
void BLENUS::loop() {
if (ring_buf_is_empty(&global_ble_tx_ring_buf)) {
return;
}
if (!atomic_cas(&this->tx_status_, TX_ENABLED, TX_BUSY)) {
if (atomic_get(&this->tx_status_) == TX_DISABLED) {
ring_buf_reset(&global_ble_tx_ring_buf);
}
return;
}
bt_conn *conn = this->conn_.load();
if (conn) {
conn = bt_conn_ref(conn);
}
if (nullptr == conn) {
atomic_cas(&this->tx_status_, TX_BUSY, TX_ENABLED);
return;
}
uint32_t req_len = bt_nus_get_mtu(conn);
uint8_t *buf;
uint32_t size = ring_buf_get_claim(&global_ble_tx_ring_buf, &buf, req_len);
int err, err2;
err = bt_nus_send(conn, buf, size);
err2 = ring_buf_get_finish(&global_ble_tx_ring_buf, size);
if (err2) {
// It should no happen.
ESP_LOGE(TAG, "Size %u exceeds valid bytes in the ring buffer (%d error)", size, err2);
}
if (err == 0) {
ESP_LOGVV(TAG, "Sent %d bytes", size);
} else {
ESP_LOGE(TAG, "Failed to send %d bytes (%d error)", size, err);
atomic_cas(&this->tx_status_, TX_BUSY, TX_ENABLED);
}
bt_conn_unref(conn);
}
} // namespace esphome::ble_nus
#endif

View File

@@ -0,0 +1,37 @@
#pragma once
#ifdef USE_ZEPHYR
#include "esphome/core/defines.h"
#include "esphome/core/component.h"
#include <shell/shell_bt_nus.h>
#include <atomic>
namespace esphome::ble_nus {
class BLENUS : public Component {
enum TxStatus {
TX_DISABLED,
TX_ENABLED,
TX_BUSY,
};
public:
void setup() override;
void dump_config() override;
void loop() override;
size_t write_array(const uint8_t *data, size_t len);
void set_expose_log(bool expose_log) { this->expose_log_ = expose_log; }
protected:
static void send_enabled_callback(bt_nus_send_status status);
static void tx_callback(bt_conn *conn);
static void rx_callback(bt_conn *conn, const uint8_t *data, uint16_t len);
static void connected(bt_conn *conn, uint8_t err);
static void disconnected(bt_conn *conn, uint8_t reason);
std::atomic<bt_conn *> conn_ = nullptr;
bool expose_log_ = false;
atomic_t tx_status_ = ATOMIC_INIT(TX_DISABLED);
};
} // namespace esphome::ble_nus
#endif

View File

@@ -155,16 +155,12 @@ esp32_ble_tracker::AdvertisementParserType BluetoothProxy::get_advertisement_par
BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool reserve) {
for (uint8_t i = 0; i < this->connection_count_; i++) {
auto *connection = this->connections_[i];
if (connection->get_address() == address)
uint64_t conn_addr = connection->get_address();
if (conn_addr == address)
return connection;
}
if (!reserve)
return nullptr;
for (uint8_t i = 0; i < this->connection_count_; i++) {
auto *connection = this->connections_[i];
if (connection->get_address() == 0) {
if (reserve && conn_addr == 0) {
connection->send_service_ = INIT_SENDING_SERVICES;
connection->set_address(address);
// All connections must start at INIT
@@ -175,7 +171,6 @@ BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool rese
return connection;
}
}
return nullptr;
}

View File

@@ -41,7 +41,7 @@ CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(BME680BSECComponent),
cv.Optional(CONF_TEMPERATURE_OFFSET, default=0): cv.temperature,
cv.Optional(CONF_TEMPERATURE_OFFSET, default=0): cv.temperature_delta,
cv.Optional(CONF_IAQ_MODE, default="STATIC"): cv.enum(
IAQ_MODE_OPTIONS, upper=True
),

View File

@@ -139,7 +139,7 @@ CONFIG_SCHEMA_BASE = (
cv.Optional(CONF_SUPPLY_VOLTAGE, default="3.3V"): cv.enum(
VOLTAGE_OPTIONS, upper=True
),
cv.Optional(CONF_TEMPERATURE_OFFSET, default=0): cv.temperature,
cv.Optional(CONF_TEMPERATURE_OFFSET, default=0): cv.temperature_delta,
cv.Optional(
CONF_STATE_SAVE_INTERVAL, default="6hours"
): cv.positive_time_period_minutes,

View File

@@ -6,6 +6,42 @@ namespace climate {
static const char *const TAG = "climate";
// Memory-efficient lookup tables
struct StringToUint8 {
const char *str;
const uint8_t value;
};
constexpr StringToUint8 CLIMATE_MODES_BY_STR[] = {
{"OFF", CLIMATE_MODE_OFF},
{"AUTO", CLIMATE_MODE_AUTO},
{"COOL", CLIMATE_MODE_COOL},
{"HEAT", CLIMATE_MODE_HEAT},
{"FAN_ONLY", CLIMATE_MODE_FAN_ONLY},
{"DRY", CLIMATE_MODE_DRY},
{"HEAT_COOL", CLIMATE_MODE_HEAT_COOL},
};
constexpr StringToUint8 CLIMATE_FAN_MODES_BY_STR[] = {
{"ON", CLIMATE_FAN_ON}, {"OFF", CLIMATE_FAN_OFF}, {"AUTO", CLIMATE_FAN_AUTO},
{"LOW", CLIMATE_FAN_LOW}, {"MEDIUM", CLIMATE_FAN_MEDIUM}, {"HIGH", CLIMATE_FAN_HIGH},
{"MIDDLE", CLIMATE_FAN_MIDDLE}, {"FOCUS", CLIMATE_FAN_FOCUS}, {"DIFFUSE", CLIMATE_FAN_DIFFUSE},
{"QUIET", CLIMATE_FAN_QUIET},
};
constexpr StringToUint8 CLIMATE_PRESETS_BY_STR[] = {
{"ECO", CLIMATE_PRESET_ECO}, {"AWAY", CLIMATE_PRESET_AWAY}, {"BOOST", CLIMATE_PRESET_BOOST},
{"COMFORT", CLIMATE_PRESET_COMFORT}, {"HOME", CLIMATE_PRESET_HOME}, {"SLEEP", CLIMATE_PRESET_SLEEP},
{"ACTIVITY", CLIMATE_PRESET_ACTIVITY}, {"NONE", CLIMATE_PRESET_NONE},
};
constexpr StringToUint8 CLIMATE_SWING_MODES_BY_STR[] = {
{"OFF", CLIMATE_SWING_OFF},
{"BOTH", CLIMATE_SWING_BOTH},
{"VERTICAL", CLIMATE_SWING_VERTICAL},
{"HORIZONTAL", CLIMATE_SWING_HORIZONTAL},
};
void ClimateCall::perform() {
this->parent_->control_callback_.call(*this);
ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str());
@@ -50,47 +86,46 @@ void ClimateCall::perform() {
}
this->parent_->control(*this);
}
void ClimateCall::validate_() {
auto traits = this->parent_->get_traits();
if (this->mode_.has_value()) {
auto mode = *this->mode_;
if (!traits.supports_mode(mode)) {
ESP_LOGW(TAG, " Mode %s is not supported by this device!", LOG_STR_ARG(climate_mode_to_string(mode)));
ESP_LOGW(TAG, " Mode %s not supported", LOG_STR_ARG(climate_mode_to_string(mode)));
this->mode_.reset();
}
}
if (this->custom_fan_mode_.has_value()) {
auto custom_fan_mode = *this->custom_fan_mode_;
if (!traits.supports_custom_fan_mode(custom_fan_mode)) {
ESP_LOGW(TAG, " Fan Mode %s is not supported by this device!", custom_fan_mode.c_str());
ESP_LOGW(TAG, " Fan Mode %s not supported", custom_fan_mode.c_str());
this->custom_fan_mode_.reset();
}
} else if (this->fan_mode_.has_value()) {
auto fan_mode = *this->fan_mode_;
if (!traits.supports_fan_mode(fan_mode)) {
ESP_LOGW(TAG, " Fan Mode %s is not supported by this device!",
LOG_STR_ARG(climate_fan_mode_to_string(fan_mode)));
ESP_LOGW(TAG, " Fan Mode %s not supported", LOG_STR_ARG(climate_fan_mode_to_string(fan_mode)));
this->fan_mode_.reset();
}
}
if (this->custom_preset_.has_value()) {
auto custom_preset = *this->custom_preset_;
if (!traits.supports_custom_preset(custom_preset)) {
ESP_LOGW(TAG, " Preset %s is not supported by this device!", custom_preset.c_str());
ESP_LOGW(TAG, " Preset %s not supported", custom_preset.c_str());
this->custom_preset_.reset();
}
} else if (this->preset_.has_value()) {
auto preset = *this->preset_;
if (!traits.supports_preset(preset)) {
ESP_LOGW(TAG, " Preset %s is not supported by this device!", LOG_STR_ARG(climate_preset_to_string(preset)));
ESP_LOGW(TAG, " Preset %s not supported", LOG_STR_ARG(climate_preset_to_string(preset)));
this->preset_.reset();
}
}
if (this->swing_mode_.has_value()) {
auto swing_mode = *this->swing_mode_;
if (!traits.supports_swing_mode(swing_mode)) {
ESP_LOGW(TAG, " Swing Mode %s is not supported by this device!",
LOG_STR_ARG(climate_swing_mode_to_string(swing_mode)));
ESP_LOGW(TAG, " Swing Mode %s not supported", LOG_STR_ARG(climate_swing_mode_to_string(swing_mode)));
this->swing_mode_.reset();
}
}
@@ -99,159 +134,127 @@ void ClimateCall::validate_() {
if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
ESP_LOGW(TAG, " Cannot set target temperature for climate device "
"with two-point target temperature!");
"with two-point target temperature");
this->target_temperature_.reset();
} else if (std::isnan(target)) {
ESP_LOGW(TAG, " Target temperature must not be NAN!");
ESP_LOGW(TAG, " Target temperature must not be NAN");
this->target_temperature_.reset();
}
}
if (this->target_temperature_low_.has_value() || this->target_temperature_high_.has_value()) {
if (!traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
ESP_LOGW(TAG, " Cannot set low/high target temperature for this device!");
ESP_LOGW(TAG, " Cannot set low/high target temperature");
this->target_temperature_low_.reset();
this->target_temperature_high_.reset();
}
}
if (this->target_temperature_low_.has_value() && std::isnan(*this->target_temperature_low_)) {
ESP_LOGW(TAG, " Target temperature low must not be NAN!");
ESP_LOGW(TAG, " Target temperature low must not be NAN");
this->target_temperature_low_.reset();
}
if (this->target_temperature_high_.has_value() && std::isnan(*this->target_temperature_high_)) {
ESP_LOGW(TAG, " Target temperature low must not be NAN!");
ESP_LOGW(TAG, " Target temperature high must not be NAN");
this->target_temperature_high_.reset();
}
if (this->target_temperature_low_.has_value() && this->target_temperature_high_.has_value()) {
float low = *this->target_temperature_low_;
float high = *this->target_temperature_high_;
if (low > high) {
ESP_LOGW(TAG, " Target temperature low %.2f must be smaller than target temperature high %.2f!", low, high);
ESP_LOGW(TAG, " Target temperature low %.2f must be less than target temperature high %.2f", low, high);
this->target_temperature_low_.reset();
this->target_temperature_high_.reset();
}
}
}
ClimateCall &ClimateCall::set_mode(ClimateMode mode) {
this->mode_ = mode;
return *this;
}
ClimateCall &ClimateCall::set_mode(const std::string &mode) {
if (str_equals_case_insensitive(mode, "OFF")) {
this->set_mode(CLIMATE_MODE_OFF);
} else if (str_equals_case_insensitive(mode, "AUTO")) {
this->set_mode(CLIMATE_MODE_AUTO);
} else if (str_equals_case_insensitive(mode, "COOL")) {
this->set_mode(CLIMATE_MODE_COOL);
} else if (str_equals_case_insensitive(mode, "HEAT")) {
this->set_mode(CLIMATE_MODE_HEAT);
} else if (str_equals_case_insensitive(mode, "FAN_ONLY")) {
this->set_mode(CLIMATE_MODE_FAN_ONLY);
} else if (str_equals_case_insensitive(mode, "DRY")) {
this->set_mode(CLIMATE_MODE_DRY);
} else if (str_equals_case_insensitive(mode, "HEAT_COOL")) {
this->set_mode(CLIMATE_MODE_HEAT_COOL);
} else {
ESP_LOGW(TAG, "'%s' - Unrecognized mode %s", this->parent_->get_name().c_str(), mode.c_str());
for (const auto &mode_entry : CLIMATE_MODES_BY_STR) {
if (str_equals_case_insensitive(mode, mode_entry.str)) {
this->set_mode(static_cast<ClimateMode>(mode_entry.value));
return *this;
}
}
ESP_LOGW(TAG, "'%s' - Unrecognized mode %s", this->parent_->get_name().c_str(), mode.c_str());
return *this;
}
ClimateCall &ClimateCall::set_fan_mode(ClimateFanMode fan_mode) {
this->fan_mode_ = fan_mode;
this->custom_fan_mode_.reset();
return *this;
}
ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) {
if (str_equals_case_insensitive(fan_mode, "ON")) {
this->set_fan_mode(CLIMATE_FAN_ON);
} else if (str_equals_case_insensitive(fan_mode, "OFF")) {
this->set_fan_mode(CLIMATE_FAN_OFF);
} else if (str_equals_case_insensitive(fan_mode, "AUTO")) {
this->set_fan_mode(CLIMATE_FAN_AUTO);
} else if (str_equals_case_insensitive(fan_mode, "LOW")) {
this->set_fan_mode(CLIMATE_FAN_LOW);
} else if (str_equals_case_insensitive(fan_mode, "MEDIUM")) {
this->set_fan_mode(CLIMATE_FAN_MEDIUM);
} else if (str_equals_case_insensitive(fan_mode, "HIGH")) {
this->set_fan_mode(CLIMATE_FAN_HIGH);
} else if (str_equals_case_insensitive(fan_mode, "MIDDLE")) {
this->set_fan_mode(CLIMATE_FAN_MIDDLE);
} else if (str_equals_case_insensitive(fan_mode, "FOCUS")) {
this->set_fan_mode(CLIMATE_FAN_FOCUS);
} else if (str_equals_case_insensitive(fan_mode, "DIFFUSE")) {
this->set_fan_mode(CLIMATE_FAN_DIFFUSE);
} else if (str_equals_case_insensitive(fan_mode, "QUIET")) {
this->set_fan_mode(CLIMATE_FAN_QUIET);
} else {
if (this->parent_->get_traits().supports_custom_fan_mode(fan_mode)) {
this->custom_fan_mode_ = fan_mode;
this->fan_mode_.reset();
} else {
ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), fan_mode.c_str());
for (const auto &mode_entry : CLIMATE_FAN_MODES_BY_STR) {
if (str_equals_case_insensitive(fan_mode, mode_entry.str)) {
this->set_fan_mode(static_cast<ClimateFanMode>(mode_entry.value));
return *this;
}
}
if (this->parent_->get_traits().supports_custom_fan_mode(fan_mode)) {
this->custom_fan_mode_ = fan_mode;
this->fan_mode_.reset();
} else {
ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), fan_mode.c_str());
}
return *this;
}
ClimateCall &ClimateCall::set_fan_mode(optional<std::string> fan_mode) {
if (fan_mode.has_value()) {
this->set_fan_mode(fan_mode.value());
}
return *this;
}
ClimateCall &ClimateCall::set_preset(ClimatePreset preset) {
this->preset_ = preset;
this->custom_preset_.reset();
return *this;
}
ClimateCall &ClimateCall::set_preset(const std::string &preset) {
if (str_equals_case_insensitive(preset, "ECO")) {
this->set_preset(CLIMATE_PRESET_ECO);
} else if (str_equals_case_insensitive(preset, "AWAY")) {
this->set_preset(CLIMATE_PRESET_AWAY);
} else if (str_equals_case_insensitive(preset, "BOOST")) {
this->set_preset(CLIMATE_PRESET_BOOST);
} else if (str_equals_case_insensitive(preset, "COMFORT")) {
this->set_preset(CLIMATE_PRESET_COMFORT);
} else if (str_equals_case_insensitive(preset, "HOME")) {
this->set_preset(CLIMATE_PRESET_HOME);
} else if (str_equals_case_insensitive(preset, "SLEEP")) {
this->set_preset(CLIMATE_PRESET_SLEEP);
} else if (str_equals_case_insensitive(preset, "ACTIVITY")) {
this->set_preset(CLIMATE_PRESET_ACTIVITY);
} else if (str_equals_case_insensitive(preset, "NONE")) {
this->set_preset(CLIMATE_PRESET_NONE);
} else {
if (this->parent_->get_traits().supports_custom_preset(preset)) {
this->custom_preset_ = preset;
this->preset_.reset();
} else {
ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), preset.c_str());
for (const auto &preset_entry : CLIMATE_PRESETS_BY_STR) {
if (str_equals_case_insensitive(preset, preset_entry.str)) {
this->set_preset(static_cast<ClimatePreset>(preset_entry.value));
return *this;
}
}
if (this->parent_->get_traits().supports_custom_preset(preset)) {
this->custom_preset_ = preset;
this->preset_.reset();
} else {
ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), preset.c_str());
}
return *this;
}
ClimateCall &ClimateCall::set_preset(optional<std::string> preset) {
if (preset.has_value()) {
this->set_preset(preset.value());
}
return *this;
}
ClimateCall &ClimateCall::set_swing_mode(ClimateSwingMode swing_mode) {
this->swing_mode_ = swing_mode;
return *this;
}
ClimateCall &ClimateCall::set_swing_mode(const std::string &swing_mode) {
if (str_equals_case_insensitive(swing_mode, "OFF")) {
this->set_swing_mode(CLIMATE_SWING_OFF);
} else if (str_equals_case_insensitive(swing_mode, "BOTH")) {
this->set_swing_mode(CLIMATE_SWING_BOTH);
} else if (str_equals_case_insensitive(swing_mode, "VERTICAL")) {
this->set_swing_mode(CLIMATE_SWING_VERTICAL);
} else if (str_equals_case_insensitive(swing_mode, "HORIZONTAL")) {
this->set_swing_mode(CLIMATE_SWING_HORIZONTAL);
} else {
ESP_LOGW(TAG, "'%s' - Unrecognized swing mode %s", this->parent_->get_name().c_str(), swing_mode.c_str());
for (const auto &mode_entry : CLIMATE_SWING_MODES_BY_STR) {
if (str_equals_case_insensitive(swing_mode, mode_entry.str)) {
this->set_swing_mode(static_cast<ClimateSwingMode>(mode_entry.value));
return *this;
}
}
ESP_LOGW(TAG, "'%s' - Unrecognized swing mode %s", this->parent_->get_name().c_str(), swing_mode.c_str());
return *this;
}
@@ -259,59 +262,71 @@ ClimateCall &ClimateCall::set_target_temperature(float target_temperature) {
this->target_temperature_ = target_temperature;
return *this;
}
ClimateCall &ClimateCall::set_target_temperature_low(float target_temperature_low) {
this->target_temperature_low_ = target_temperature_low;
return *this;
}
ClimateCall &ClimateCall::set_target_temperature_high(float target_temperature_high) {
this->target_temperature_high_ = target_temperature_high;
return *this;
}
ClimateCall &ClimateCall::set_target_humidity(float target_humidity) {
this->target_humidity_ = target_humidity;
return *this;
}
const optional<ClimateMode> &ClimateCall::get_mode() const { return this->mode_; }
const optional<float> &ClimateCall::get_target_temperature() const { return this->target_temperature_; }
const optional<float> &ClimateCall::get_target_temperature_low() const { return this->target_temperature_low_; }
const optional<float> &ClimateCall::get_target_temperature_high() const { return this->target_temperature_high_; }
const optional<float> &ClimateCall::get_target_humidity() const { return this->target_humidity_; }
const optional<ClimateMode> &ClimateCall::get_mode() const { return this->mode_; }
const optional<ClimateFanMode> &ClimateCall::get_fan_mode() const { return this->fan_mode_; }
const optional<std::string> &ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; }
const optional<ClimatePreset> &ClimateCall::get_preset() const { return this->preset_; }
const optional<std::string> &ClimateCall::get_custom_preset() const { return this->custom_preset_; }
const optional<ClimateSwingMode> &ClimateCall::get_swing_mode() const { return this->swing_mode_; }
const optional<ClimatePreset> &ClimateCall::get_preset() const { return this->preset_; }
const optional<std::string> &ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; }
const optional<std::string> &ClimateCall::get_custom_preset() const { return this->custom_preset_; }
ClimateCall &ClimateCall::set_target_temperature_high(optional<float> target_temperature_high) {
this->target_temperature_high_ = target_temperature_high;
return *this;
}
ClimateCall &ClimateCall::set_target_temperature_low(optional<float> target_temperature_low) {
this->target_temperature_low_ = target_temperature_low;
return *this;
}
ClimateCall &ClimateCall::set_target_temperature(optional<float> target_temperature) {
this->target_temperature_ = target_temperature;
return *this;
}
ClimateCall &ClimateCall::set_target_humidity(optional<float> target_humidity) {
this->target_humidity_ = target_humidity;
return *this;
}
ClimateCall &ClimateCall::set_mode(optional<ClimateMode> mode) {
this->mode_ = mode;
return *this;
}
ClimateCall &ClimateCall::set_fan_mode(optional<ClimateFanMode> fan_mode) {
this->fan_mode_ = fan_mode;
this->custom_fan_mode_.reset();
return *this;
}
ClimateCall &ClimateCall::set_preset(optional<ClimatePreset> preset) {
this->preset_ = preset;
this->custom_preset_.reset();
return *this;
}
ClimateCall &ClimateCall::set_swing_mode(optional<ClimateSwingMode> swing_mode) {
this->swing_mode_ = swing_mode;
return *this;
@@ -336,6 +351,7 @@ optional<ClimateDeviceRestoreState> Climate::restore_state_() {
return {};
return recovered;
}
void Climate::save_state_() {
#if (defined(USE_ESP_IDF) || (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0))) && \
!defined(CLANG_TIDY)
@@ -369,12 +385,14 @@ void Climate::save_state_() {
if (!traits.get_supported_custom_fan_modes().empty() && custom_fan_mode.has_value()) {
state.uses_custom_fan_mode = true;
const auto &supported = traits.get_supported_custom_fan_modes();
std::vector<std::string> vec{supported.begin(), supported.end()};
for (size_t i = 0; i < vec.size(); i++) {
if (vec[i] == custom_fan_mode) {
// std::set has consistent order (lexicographic for strings)
size_t i = 0;
for (const auto &mode : supported) {
if (mode == custom_fan_mode) {
state.custom_fan_mode = i;
break;
}
i++;
}
}
if (traits.get_supports_presets() && preset.has_value()) {
@@ -384,12 +402,14 @@ void Climate::save_state_() {
if (!traits.get_supported_custom_presets().empty() && custom_preset.has_value()) {
state.uses_custom_preset = true;
const auto &supported = traits.get_supported_custom_presets();
std::vector<std::string> vec{supported.begin(), supported.end()};
for (size_t i = 0; i < vec.size(); i++) {
if (vec[i] == custom_preset) {
// std::set has consistent order (lexicographic for strings)
size_t i = 0;
for (const auto &preset : supported) {
if (preset == custom_preset) {
state.custom_preset = i;
break;
}
i++;
}
}
if (traits.get_supports_swing_modes()) {
@@ -398,6 +418,7 @@ void Climate::save_state_() {
this->rtc_.save(&state);
}
void Climate::publish_state() {
ESP_LOGD(TAG, "'%s' - Sending state:", this->name_.c_str());
auto traits = this->get_traits();
@@ -469,16 +490,20 @@ ClimateTraits Climate::get_traits() {
void Climate::set_visual_min_temperature_override(float visual_min_temperature_override) {
this->visual_min_temperature_override_ = visual_min_temperature_override;
}
void Climate::set_visual_max_temperature_override(float visual_max_temperature_override) {
this->visual_max_temperature_override_ = visual_max_temperature_override;
}
void Climate::set_visual_temperature_step_override(float target, float current) {
this->visual_target_temperature_step_override_ = target;
this->visual_current_temperature_step_override_ = current;
}
void Climate::set_visual_min_humidity_override(float visual_min_humidity_override) {
this->visual_min_humidity_override_ = visual_min_humidity_override;
}
void Climate::set_visual_max_humidity_override(float visual_max_humidity_override) {
this->visual_max_humidity_override_ = visual_max_humidity_override;
}
@@ -510,6 +535,7 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) {
}
return call;
}
void ClimateDeviceRestoreState::apply(Climate *climate) {
auto traits = climate->get_traits();
climate->mode = this->mode;
@@ -527,22 +553,34 @@ void ClimateDeviceRestoreState::apply(Climate *climate) {
climate->fan_mode = this->fan_mode;
}
if (!traits.get_supported_custom_fan_modes().empty() && this->uses_custom_fan_mode) {
// std::set has consistent order (lexicographic for strings), so this is ok
// std::set has consistent order (lexicographic for strings)
const auto &modes = traits.get_supported_custom_fan_modes();
std::vector<std::string> modes_vec{modes.begin(), modes.end()};
if (custom_fan_mode < modes_vec.size()) {
climate->custom_fan_mode = modes_vec[this->custom_fan_mode];
if (custom_fan_mode < modes.size()) {
size_t i = 0;
for (const auto &mode : modes) {
if (i == this->custom_fan_mode) {
climate->custom_fan_mode = mode;
break;
}
i++;
}
}
}
if (traits.get_supports_presets() && !this->uses_custom_preset) {
climate->preset = this->preset;
}
if (!traits.get_supported_custom_presets().empty() && uses_custom_preset) {
// std::set has consistent order (lexicographic for strings), so this is ok
// std::set has consistent order (lexicographic for strings)
const auto &presets = traits.get_supported_custom_presets();
std::vector<std::string> presets_vec{presets.begin(), presets.end()};
if (custom_preset < presets_vec.size()) {
climate->custom_preset = presets_vec[this->custom_preset];
if (custom_preset < presets.size()) {
size_t i = 0;
for (const auto &preset : presets) {
if (i == this->custom_preset) {
climate->custom_preset = preset;
break;
}
i++;
}
}
}
if (traits.get_supports_swing_modes()) {
@@ -579,68 +617,68 @@ void Climate::dump_traits_(const char *tag) {
auto traits = this->get_traits();
ESP_LOGCONFIG(tag, "ClimateTraits:");
ESP_LOGCONFIG(tag,
" [x] Visual settings:\n"
" - Min temperature: %.1f\n"
" - Max temperature: %.1f\n"
" - Temperature step:\n"
" Target: %.1f",
" Visual settings:\n"
" - Min temperature: %.1f\n"
" - Max temperature: %.1f\n"
" - Temperature step:\n"
" Target: %.1f",
traits.get_visual_min_temperature(), traits.get_visual_max_temperature(),
traits.get_visual_target_temperature_step());
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) {
ESP_LOGCONFIG(tag, " Current: %.1f", traits.get_visual_current_temperature_step());
ESP_LOGCONFIG(tag, " Current: %.1f", traits.get_visual_current_temperature_step());
}
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY |
climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) {
ESP_LOGCONFIG(tag,
" - Min humidity: %.0f\n"
" - Max humidity: %.0f",
" - Min humidity: %.0f\n"
" - Max humidity: %.0f",
traits.get_visual_min_humidity(), traits.get_visual_max_humidity());
}
if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
ESP_LOGCONFIG(tag, " [x] Supports two-point target temperature");
ESP_LOGCONFIG(tag, " Supports two-point target temperature");
}
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) {
ESP_LOGCONFIG(tag, " [x] Supports current temperature");
ESP_LOGCONFIG(tag, " Supports current temperature");
}
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
ESP_LOGCONFIG(tag, " [x] Supports target humidity");
ESP_LOGCONFIG(tag, " Supports target humidity");
}
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) {
ESP_LOGCONFIG(tag, " [x] Supports current humidity");
ESP_LOGCONFIG(tag, " Supports current humidity");
}
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) {
ESP_LOGCONFIG(tag, " [x] Supports action");
ESP_LOGCONFIG(tag, " Supports action");
}
if (!traits.get_supported_modes().empty()) {
ESP_LOGCONFIG(tag, " [x] Supported modes:");
ESP_LOGCONFIG(tag, " Supported modes:");
for (ClimateMode m : traits.get_supported_modes())
ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_mode_to_string(m)));
ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_mode_to_string(m)));
}
if (!traits.get_supported_fan_modes().empty()) {
ESP_LOGCONFIG(tag, " [x] Supported fan modes:");
ESP_LOGCONFIG(tag, " Supported fan modes:");
for (ClimateFanMode m : traits.get_supported_fan_modes())
ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_fan_mode_to_string(m)));
ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_fan_mode_to_string(m)));
}
if (!traits.get_supported_custom_fan_modes().empty()) {
ESP_LOGCONFIG(tag, " [x] Supported custom fan modes:");
ESP_LOGCONFIG(tag, " Supported custom fan modes:");
for (const std::string &s : traits.get_supported_custom_fan_modes())
ESP_LOGCONFIG(tag, " - %s", s.c_str());
ESP_LOGCONFIG(tag, " - %s", s.c_str());
}
if (!traits.get_supported_presets().empty()) {
ESP_LOGCONFIG(tag, " [x] Supported presets:");
ESP_LOGCONFIG(tag, " Supported presets:");
for (ClimatePreset p : traits.get_supported_presets())
ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_preset_to_string(p)));
ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_preset_to_string(p)));
}
if (!traits.get_supported_custom_presets().empty()) {
ESP_LOGCONFIG(tag, " [x] Supported custom presets:");
ESP_LOGCONFIG(tag, " Supported custom presets:");
for (const std::string &s : traits.get_supported_custom_presets())
ESP_LOGCONFIG(tag, " - %s", s.c_str());
ESP_LOGCONFIG(tag, " - %s", s.c_str());
}
if (!traits.get_supported_swing_modes().empty()) {
ESP_LOGCONFIG(tag, " [x] Supported swing modes:");
ESP_LOGCONFIG(tag, " Supported swing modes:");
for (ClimateSwingMode m : traits.get_supported_swing_modes())
ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_swing_mode_to_string(m)));
ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_swing_mode_to_string(m)));
}
}

View File

@@ -93,30 +93,31 @@ class ClimateCall {
void perform();
const optional<ClimateMode> &get_mode() const;
const optional<float> &get_target_temperature() const;
const optional<float> &get_target_temperature_low() const;
const optional<float> &get_target_temperature_high() const;
const optional<float> &get_target_humidity() const;
const optional<ClimateMode> &get_mode() const;
const optional<ClimateFanMode> &get_fan_mode() const;
const optional<ClimateSwingMode> &get_swing_mode() const;
const optional<std::string> &get_custom_fan_mode() const;
const optional<ClimatePreset> &get_preset() const;
const optional<std::string> &get_custom_fan_mode() const;
const optional<std::string> &get_custom_preset() const;
protected:
void validate_();
Climate *const parent_;
optional<ClimateMode> mode_;
optional<float> target_temperature_;
optional<float> target_temperature_low_;
optional<float> target_temperature_high_;
optional<float> target_humidity_;
optional<ClimateMode> mode_;
optional<ClimateFanMode> fan_mode_;
optional<ClimateSwingMode> swing_mode_;
optional<std::string> custom_fan_mode_;
optional<ClimatePreset> preset_;
optional<std::string> custom_fan_mode_;
optional<std::string> custom_preset_;
};
@@ -169,47 +170,6 @@ class Climate : public EntityBase {
public:
Climate() {}
/// The active mode of the climate device.
ClimateMode mode{CLIMATE_MODE_OFF};
/// The active state of the climate device.
ClimateAction action{CLIMATE_ACTION_OFF};
/// The current temperature of the climate device, as reported from the integration.
float current_temperature{NAN};
/// The current humidity of the climate device, as reported from the integration.
float current_humidity{NAN};
union {
/// The target temperature of the climate device.
float target_temperature;
struct {
/// The minimum target temperature of the climate device, for climate devices with split target temperature.
float target_temperature_low{NAN};
/// The maximum target temperature of the climate device, for climate devices with split target temperature.
float target_temperature_high{NAN};
};
};
/// The target humidity of the climate device.
float target_humidity;
/// The active fan mode of the climate device.
optional<ClimateFanMode> fan_mode;
/// The active swing mode of the climate device.
ClimateSwingMode swing_mode;
/// The active custom fan mode of the climate device.
optional<std::string> custom_fan_mode;
/// The active preset of the climate device.
optional<ClimatePreset> preset;
/// The active custom preset mode of the climate device.
optional<std::string> custom_preset;
/** Add a callback for the climate device state, each time the state of the climate device is updated
* (using publish_state), this callback will be called.
*
@@ -251,6 +211,47 @@ class Climate : public EntityBase {
void set_visual_min_humidity_override(float visual_min_humidity_override);
void set_visual_max_humidity_override(float visual_max_humidity_override);
/// The current temperature of the climate device, as reported from the integration.
float current_temperature{NAN};
/// The current humidity of the climate device, as reported from the integration.
float current_humidity{NAN};
union {
/// The target temperature of the climate device.
float target_temperature;
struct {
/// The minimum target temperature of the climate device, for climate devices with split target temperature.
float target_temperature_low{NAN};
/// The maximum target temperature of the climate device, for climate devices with split target temperature.
float target_temperature_high{NAN};
};
};
/// The target humidity of the climate device.
float target_humidity;
/// The active fan mode of the climate device.
optional<ClimateFanMode> fan_mode;
/// The active preset of the climate device.
optional<ClimatePreset> preset;
/// The active custom fan mode of the climate device.
optional<std::string> custom_fan_mode;
/// The active custom preset mode of the climate device.
optional<std::string> custom_preset;
/// The active mode of the climate device.
ClimateMode mode{CLIMATE_MODE_OFF};
/// The active state of the climate device.
ClimateAction action{CLIMATE_ACTION_OFF};
/// The active swing mode of the climate device.
ClimateSwingMode swing_mode{CLIMATE_SWING_OFF};
protected:
friend ClimateCall;

View File

@@ -1,8 +1,8 @@
#pragma once
#include "esphome/core/helpers.h"
#include "climate_mode.h"
#include <set>
#include "climate_mode.h"
#include "esphome/core/helpers.h"
namespace esphome {
@@ -109,44 +109,12 @@ class ClimateTraits {
void set_supported_modes(std::set<ClimateMode> modes) { this->supported_modes_ = std::move(modes); }
void add_supported_mode(ClimateMode mode) { this->supported_modes_.insert(mode); }
ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
void set_supports_auto_mode(bool supports_auto_mode) { set_mode_support_(CLIMATE_MODE_AUTO, supports_auto_mode); }
ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
void set_supports_cool_mode(bool supports_cool_mode) { set_mode_support_(CLIMATE_MODE_COOL, supports_cool_mode); }
ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
void set_supports_heat_mode(bool supports_heat_mode) { set_mode_support_(CLIMATE_MODE_HEAT, supports_heat_mode); }
ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
void set_supports_heat_cool_mode(bool supported) { set_mode_support_(CLIMATE_MODE_HEAT_COOL, supported); }
ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
void set_supports_fan_only_mode(bool supports_fan_only_mode) {
set_mode_support_(CLIMATE_MODE_FAN_ONLY, supports_fan_only_mode);
}
ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
void set_supports_dry_mode(bool supports_dry_mode) { set_mode_support_(CLIMATE_MODE_DRY, supports_dry_mode); }
bool supports_mode(ClimateMode mode) const { return this->supported_modes_.count(mode); }
const std::set<ClimateMode> &get_supported_modes() const { return this->supported_modes_; }
void set_supported_fan_modes(std::set<ClimateFanMode> modes) { this->supported_fan_modes_ = std::move(modes); }
void add_supported_fan_mode(ClimateFanMode mode) { this->supported_fan_modes_.insert(mode); }
void add_supported_custom_fan_mode(const std::string &mode) { this->supported_custom_fan_modes_.insert(mode); }
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
void set_supports_fan_mode_on(bool supported) { set_fan_mode_support_(CLIMATE_FAN_ON, supported); }
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
void set_supports_fan_mode_off(bool supported) { set_fan_mode_support_(CLIMATE_FAN_OFF, supported); }
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
void set_supports_fan_mode_auto(bool supported) { set_fan_mode_support_(CLIMATE_FAN_AUTO, supported); }
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
void set_supports_fan_mode_low(bool supported) { set_fan_mode_support_(CLIMATE_FAN_LOW, supported); }
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
void set_supports_fan_mode_medium(bool supported) { set_fan_mode_support_(CLIMATE_FAN_MEDIUM, supported); }
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
void set_supports_fan_mode_high(bool supported) { set_fan_mode_support_(CLIMATE_FAN_HIGH, supported); }
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
void set_supports_fan_mode_middle(bool supported) { set_fan_mode_support_(CLIMATE_FAN_MIDDLE, supported); }
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
void set_supports_fan_mode_focus(bool supported) { set_fan_mode_support_(CLIMATE_FAN_FOCUS, supported); }
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
void set_supports_fan_mode_diffuse(bool supported) { set_fan_mode_support_(CLIMATE_FAN_DIFFUSE, supported); }
bool supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.count(fan_mode); }
bool get_supports_fan_modes() const {
return !this->supported_fan_modes_.empty() || !this->supported_custom_fan_modes_.empty();
@@ -178,16 +146,6 @@ class ClimateTraits {
void set_supported_swing_modes(std::set<ClimateSwingMode> modes) { this->supported_swing_modes_ = std::move(modes); }
void add_supported_swing_mode(ClimateSwingMode mode) { this->supported_swing_modes_.insert(mode); }
ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20")
void set_supports_swing_mode_off(bool supported) { set_swing_mode_support_(CLIMATE_SWING_OFF, supported); }
ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20")
void set_supports_swing_mode_both(bool supported) { set_swing_mode_support_(CLIMATE_SWING_BOTH, supported); }
ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20")
void set_supports_swing_mode_vertical(bool supported) { set_swing_mode_support_(CLIMATE_SWING_VERTICAL, supported); }
ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20")
void set_supports_swing_mode_horizontal(bool supported) {
set_swing_mode_support_(CLIMATE_SWING_HORIZONTAL, supported);
}
bool supports_swing_mode(ClimateSwingMode swing_mode) const { return this->supported_swing_modes_.count(swing_mode); }
bool get_supports_swing_modes() const { return !this->supported_swing_modes_.empty(); }
const std::set<ClimateSwingMode> &get_supported_swing_modes() const { return this->supported_swing_modes_; }

View File

@@ -8,7 +8,10 @@ static const char *const TAG = "climate_ir";
climate::ClimateTraits ClimateIR::traits() {
auto traits = climate::ClimateTraits();
traits.set_supports_current_temperature(this->sensor_ != nullptr);
if (this->sensor_ != nullptr) {
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
}
traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_HEAT_COOL});
if (this->supports_cool_)
traits.add_supported_mode(climate::CLIMATE_MODE_COOL);
@@ -19,7 +22,6 @@ climate::ClimateTraits ClimateIR::traits() {
if (this->supports_fan_only_)
traits.add_supported_mode(climate::CLIMATE_MODE_FAN_ONLY);
traits.set_supports_two_point_target_temperature(false);
traits.set_visual_min_temperature(this->minimum_temperature_);
traits.set_visual_max_temperature(this->maximum_temperature_);
traits.set_visual_temperature_step(this->temperature_step_);

View File

@@ -1,6 +1,6 @@
#include "cover.h"
#include "esphome/core/log.h"
#include <strings.h>
#include "esphome/core/log.h"
namespace esphome {
namespace cover {
@@ -144,21 +144,7 @@ CoverCall &CoverCall::set_stop(bool stop) {
bool CoverCall::get_stop() const { return this->stop_; }
CoverCall Cover::make_call() { return {this}; }
void Cover::open() {
auto call = this->make_call();
call.set_command_open();
call.perform();
}
void Cover::close() {
auto call = this->make_call();
call.set_command_close();
call.perform();
}
void Cover::stop() {
auto call = this->make_call();
call.set_command_stop();
call.perform();
}
void Cover::add_on_state_callback(std::function<void()> &&f) { this->state_callback_.add(std::move(f)); }
void Cover::publish_state(bool save) {
this->position = clamp(this->position, 0.0f, 1.0f);

View File

@@ -4,6 +4,7 @@
#include "esphome/core/entity_base.h"
#include "esphome/core/helpers.h"
#include "esphome/core/preferences.h"
#include "cover_traits.h"
namespace esphome {
@@ -125,25 +126,6 @@ class Cover : public EntityBase, public EntityBase_DeviceClass {
/// Construct a new cover call used to control the cover.
CoverCall make_call();
/** Open the cover.
*
* This is a legacy method and may be removed later, please use `.make_call()` instead.
*/
ESPDEPRECATED("open() is deprecated, use make_call().set_command_open().perform() instead.", "2021.9")
void open();
/** Close the cover.
*
* This is a legacy method and may be removed later, please use `.make_call()` instead.
*/
ESPDEPRECATED("close() is deprecated, use make_call().set_command_close().perform() instead.", "2021.9")
void close();
/** Stop the cover.
*
* This is a legacy method and may be removed later, please use `.make_call()` instead.
* As per solution from issue #2885 the call should include perform()
*/
ESPDEPRECATED("stop() is deprecated, use make_call().set_command_stop().perform() instead.", "2021.9")
void stop();
void add_on_state_callback(std::function<void()> &&f);

View File

@@ -241,9 +241,7 @@ uint8_t DaikinArcClimate::humidity_() {
climate::ClimateTraits DaikinArcClimate::traits() {
climate::ClimateTraits traits = climate_ir::ClimateIR::traits();
traits.set_supports_current_temperature(true);
traits.set_supports_current_humidity(false);
traits.set_supports_target_humidity(true);
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE | climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY);
traits.set_visual_min_humidity(38);
traits.set_visual_max_humidity(52);
return traits;

View File

@@ -82,16 +82,14 @@ class DemoClimate : public climate::Climate, public Component {
climate::ClimateTraits traits{};
switch (type_) {
case DemoClimateType::TYPE_1:
traits.set_supports_current_temperature(true);
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE | climate::CLIMATE_SUPPORTS_ACTION);
traits.set_supported_modes({
climate::CLIMATE_MODE_OFF,
climate::CLIMATE_MODE_HEAT,
});
traits.set_supports_action(true);
traits.set_visual_temperature_step(0.5);
break;
case DemoClimateType::TYPE_2:
traits.set_supports_current_temperature(false);
traits.set_supported_modes({
climate::CLIMATE_MODE_OFF,
climate::CLIMATE_MODE_HEAT,
@@ -100,7 +98,7 @@ class DemoClimate : public climate::Climate, public Component {
climate::CLIMATE_MODE_DRY,
climate::CLIMATE_MODE_FAN_ONLY,
});
traits.set_supports_action(true);
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_ACTION);
traits.set_supported_fan_modes({
climate::CLIMATE_FAN_ON,
climate::CLIMATE_FAN_OFF,
@@ -123,8 +121,8 @@ class DemoClimate : public climate::Climate, public Component {
traits.set_supported_custom_presets({"My Preset"});
break;
case DemoClimateType::TYPE_3:
traits.set_supports_current_temperature(true);
traits.set_supports_two_point_target_temperature(true);
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE |
climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE);
traits.set_supported_modes({
climate::CLIMATE_MODE_OFF,
climate::CLIMATE_MODE_COOL,

View File

@@ -103,7 +103,7 @@ bool EPaperBase::is_idle_() {
if (this->busy_pin_ == nullptr) {
return true;
}
return !this->busy_pin_->digital_read();
return this->busy_pin_->digital_read();
}
void EPaperBase::reset() {

View File

@@ -1,3 +1,4 @@
import contextlib
from dataclasses import dataclass
import itertools
import logging
@@ -102,6 +103,10 @@ COMPILER_OPTIMIZATIONS = {
"SIZE": "CONFIG_COMPILER_OPTIMIZATION_SIZE",
}
# Socket limit configuration for ESP-IDF
# ESP-IDF CONFIG_LWIP_MAX_SOCKETS has range 1-253, default 10
DEFAULT_MAX_SOCKETS = 10 # ESP-IDF default
ARDUINO_ALLOWED_VARIANTS = [
VARIANT_ESP32,
VARIANT_ESP32C3,
@@ -746,6 +751,72 @@ CONFIG_SCHEMA = cv.All(
FINAL_VALIDATE_SCHEMA = cv.Schema(final_validate)
def _configure_lwip_max_sockets(conf: dict) -> None:
"""Calculate and set CONFIG_LWIP_MAX_SOCKETS based on component needs.
Socket component tracks consumer needs via consume_sockets() called during config validation.
This function runs in to_code() after all components have registered their socket needs.
User-provided sdkconfig_options take precedence.
"""
from esphome.components.socket import KEY_SOCKET_CONSUMERS
# Check if user manually specified CONFIG_LWIP_MAX_SOCKETS
user_max_sockets = conf.get(CONF_SDKCONFIG_OPTIONS, {}).get(
"CONFIG_LWIP_MAX_SOCKETS"
)
socket_consumers: dict[str, int] = CORE.data.get(KEY_SOCKET_CONSUMERS, {})
total_sockets = sum(socket_consumers.values())
# Early return if no sockets registered and no user override
if total_sockets == 0 and user_max_sockets is None:
return
components_list = ", ".join(
f"{name}={count}" for name, count in sorted(socket_consumers.items())
)
# User specified their own value - respect it but warn if insufficient
if user_max_sockets is not None:
_LOGGER.info(
"Using user-provided CONFIG_LWIP_MAX_SOCKETS: %s",
user_max_sockets,
)
# Warn if user's value is less than what components need
if total_sockets > 0:
user_sockets_int = 0
with contextlib.suppress(ValueError, TypeError):
user_sockets_int = int(user_max_sockets)
if user_sockets_int < total_sockets:
_LOGGER.warning(
"CONFIG_LWIP_MAX_SOCKETS is set to %d but your configuration "
"needs %d sockets (registered: %s). You may experience socket "
"exhaustion errors. Consider increasing to at least %d.",
user_sockets_int,
total_sockets,
components_list,
total_sockets,
)
# User's value already added via sdkconfig_options processing
return
# Auto-calculate based on component needs
# Use at least the ESP-IDF default (10), or the total needed by components
max_sockets = max(DEFAULT_MAX_SOCKETS, total_sockets)
log_level = logging.INFO if max_sockets > DEFAULT_MAX_SOCKETS else logging.DEBUG
_LOGGER.log(
log_level,
"Setting CONFIG_LWIP_MAX_SOCKETS to %d (registered: %s)",
max_sockets,
components_list,
)
add_idf_sdkconfig_option("CONFIG_LWIP_MAX_SOCKETS", max_sockets)
async def to_code(config):
cg.add_platformio_option("board", config[CONF_BOARD])
cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE])
@@ -779,6 +850,16 @@ async def to_code(config):
Path(__file__).parent / "post_build.py.script",
)
# In testing mode, add IRAM fix script to allow linking grouped component tests
# Similar to ESP8266's approach but for ESP-IDF
if CORE.testing_mode:
cg.add_build_flag("-DESPHOME_TESTING_MODE")
add_extra_script(
"pre",
"iram_fix.py",
Path(__file__).parent / "iram_fix.py.script",
)
if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
cg.add_platformio_option("framework", "espidf")
cg.add_build_flag("-DUSE_ESP_IDF")
@@ -805,6 +886,7 @@ async def to_code(config):
add_idf_sdkconfig_option("CONFIG_AUTOSTART_ARDUINO", True)
add_idf_sdkconfig_option("CONFIG_MBEDTLS_PSK_MODES", True)
add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True)
add_idf_sdkconfig_option("CONFIG_ESP_PHY_REDUCE_TX_POWER", True)
cg.add_build_flag("-Wno-nonnull-compare")
@@ -855,6 +937,9 @@ async def to_code(config):
add_idf_sdkconfig_option("CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES", False)
if not advanced.get(CONF_ENABLE_LWIP_BRIDGE_INTERFACE, False):
add_idf_sdkconfig_option("CONFIG_LWIP_BRIDGEIF_MAX_PORTS", 0)
_configure_lwip_max_sockets(conf)
if advanced.get(CONF_EXECUTE_FROM_PSRAM, False):
add_idf_sdkconfig_option("CONFIG_SPIRAM_FETCH_INSTRUCTIONS", True)
add_idf_sdkconfig_option("CONFIG_SPIRAM_RODATA", True)

View File

@@ -6,6 +6,7 @@
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <esp_idf_version.h>
#include <esp_ota_ops.h>
#include <esp_task_wdt.h>
#include <esp_timer.h>
#include <soc/rtc.h>
@@ -52,6 +53,16 @@ void arch_init() {
disableCore1WDT();
#endif
#endif
// If the bootloader was compiled with CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE the current
// partition will get rolled back unless it is marked as valid.
esp_ota_img_states_t state;
const esp_partition_t *running = esp_ota_get_running_partition();
if (esp_ota_get_state_partition(running, &state) == ESP_OK) {
if (state == ESP_OTA_IMG_PENDING_VERIFY) {
esp_ota_mark_app_valid_cancel_rollback();
}
}
}
void IRAM_ATTR HOT arch_feed_wdt() { esp_task_wdt_reset(); }

View File

@@ -0,0 +1,71 @@
import os
import re
# pylint: disable=E0602
Import("env") # noqa
# IRAM size for testing mode (2MB - large enough to accommodate grouped tests)
TESTING_IRAM_SIZE = 0x200000
def patch_idf_linker_script(source, target, env):
"""Patch ESP-IDF linker script to increase IRAM size for testing mode."""
# Check if we're in testing mode by looking for the define
build_flags = env.get("BUILD_FLAGS", [])
testing_mode = any("-DESPHOME_TESTING_MODE" in flag for flag in build_flags)
if not testing_mode:
return
# For ESP-IDF, the linker scripts are generated in the build directory
build_dir = env.subst("$BUILD_DIR")
# The memory.ld file is directly in the build directory
memory_ld = os.path.join(build_dir, "memory.ld")
if not os.path.exists(memory_ld):
print(f"ESPHome: Warning - could not find linker script at {memory_ld}")
return
try:
with open(memory_ld, "r") as f:
content = f.read()
except OSError as e:
print(f"ESPHome: Error reading linker script: {e}")
return
# Check if this file contains iram0_0_seg
if 'iram0_0_seg' not in content:
print(f"ESPHome: Warning - iram0_0_seg not found in {memory_ld}")
return
# Look for iram0_0_seg definition and increase its length
# ESP-IDF format can be:
# iram0_0_seg (RX) : org = 0x40080000, len = 0x20000 + 0x0
# or more complex with nested parentheses:
# iram0_0_seg (RX) : org = (0x40370000 + 0x4000), len = (((0x403CB700 - (0x40378000 - 0x3FC88000)) - 0x3FC88000) + 0x8000 - 0x4000)
# We want to change len to TESTING_IRAM_SIZE for testing
# Use a more robust approach: find the line and manually parse it
lines = content.split('\n')
for i, line in enumerate(lines):
if 'iram0_0_seg' in line and 'len' in line:
# Find the position of "len = " and replace everything after it until the end of the statement
match = re.search(r'(iram0_0_seg\s*\([^)]*\)\s*:\s*org\s*=\s*(?:\([^)]+\)|0x[0-9a-fA-F]+)\s*,\s*len\s*=\s*)(.+?)(\s*)$', line)
if match:
lines[i] = f"{match.group(1)}{TESTING_IRAM_SIZE:#x}{match.group(3)}"
break
updated = '\n'.join(lines)
if updated != content:
with open(memory_ld, "w") as f:
f.write(updated)
print(f"ESPHome: Patched IRAM size to {TESTING_IRAM_SIZE:#x} in {memory_ld} for testing mode")
else:
print(f"ESPHome: Warning - could not patch iram0_0_seg in {memory_ld}")
# Hook into the build process before linking
# For ESP-IDF, we need to run this after the linker scripts are generated
env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", patch_idf_linker_script)

View File

@@ -61,12 +61,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
this->address_str_ = "";
} else {
char buf[18];
uint8_t mac[6] = {
(uint8_t) ((this->address_ >> 40) & 0xff), (uint8_t) ((this->address_ >> 32) & 0xff),
(uint8_t) ((this->address_ >> 24) & 0xff), (uint8_t) ((this->address_ >> 16) & 0xff),
(uint8_t) ((this->address_ >> 8) & 0xff), (uint8_t) ((this->address_ >> 0) & 0xff),
};
format_mac_addr_upper(mac, buf);
format_mac_addr_upper(this->remote_bda_, buf);
this->address_str_ = buf;
}
}

View File

@@ -1,6 +1,7 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_MODE, CONF_PORT
from esphome.types import ConfigType
CODEOWNERS = ["@ayufan"]
AUTO_LOAD = ["camera"]
@@ -13,13 +14,27 @@ Mode = esp32_camera_web_server_ns.enum("Mode")
MODES = {"STREAM": Mode.STREAM, "SNAPSHOT": Mode.SNAPSHOT}
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(CameraWebServer),
cv.Required(CONF_PORT): cv.port,
cv.Required(CONF_MODE): cv.enum(MODES, upper=True),
},
).extend(cv.COMPONENT_SCHEMA)
def _consume_camera_web_server_sockets(config: ConfigType) -> ConfigType:
"""Register socket needs for camera web server."""
from esphome.components import socket
# Each camera web server instance needs 1 listening socket + 2 client connections
sockets_needed = 3
socket.consume_sockets(sockets_needed, "esp32_camera_web_server")(config)
return config
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(CameraWebServer),
cv.Required(CONF_PORT): cv.port,
cv.Required(CONF_MODE): cv.enum(MODES, upper=True),
},
).extend(cv.COMPONENT_SCHEMA),
_consume_camera_web_server_sockets,
)
async def to_code(config):

View File

@@ -95,7 +95,7 @@ async def to_code(config):
if framework_ver >= cv.Version(5, 5, 0):
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.1.5")
esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.3")
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.5.11")
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.6.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")

View File

@@ -1,11 +1,11 @@
from esphome import automation
import esphome.codegen as cg
from esphome.components import binary_sensor, esp32_ble, output
from esphome.components import binary_sensor, esp32_ble, improv_base, output
from esphome.components.esp32_ble import BTLoggers
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_ON_STATE, CONF_TRIGGER_ID
AUTO_LOAD = ["esp32_ble_server"]
AUTO_LOAD = ["esp32_ble_server", "improv_base"]
CODEOWNERS = ["@jesserockz"]
DEPENDENCIES = ["wifi", "esp32"]
@@ -20,6 +20,7 @@ CONF_ON_STOP = "on_stop"
CONF_STATUS_INDICATOR = "status_indicator"
CONF_WIFI_TIMEOUT = "wifi_timeout"
improv_ns = cg.esphome_ns.namespace("improv")
Error = improv_ns.enum("Error")
State = improv_ns.enum("State")
@@ -43,55 +44,63 @@ ESP32ImprovStoppedTrigger = esp32_improv_ns.class_(
)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(ESP32ImprovComponent),
cv.Required(CONF_AUTHORIZER): cv.Any(
cv.none, cv.use_id(binary_sensor.BinarySensor)
),
cv.Optional(CONF_STATUS_INDICATOR): cv.use_id(output.BinaryOutput),
cv.Optional(
CONF_IDENTIFY_DURATION, default="10s"
): cv.positive_time_period_milliseconds,
cv.Optional(
CONF_AUTHORIZED_DURATION, default="1min"
): cv.positive_time_period_milliseconds,
cv.Optional(
CONF_WIFI_TIMEOUT, default="1min"
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_ON_PROVISIONED): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
ESP32ImprovProvisionedTrigger
),
}
),
cv.Optional(CONF_ON_PROVISIONING): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
ESP32ImprovProvisioningTrigger
),
}
),
cv.Optional(CONF_ON_START): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESP32ImprovStartTrigger),
}
),
cv.Optional(CONF_ON_STATE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESP32ImprovStateTrigger),
}
),
cv.Optional(CONF_ON_STOP): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
ESP32ImprovStoppedTrigger
),
}
),
}
).extend(cv.COMPONENT_SCHEMA)
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(ESP32ImprovComponent),
cv.Required(CONF_AUTHORIZER): cv.Any(
cv.none, cv.use_id(binary_sensor.BinarySensor)
),
cv.Optional(CONF_STATUS_INDICATOR): cv.use_id(output.BinaryOutput),
cv.Optional(
CONF_IDENTIFY_DURATION, default="10s"
): cv.positive_time_period_milliseconds,
cv.Optional(
CONF_AUTHORIZED_DURATION, default="1min"
): cv.positive_time_period_milliseconds,
cv.Optional(
CONF_WIFI_TIMEOUT, default="1min"
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_ON_PROVISIONED): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
ESP32ImprovProvisionedTrigger
),
}
),
cv.Optional(CONF_ON_PROVISIONING): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
ESP32ImprovProvisioningTrigger
),
}
),
cv.Optional(CONF_ON_START): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
ESP32ImprovStartTrigger
),
}
),
cv.Optional(CONF_ON_STATE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
ESP32ImprovStateTrigger
),
}
),
cv.Optional(CONF_ON_STOP): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
ESP32ImprovStoppedTrigger
),
}
),
}
)
.extend(improv_base.IMPROV_SCHEMA)
.extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config):
@@ -102,7 +111,8 @@ async def to_code(config):
await cg.register_component(var, config)
cg.add_define("USE_IMPROV")
cg.add_library("improv/Improv", "1.2.4")
await improv_base.setup_improv_core(var, config, "esp32_improv")
cg.add(var.set_identify_duration(config[CONF_IDENTIFY_DURATION]))
cg.add(var.set_authorized_duration(config[CONF_AUTHORIZED_DURATION]))

View File

@@ -1,10 +1,10 @@
#include "esp32_improv_component.h"
#include "esphome/components/bytebuffer/bytebuffer.h"
#include "esphome/components/esp32_ble/ble.h"
#include "esphome/components/esp32_ble_server/ble_2902.h"
#include "esphome/core/application.h"
#include "esphome/core/log.h"
#include "esphome/components/bytebuffer/bytebuffer.h"
#ifdef USE_ESP32
@@ -384,17 +384,34 @@ void ESP32ImprovComponent::check_wifi_connection_() {
this->connecting_sta_ = {};
this->cancel_timeout("wifi-connect-timeout");
std::vector<std::string> urls = {ESPHOME_MY_LINK};
// Build URL list with minimal allocations
// Maximum 3 URLs: custom next_url + ESPHOME_MY_LINK + webserver URL
std::string url_strings[3];
size_t url_count = 0;
#ifdef USE_ESP32_IMPROV_NEXT_URL
// Add next_url if configured (should be first per Improv BLE spec)
std::string next_url = this->get_formatted_next_url_();
if (!next_url.empty()) {
url_strings[url_count++] = std::move(next_url);
}
#endif
// Add default URLs for backward compatibility
url_strings[url_count++] = ESPHOME_MY_LINK;
#ifdef USE_WEBSERVER
for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) {
if (ip.is_ip4()) {
std::string webserver_url = "http://" + ip.str() + ":" + to_string(USE_WEBSERVER_PORT);
urls.push_back(webserver_url);
char url_buffer[64];
snprintf(url_buffer, sizeof(url_buffer), "http://%s:%d", ip.str().c_str(), USE_WEBSERVER_PORT);
url_strings[url_count++] = url_buffer;
break;
}
}
#endif
std::vector<uint8_t> data = improv::build_rpc_response(improv::WIFI_SETTINGS, urls);
// Pass to build_rpc_response using vector constructor from iterators to avoid extra copies
std::vector<uint8_t> data = improv::build_rpc_response(
improv::WIFI_SETTINGS, std::vector<std::string>(url_strings, url_strings + url_count));
this->send_response_(data);
} else if (this->is_active() && this->state_ != improv::STATE_PROVISIONED) {
ESP_LOGD(TAG, "WiFi provisioned externally");

View File

@@ -7,6 +7,7 @@
#include "esphome/components/esp32_ble_server/ble_characteristic.h"
#include "esphome/components/esp32_ble_server/ble_server.h"
#include "esphome/components/improv_base/improv_base.h"
#include "esphome/components/wifi/wifi_component.h"
#ifdef USE_ESP32_IMPROV_STATE_CALLBACK
@@ -32,7 +33,7 @@ namespace esp32_improv {
using namespace esp32_ble_server;
class ESP32ImprovComponent : public Component {
class ESP32ImprovComponent : public Component, public improv_base::ImprovBase {
public:
ESP32ImprovComponent();
void dump_config() override;

View File

@@ -190,7 +190,9 @@ async def to_code(config):
cg.add_define("ESPHOME_VARIANT", "ESP8266")
cg.add_define(ThreadModel.SINGLE)
cg.add_platformio_option("extra_scripts", ["pre:iram_fix.py", "post:post_build.py"])
cg.add_platformio_option(
"extra_scripts", ["pre:testing_mode.py", "post:post_build.py"]
)
conf = config[CONF_FRAMEWORK]
cg.add_platformio_option("framework", "arduino")
@@ -230,9 +232,9 @@ async def to_code(config):
# For cases where nullptrs can be handled, use nothrow: `new (std::nothrow) T;`
cg.add_build_flag("-DNEW_OOM_ABORT")
# In testing mode, fake a larger IRAM to allow linking grouped component tests
# Real ESP8266 hardware only has 32KB IRAM, but for CI testing we pretend it has 2MB
# This is done via a pre-build script that generates a custom linker script
# In testing mode, fake larger memory to allow linking grouped component tests
# Real ESP8266 hardware only has 32KB IRAM and ~80KB RAM, but for CI testing
# we pretend it has much larger memory to test that components compile together
if CORE.testing_mode:
cg.add_build_flag("-DESPHOME_TESTING_MODE")
@@ -271,8 +273,8 @@ def copy_files():
post_build_file,
CORE.relative_build_path("post_build.py"),
)
iram_fix_file = dir / "iram_fix.py.script"
testing_mode_file = dir / "testing_mode.py.script"
copy_file_if_changed(
iram_fix_file,
CORE.relative_build_path("iram_fix.py"),
testing_mode_file,
CORE.relative_build_path("testing_mode.py"),
)

View File

@@ -1,44 +0,0 @@
import os
import re
# pylint: disable=E0602
Import("env") # noqa
def patch_linker_script_after_preprocess(source, target, env):
"""Patch the local linker script after PlatformIO preprocesses it."""
# Check if we're in testing mode by looking for the define
build_flags = env.get("BUILD_FLAGS", [])
testing_mode = any("-DESPHOME_TESTING_MODE" in flag for flag in build_flags)
if not testing_mode:
return
# Get the local linker script path
build_dir = env.subst("$BUILD_DIR")
local_ld = os.path.join(build_dir, "ld", "local.eagle.app.v6.common.ld")
if not os.path.exists(local_ld):
return
# Read the linker script
with open(local_ld, "r") as f:
content = f.read()
# Replace IRAM size from 0x8000 (32KB) to 0x200000 (2MB)
# The line looks like: iram1_0_seg : org = 0x40100000, len = 0x8000
updated = re.sub(
r"(iram1_0_seg\s*:\s*org\s*=\s*0x40100000\s*,\s*len\s*=\s*)0x8000",
r"\g<1>0x200000",
content,
)
if updated != content:
with open(local_ld, "w") as f:
f.write(updated)
print("ESPHome: Patched IRAM size to 2MB for testing mode")
# Hook into the build process right before linking
# This runs after PlatformIO has already preprocessed the linker scripts
env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", patch_linker_script_after_preprocess)

View File

@@ -0,0 +1,166 @@
import os
import re
# pylint: disable=E0602
Import("env") # noqa
# Memory sizes for testing mode (allow larger builds for CI component grouping)
TESTING_IRAM_SIZE = "0x200000" # 2MB
TESTING_DRAM_SIZE = "0x200000" # 2MB
TESTING_FLASH_SIZE = "0x2000000" # 32MB
def patch_segment_size(content, segment_name, new_size, label):
"""Patch a memory segment's length in linker script.
Args:
content: Linker script content
segment_name: Name of the segment (e.g., 'iram1_0_seg')
new_size: New size as hex string (e.g., '0x200000')
label: Human-readable label for logging (e.g., 'IRAM')
Returns:
Tuple of (patched_content, was_patched)
"""
# Match: segment_name : org = 0x..., len = 0x...
pattern = rf"({segment_name}\s*:\s*org\s*=\s*0x[0-9a-fA-F]+\s*,\s*len\s*=\s*)0x[0-9a-fA-F]+"
new_content = re.sub(pattern, rf"\g<1>{new_size}", content)
return new_content, new_content != content
def apply_memory_patches(content):
"""Apply IRAM, DRAM, and Flash patches to linker script content.
Args:
content: Linker script content as string
Returns:
Patched content as string
"""
patches_applied = []
# Patch IRAM (for larger code in IRAM)
content, patched = patch_segment_size(content, "iram1_0_seg", TESTING_IRAM_SIZE, "IRAM")
if patched:
patches_applied.append("IRAM")
# Patch DRAM (for larger BSS/data sections)
content, patched = patch_segment_size(content, "dram0_0_seg", TESTING_DRAM_SIZE, "DRAM")
if patched:
patches_applied.append("DRAM")
# Patch Flash (for larger code sections)
content, patched = patch_segment_size(content, "irom0_0_seg", TESTING_FLASH_SIZE, "Flash")
if patched:
patches_applied.append("Flash")
if patches_applied:
iram_mb = int(TESTING_IRAM_SIZE, 16) // (1024 * 1024)
dram_mb = int(TESTING_DRAM_SIZE, 16) // (1024 * 1024)
flash_mb = int(TESTING_FLASH_SIZE, 16) // (1024 * 1024)
print(f" Patched memory segments: {', '.join(patches_applied)} (IRAM/DRAM: {iram_mb}MB, Flash: {flash_mb}MB)")
return content
def patch_linker_script_file(filepath, description):
"""Patch a linker script file in the build directory with enlarged memory segments.
This function modifies linker scripts in the build directory only (never SDK files).
It patches IRAM, DRAM, and Flash segments to allow larger builds in testing mode.
Args:
filepath: Path to the linker script file in the build directory
description: Human-readable description for logging
Returns:
True if the file was patched, False if already patched or not found
"""
if not os.path.exists(filepath):
print(f"ESPHome: {description} not found at {filepath}")
return False
print(f"ESPHome: Patching {description}...")
with open(filepath, "r") as f:
content = f.read()
patched_content = apply_memory_patches(content)
if patched_content != content:
with open(filepath, "w") as f:
f.write(patched_content)
print(f"ESPHome: Successfully patched {description}")
return True
else:
print(f"ESPHome: {description} already patched or no changes needed")
return False
def patch_local_linker_script(source, target, env):
"""Patch the local.eagle.app.v6.common.ld in build directory.
This patches the preprocessed linker script that PlatformIO creates in the build
directory, enlarging IRAM, DRAM, and Flash segments for testing mode.
Args:
source: SCons source nodes
target: SCons target nodes
env: SCons environment
"""
# Check if we're in testing mode
build_flags = env.get("BUILD_FLAGS", [])
testing_mode = any("-DESPHOME_TESTING_MODE" in flag for flag in build_flags)
if not testing_mode:
return
# Patch the local linker script if it exists
build_dir = env.subst("$BUILD_DIR")
ld_dir = os.path.join(build_dir, "ld")
if os.path.exists(ld_dir):
local_ld = os.path.join(ld_dir, "local.eagle.app.v6.common.ld")
if os.path.exists(local_ld):
patch_linker_script_file(local_ld, "local.eagle.app.v6.common.ld")
# Check if we're in testing mode
build_flags = env.get("BUILD_FLAGS", [])
testing_mode = any("-DESPHOME_TESTING_MODE" in flag for flag in build_flags)
if testing_mode:
# Create a custom linker script in the build directory with patched memory limits
# This allows larger IRAM/DRAM/Flash for CI component grouping tests
build_dir = env.subst("$BUILD_DIR")
ldscript = env.GetProjectOption("board_build.ldscript", "")
assert ldscript, "No linker script configured in board_build.ldscript"
framework_dir = env.PioPlatform().get_package_dir("framework-arduinoespressif8266")
assert framework_dir is not None, "Could not find framework-arduinoespressif8266 package"
# Read the original SDK linker script (read-only, SDK is never modified)
sdk_ld = os.path.join(framework_dir, "tools", "sdk", "ld", ldscript)
# Create a custom version in the build directory (isolated, temporary)
custom_ld = os.path.join(build_dir, f"testing_{ldscript}")
if os.path.exists(sdk_ld) and not os.path.exists(custom_ld):
# Read the SDK linker script
with open(sdk_ld, "r") as f:
content = f.read()
# Apply memory patches (IRAM: 2MB, DRAM: 2MB, Flash: 32MB)
patched_content = apply_memory_patches(content)
# Write the patched linker script to the build directory
with open(custom_ld, "w") as f:
f.write(patched_content)
print(f"ESPHome: Created custom linker script: {custom_ld}")
# Tell the linker to use our custom script from the build directory
assert os.path.exists(custom_ld), f"Custom linker script not found: {custom_ld}"
env.Replace(LDSCRIPT_PATH=custom_ld)
print(f"ESPHome: Using custom linker script with patched memory limits")
# Also patch local.eagle.app.v6.common.ld after PlatformIO creates it
env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", patch_local_linker_script)

View File

@@ -103,7 +103,16 @@ def ota_esphome_final_validate(config):
)
CONFIG_SCHEMA = (
def _consume_ota_sockets(config: ConfigType) -> ConfigType:
"""Register socket needs for OTA component."""
from esphome.components import socket
# OTA needs 1 listening socket (client connections are temporary during updates)
socket.consume_sockets(1, "ota")(config)
return config
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(ESPHomeOTAComponent),
@@ -130,7 +139,8 @@ CONFIG_SCHEMA = (
}
)
.extend(BASE_OTA_SCHEMA)
.extend(cv.COMPONENT_SCHEMA)
.extend(cv.COMPONENT_SCHEMA),
_consume_ota_sockets,
)
FINAL_VALIDATE_SCHEMA = ota_esphome_final_validate

View File

@@ -14,13 +14,13 @@ template<typename... Ts> class SendAction : public Action<Ts...>, public Parente
TEMPLATABLE_VALUE(std::vector<uint8_t>, data);
public:
void add_on_sent(const std::vector<Action<Ts...> *> &actions) {
void add_on_sent(const std::initializer_list<Action<Ts...> *> &actions) {
this->sent_.add_actions(actions);
if (this->flags_.wait_for_sent) {
this->sent_.add_action(new LambdaAction<Ts...>([this](Ts... x) { this->play_next_(x...); }));
}
}
void add_on_error(const std::vector<Action<Ts...> *> &actions) {
void add_on_error(const std::initializer_list<Action<Ts...> *> &actions) {
this->error_.add_actions(actions);
if (this->flags_.wait_for_sent) {
this->error_.add_action(new LambdaAction<Ts...>([this](Ts... x) {

View File

@@ -0,0 +1,39 @@
"""ESP-NOW transport platform for packet_transport component."""
import esphome.codegen as cg
from esphome.components.packet_transport import (
PacketTransport,
new_packet_transport,
transport_schema,
)
import esphome.config_validation as cv
from esphome.core import HexInt
from esphome.cpp_types import PollingComponent
from .. import ESPNowComponent, espnow_ns
CODEOWNERS = ["@EasilyBoredEngineer"]
DEPENDENCIES = ["espnow"]
ESPNowTransport = espnow_ns.class_("ESPNowTransport", PacketTransport, PollingComponent)
CONF_ESPNOW_ID = "espnow_id"
CONF_PEER_ADDRESS = "peer_address"
CONFIG_SCHEMA = transport_schema(ESPNowTransport).extend(
{
cv.GenerateID(CONF_ESPNOW_ID): cv.use_id(ESPNowComponent),
cv.Optional(CONF_PEER_ADDRESS, default="FF:FF:FF:FF:FF:FF"): cv.mac_address,
}
)
async def to_code(config):
"""Set up the ESP-NOW transport component."""
var, _ = await new_packet_transport(config)
await cg.register_parented(var, config[CONF_ESPNOW_ID])
# Set peer address - convert MAC to parts array like ESP-NOW does
mac = config[CONF_PEER_ADDRESS]
cg.add(var.set_peer_address([HexInt(x) for x in mac.parts]))

View File

@@ -0,0 +1,97 @@
#include "espnow_transport.h"
#ifdef USE_ESP32
#include "esphome/core/application.h"
#include "esphome/core/log.h"
namespace esphome {
namespace espnow {
static const char *const TAG = "espnow.transport";
bool ESPNowTransport::should_send() { return this->parent_ != nullptr && !this->parent_->is_failed(); }
void ESPNowTransport::setup() {
packet_transport::PacketTransport::setup();
if (this->parent_ == nullptr) {
ESP_LOGE(TAG, "ESPNow component not set");
this->mark_failed();
return;
}
ESP_LOGI(TAG, "Registering ESP-NOW handlers");
ESP_LOGI(TAG, "Peer address: %02X:%02X:%02X:%02X:%02X:%02X", this->peer_address_[0], this->peer_address_[1],
this->peer_address_[2], this->peer_address_[3], this->peer_address_[4], this->peer_address_[5]);
// Register received handler
this->parent_->register_received_handler(static_cast<ESPNowReceivedPacketHandler *>(this));
// Register broadcasted handler
this->parent_->register_broadcasted_handler(static_cast<ESPNowBroadcastedHandler *>(this));
}
void ESPNowTransport::update() {
packet_transport::PacketTransport::update();
this->updated_ = true;
}
void ESPNowTransport::send_packet(const std::vector<uint8_t> &buf) const {
if (this->parent_ == nullptr) {
ESP_LOGE(TAG, "ESPNow component not set");
return;
}
if (buf.empty()) {
ESP_LOGW(TAG, "Attempted to send empty packet");
return;
}
if (buf.size() > ESP_NOW_MAX_DATA_LEN) {
ESP_LOGE(TAG, "Packet too large: %zu bytes (max %d)", buf.size(), ESP_NOW_MAX_DATA_LEN);
return;
}
// Send to configured peer address
this->parent_->send(this->peer_address_.data(), buf.data(), buf.size(), [](esp_err_t err) {
if (err != ESP_OK) {
ESP_LOGW(TAG, "Send failed: %d", err);
}
});
}
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]);
if (data == nullptr || size == 0) {
ESP_LOGW(TAG, "Received empty or null packet");
return false;
}
this->packet_buffer_.resize(size);
memcpy(this->packet_buffer_.data(), data, size);
this->process_(this->packet_buffer_);
return false; // Allow other handlers to run
}
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]);
if (data == nullptr || size == 0) {
ESP_LOGW(TAG, "Received empty or null broadcast packet");
return false;
}
this->packet_buffer_.resize(size);
memcpy(this->packet_buffer_.data(), data, size);
this->process_(this->packet_buffer_);
return false; // Allow other handlers to run
}
} // namespace espnow
} // namespace esphome
#endif // USE_ESP32

View File

@@ -0,0 +1,44 @@
#pragma once
#include "../espnow_component.h"
#ifdef USE_ESP32
#include "esphome/core/component.h"
#include "esphome/components/packet_transport/packet_transport.h"
#include <vector>
namespace esphome {
namespace espnow {
class ESPNowTransport : public packet_transport::PacketTransport,
public Parented<ESPNowComponent>,
public ESPNowReceivedPacketHandler,
public ESPNowBroadcastedHandler {
public:
void setup() override;
void update() override;
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
void set_peer_address(peer_address_t address) {
memcpy(this->peer_address_.data(), address.data(), ESP_NOW_ETH_ALEN);
}
// ESPNow handler interface
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;
size_t get_max_packet_size() override { return ESP_NOW_MAX_DATA_LEN; }
bool should_send() override;
peer_address_t peer_address_{{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}};
std::vector<uint8_t> packet_buffer_;
};
} // namespace espnow
} // namespace esphome
#endif // USE_ESP32

View File

@@ -38,7 +38,6 @@ IS_PLATFORM_COMPONENT = True
fan_ns = cg.esphome_ns.namespace("fan")
Fan = fan_ns.class_("Fan", cg.EntityBase)
FanState = fan_ns.class_("Fan", Fan, cg.Component)
FanDirection = fan_ns.enum("FanDirection", is_class=True)
FAN_DIRECTION_ENUM = {

View File

@@ -1,8 +1,8 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "fan_state.h"
#include "esphome/core/component.h"
#include "fan.h"
namespace esphome {
namespace fan {

View File

@@ -1,16 +0,0 @@
#include "fan_state.h"
namespace esphome {
namespace fan {
static const char *const TAG = "fan";
void FanState::setup() {
auto restore = this->restore_state_();
if (restore)
restore->to_call(*this).perform();
}
float FanState::get_setup_priority() const { return setup_priority::DATA - 1.0f; }
} // namespace fan
} // namespace esphome

View File

@@ -1,34 +0,0 @@
#pragma once
#include "esphome/core/component.h"
#include "fan.h"
namespace esphome {
namespace fan {
enum ESPDEPRECATED("LegacyFanDirection members are deprecated, use FanDirection instead.",
"2022.2") LegacyFanDirection {
FAN_DIRECTION_FORWARD = 0,
FAN_DIRECTION_REVERSE = 1
};
class ESPDEPRECATED("FanState is deprecated, use Fan instead.", "2022.2") FanState : public Fan, public Component {
public:
FanState() = default;
/// Get the traits of this fan.
FanTraits get_traits() override { return this->traits_; }
/// Set the traits of this fan (i.e. what features it supports).
void set_traits(const FanTraits &traits) { this->traits_ = traits; }
void setup() override;
float get_setup_priority() const override;
protected:
void control(const FanCall &call) override { this->publish_state(); }
FanTraits traits_{};
};
} // namespace fan
} // namespace esphome

View File

@@ -65,7 +65,7 @@ HaierClimateBase::HaierClimateBase()
{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH});
this->traits_.set_supported_swing_modes({climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH,
climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL});
this->traits_.set_supports_current_temperature(true);
this->traits_.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
}
HaierClimateBase::~HaierClimateBase() {}

View File

@@ -16,7 +16,8 @@ void HDC1080Component::setup() {
// if configuration fails - there is a problem
if (this->write_register(HDC1080_CMD_CONFIGURATION, config, 2) != i2c::ERROR_OK) {
this->mark_failed();
ESP_LOGW(TAG, "Failed to configure HDC1080");
this->status_set_warning();
return;
}
}

View File

@@ -3,6 +3,8 @@ import re
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import __version__
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType
CODEOWNERS = ["@esphome/core"]
@@ -35,7 +37,9 @@ def _process_next_url(url: str):
return url
async def setup_improv_core(var, config):
if CONF_NEXT_URL in config:
cg.add(var.set_next_url(_process_next_url(config[CONF_NEXT_URL])))
async def setup_improv_core(var: MockObj, config: ConfigType, component: str):
if next_url := config.get(CONF_NEXT_URL):
cg.add(var.set_next_url(_process_next_url(next_url)))
cg.add_define(f"USE_{component.upper()}_NEXT_URL")
cg.add_library("improv/Improv", "1.2.4")

View File

@@ -2,36 +2,50 @@
#include "esphome/components/network/util.h"
#include "esphome/core/application.h"
#include "esphome/core/defines.h"
namespace esphome {
namespace improv_base {
#if defined(USE_ESP32_IMPROV_NEXT_URL) || defined(USE_IMPROV_SERIAL_NEXT_URL)
static constexpr const char DEVICE_NAME_PLACEHOLDER[] = "{{device_name}}";
static constexpr size_t DEVICE_NAME_PLACEHOLDER_LEN = sizeof(DEVICE_NAME_PLACEHOLDER) - 1;
static constexpr const char IP_ADDRESS_PLACEHOLDER[] = "{{ip_address}}";
static constexpr size_t IP_ADDRESS_PLACEHOLDER_LEN = sizeof(IP_ADDRESS_PLACEHOLDER) - 1;
static void replace_all_in_place(std::string &str, const char *placeholder, size_t placeholder_len,
const std::string &replacement) {
size_t pos = 0;
const size_t replacement_len = replacement.length();
while ((pos = str.find(placeholder, pos)) != std::string::npos) {
str.replace(pos, placeholder_len, replacement);
pos += replacement_len;
}
}
std::string ImprovBase::get_formatted_next_url_() {
if (this->next_url_.empty()) {
return "";
}
std::string copy = this->next_url_;
// Device name
std::size_t pos = this->next_url_.find("{{device_name}}");
if (pos != std::string::npos) {
const std::string &device_name = App.get_name();
copy.replace(pos, 15, device_name);
}
// Ip address
pos = this->next_url_.find("{{ip_address}}");
if (pos != std::string::npos) {
for (auto &ip : network::get_ip_addresses()) {
if (ip.is_ip4()) {
std::string ipa = ip.str();
copy.replace(pos, 14, ipa);
break;
}
std::string formatted_url = this->next_url_;
// Replace all occurrences of {{device_name}}
replace_all_in_place(formatted_url, DEVICE_NAME_PLACEHOLDER, DEVICE_NAME_PLACEHOLDER_LEN, App.get_name());
// Replace all occurrences of {{ip_address}}
for (auto &ip : network::get_ip_addresses()) {
if (ip.is_ip4()) {
replace_all_in_place(formatted_url, IP_ADDRESS_PLACEHOLDER, IP_ADDRESS_PLACEHOLDER_LEN, ip.str());
break;
}
}
return copy;
// Note: {{esphome_version}} is replaced at code generation time in Python
return formatted_url;
}
#endif
} // namespace improv_base
} // namespace esphome

View File

@@ -1,17 +1,22 @@
#pragma once
#include <string>
#include "esphome/core/defines.h"
namespace esphome {
namespace improv_base {
class ImprovBase {
public:
#if defined(USE_ESP32_IMPROV_NEXT_URL) || defined(USE_IMPROV_SERIAL_NEXT_URL)
void set_next_url(const std::string &next_url) { this->next_url_ = next_url; }
#endif
protected:
#if defined(USE_ESP32_IMPROV_NEXT_URL) || defined(USE_IMPROV_SERIAL_NEXT_URL)
std::string get_formatted_next_url_();
std::string next_url_;
#endif
};
} // namespace improv_base

View File

@@ -43,4 +43,4 @@ FINAL_VALIDATE_SCHEMA = validate_logger
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await improv_base.setup_improv_core(var, config)
await improv_base.setup_improv_core(var, config, "improv_serial")

View File

@@ -146,9 +146,11 @@ void ImprovSerialComponent::loop() {
std::vector<uint8_t> ImprovSerialComponent::build_rpc_settings_response_(improv::Command command) {
std::vector<std::string> urls;
#ifdef USE_IMPROV_SERIAL_NEXT_URL
if (!this->next_url_.empty()) {
urls.push_back(this->get_formatted_next_url_());
}
#endif
#ifdef USE_WEBSERVER
for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) {
if (ip.is_ip4()) {

View File

@@ -14,7 +14,7 @@ void Kuntze::on_modbus_data(const std::vector<uint8_t> &data) {
auto get_16bit = [&](int i) -> uint16_t { return (uint16_t(data[i * 2]) << 8) | uint16_t(data[i * 2 + 1]); };
this->waiting_ = false;
ESP_LOGV(TAG, "Data: %s", hexencode(data).c_str());
ESP_LOGV(TAG, "Data: %s", format_hex_pretty(data).c_str());
float value = (float) get_16bit(0);
for (int i = 0; i < data[3]; i++)

View File

@@ -1,11 +1,11 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/color.h"
#include "esp_color_correction.h"
#include "esp_color_view.h"
#include "esp_range_view.h"
#include "esphome/core/color.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "light_output.h"
#include "light_state.h"
#include "transformers.h"
@@ -17,8 +17,6 @@
namespace esphome {
namespace light {
using ESPColor ESPDEPRECATED("esphome::light::ESPColor is deprecated, use esphome::Color instead.", "v1.21") = Color;
/// Convert the color information from a `LightColorValues` object to a `Color` object (does not apply brightness).
Color color_from_light_color_values(LightColorValues val);

View File

@@ -104,5 +104,200 @@ constexpr ColorModeHelper operator|(ColorModeHelper lhs, ColorMode rhs) {
return static_cast<ColorMode>(static_cast<uint8_t>(lhs) | static_cast<uint8_t>(rhs));
}
// Type alias for raw color mode bitmask values
using color_mode_bitmask_t = uint16_t;
// Constants for ColorMode count and bit range
static constexpr int COLOR_MODE_COUNT = 10; // UNKNOWN through RGB_COLD_WARM_WHITE
static constexpr int MAX_BIT_INDEX = sizeof(color_mode_bitmask_t) * 8; // Number of bits in bitmask type
// Compile-time array of all ColorMode values in declaration order
// Bit positions (0-9) map directly to enum declaration order
static constexpr ColorMode COLOR_MODES[COLOR_MODE_COUNT] = {
ColorMode::UNKNOWN, // bit 0
ColorMode::ON_OFF, // bit 1
ColorMode::BRIGHTNESS, // bit 2
ColorMode::WHITE, // bit 3
ColorMode::COLOR_TEMPERATURE, // bit 4
ColorMode::COLD_WARM_WHITE, // bit 5
ColorMode::RGB, // bit 6
ColorMode::RGB_WHITE, // bit 7
ColorMode::RGB_COLOR_TEMPERATURE, // bit 8
ColorMode::RGB_COLD_WARM_WHITE, // bit 9
};
/// Map ColorMode enum values to bit positions (0-9)
/// Bit positions follow the enum declaration order
static constexpr int mode_to_bit(ColorMode mode) {
// Linear search through COLOR_MODES array
// Compiler optimizes this to efficient code since array is constexpr
for (int i = 0; i < COLOR_MODE_COUNT; ++i) {
if (COLOR_MODES[i] == mode)
return i;
}
return 0;
}
/// Map bit positions (0-9) to ColorMode enum values
/// Bit positions follow the enum declaration order
static constexpr ColorMode bit_to_mode(int bit) {
// Direct lookup in COLOR_MODES array
return (bit >= 0 && bit < COLOR_MODE_COUNT) ? COLOR_MODES[bit] : ColorMode::UNKNOWN;
}
/// Helper to compute capability bitmask at compile time
static constexpr color_mode_bitmask_t compute_capability_bitmask(ColorCapability capability) {
color_mode_bitmask_t mask = 0;
uint8_t cap_bit = static_cast<uint8_t>(capability);
// Check each ColorMode to see if it has this capability
for (int bit = 0; bit < COLOR_MODE_COUNT; ++bit) {
uint8_t mode_val = static_cast<uint8_t>(bit_to_mode(bit));
if ((mode_val & cap_bit) != 0) {
mask |= (1 << bit);
}
}
return mask;
}
// Number of ColorCapability enum values
static constexpr int COLOR_CAPABILITY_COUNT = 6;
/// Compile-time lookup table mapping ColorCapability to bitmask
/// This array is computed at compile time using constexpr
static constexpr color_mode_bitmask_t CAPABILITY_BITMASKS[] = {
compute_capability_bitmask(ColorCapability::ON_OFF), // 1 << 0
compute_capability_bitmask(ColorCapability::BRIGHTNESS), // 1 << 1
compute_capability_bitmask(ColorCapability::WHITE), // 1 << 2
compute_capability_bitmask(ColorCapability::COLOR_TEMPERATURE), // 1 << 3
compute_capability_bitmask(ColorCapability::COLD_WARM_WHITE), // 1 << 4
compute_capability_bitmask(ColorCapability::RGB), // 1 << 5
};
/// Bitmask for storing a set of ColorMode values efficiently.
/// Replaces std::set<ColorMode> to eliminate red-black tree overhead (~586 bytes).
class ColorModeMask {
public:
constexpr ColorModeMask() = default;
/// Support initializer list syntax: {ColorMode::RGB, ColorMode::WHITE}
constexpr ColorModeMask(std::initializer_list<ColorMode> modes) {
for (auto mode : modes) {
this->add(mode);
}
}
constexpr void add(ColorMode mode) { this->mask_ |= (1 << mode_to_bit(mode)); }
/// Add multiple modes at once using initializer list
constexpr void add(std::initializer_list<ColorMode> modes) {
for (auto mode : modes) {
this->add(mode);
}
}
constexpr bool contains(ColorMode mode) const { return (this->mask_ & (1 << mode_to_bit(mode))) != 0; }
constexpr size_t size() const {
// Count set bits using Brian Kernighan's algorithm
// More efficient for sparse bitmasks (typical case: 2-4 modes out of 10)
uint16_t n = this->mask_;
size_t count = 0;
while (n) {
n &= n - 1; // Clear the least significant set bit
count++;
}
return count;
}
constexpr bool empty() const { return this->mask_ == 0; }
/// Iterator support for API encoding
class Iterator {
public:
using iterator_category = std::forward_iterator_tag;
using value_type = ColorMode;
using difference_type = std::ptrdiff_t;
using pointer = const ColorMode *;
using reference = ColorMode;
constexpr Iterator(color_mode_bitmask_t mask, int bit) : mask_(mask), bit_(bit) { advance_to_next_set_bit_(); }
constexpr ColorMode operator*() const { return bit_to_mode(bit_); }
constexpr Iterator &operator++() {
++bit_;
advance_to_next_set_bit_();
return *this;
}
constexpr bool operator==(const Iterator &other) const { return bit_ == other.bit_; }
constexpr bool operator!=(const Iterator &other) const { return !(*this == other); }
private:
constexpr void advance_to_next_set_bit_() { bit_ = ColorModeMask::find_next_set_bit(mask_, bit_); }
color_mode_bitmask_t mask_;
int bit_;
};
constexpr Iterator begin() const { return Iterator(mask_, 0); }
constexpr Iterator end() const { return Iterator(mask_, MAX_BIT_INDEX); }
/// Get the raw bitmask value for API encoding
constexpr color_mode_bitmask_t get_mask() const { return this->mask_; }
/// Find the next set bit in a bitmask starting from a given position
/// Returns the bit position, or MAX_BIT_INDEX if no more bits are set
static constexpr int find_next_set_bit(color_mode_bitmask_t mask, int start_bit) {
int bit = start_bit;
while (bit < MAX_BIT_INDEX && !(mask & (1 << bit))) {
++bit;
}
return bit;
}
/// Find the first set bit in a bitmask and return the corresponding ColorMode
/// Used for optimizing compute_color_mode_() intersection logic
static constexpr ColorMode first_mode_from_mask(color_mode_bitmask_t mask) {
return bit_to_mode(find_next_set_bit(mask, 0));
}
/// Check if a ColorMode is present in a raw bitmask value
/// Useful for checking intersection results without creating a temporary ColorModeMask
static constexpr bool mask_contains(color_mode_bitmask_t mask, ColorMode mode) {
return (mask & (1 << mode_to_bit(mode))) != 0;
}
/// Check if any mode in the bitmask has a specific capability
/// Used for checking if a light supports a capability (e.g., BRIGHTNESS, RGB)
bool has_capability(ColorCapability capability) const {
// Lookup the pre-computed bitmask for this capability and check intersection with our mask
// ColorCapability values: 1, 2, 4, 8, 16, 32 -> array indices: 0, 1, 2, 3, 4, 5
// We need to convert the power-of-2 value to an index
uint8_t cap_val = static_cast<uint8_t>(capability);
#if defined(__GNUC__) || defined(__clang__)
// Use compiler intrinsic for efficient bit position lookup (O(1) vs O(log n))
int index = __builtin_ctz(cap_val);
#else
// Fallback for compilers without __builtin_ctz
int index = 0;
while (cap_val > 1) {
cap_val >>= 1;
++index;
}
#endif
return (this->mask_ & CAPABILITY_BITMASKS[index]) != 0;
}
private:
// Using uint16_t instead of uint32_t for more efficient iteration (fewer bits to scan).
// Currently only 10 ColorMode values exist, so 16 bits is sufficient.
// Can be changed to uint32_t if more than 16 color modes are needed in the future.
// Note: Due to struct padding, uint16_t and uint32_t result in same LightTraits size (12 bytes).
color_mode_bitmask_t mask_{0};
};
} // namespace light
} // namespace esphome

View File

@@ -406,7 +406,7 @@ void LightCall::transform_parameters_() {
}
}
ColorMode LightCall::compute_color_mode_() {
auto supported_modes = this->parent_->get_traits().get_supported_color_modes();
const auto &supported_modes = this->parent_->get_traits().get_supported_color_modes();
int supported_count = supported_modes.size();
// Some lights don't support any color modes (e.g. monochromatic light), leave it at unknown.
@@ -425,20 +425,19 @@ ColorMode LightCall::compute_color_mode_() {
// If no color mode is specified, we try to guess the color mode. This is needed for backward compatibility to
// pre-colormode clients and automations, but also for the MQTT API, where HA doesn't let us know which color mode
// was used for some reason.
std::set<ColorMode> suitable_modes = this->get_suitable_color_modes_();
// Compute intersection of suitable and supported modes using bitwise AND
color_mode_bitmask_t intersection = this->get_suitable_color_modes_mask_() & supported_modes.get_mask();
// Don't change if the current mode is suitable.
if (suitable_modes.count(current_mode) > 0) {
// Don't change if the current mode is in the intersection (suitable AND supported)
if (ColorModeMask::mask_contains(intersection, current_mode)) {
ESP_LOGI(TAG, "'%s': color mode not specified; retaining %s", this->parent_->get_name().c_str(),
LOG_STR_ARG(color_mode_to_human(current_mode)));
return current_mode;
}
// Use the preferred suitable mode.
for (auto mode : suitable_modes) {
if (supported_modes.count(mode) == 0)
continue;
if (intersection != 0) {
ColorMode mode = ColorModeMask::first_mode_from_mask(intersection);
ESP_LOGI(TAG, "'%s': color mode not specified; using %s", this->parent_->get_name().c_str(),
LOG_STR_ARG(color_mode_to_human(mode)));
return mode;
@@ -451,7 +450,7 @@ ColorMode LightCall::compute_color_mode_() {
LOG_STR_ARG(color_mode_to_human(color_mode)));
return color_mode;
}
std::set<ColorMode> LightCall::get_suitable_color_modes_() {
color_mode_bitmask_t LightCall::get_suitable_color_modes_mask_() {
bool has_white = this->has_white() && this->white_ > 0.0f;
bool has_ct = this->has_color_temperature();
bool has_cwww =
@@ -459,36 +458,44 @@ std::set<ColorMode> LightCall::get_suitable_color_modes_() {
bool has_rgb = (this->has_color_brightness() && this->color_brightness_ > 0.0f) ||
(this->has_red() || this->has_green() || this->has_blue());
// Build key from flags: [rgb][cwww][ct][white]
// Build key from flags: [rgb][cwww][ct][white]
#define KEY(white, ct, cwww, rgb) ((white) << 0 | (ct) << 1 | (cwww) << 2 | (rgb) << 3)
uint8_t key = KEY(has_white, has_ct, has_cwww, has_rgb);
switch (key) {
case KEY(true, false, false, false): // white only
return {ColorMode::WHITE, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE,
ColorMode::RGB_COLD_WARM_WHITE};
return ColorModeMask({ColorMode::WHITE, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE,
ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE})
.get_mask();
case KEY(false, true, false, false): // ct only
return {ColorMode::COLOR_TEMPERATURE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE,
ColorMode::RGB_COLD_WARM_WHITE};
return ColorModeMask({ColorMode::COLOR_TEMPERATURE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE,
ColorMode::RGB_COLD_WARM_WHITE})
.get_mask();
case KEY(true, true, false, false): // white + ct
return {ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE};
return ColorModeMask(
{ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE})
.get_mask();
case KEY(false, false, true, false): // cwww only
return {ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE};
return ColorModeMask({ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE}).get_mask();
case KEY(false, false, false, false): // none
return {ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE, ColorMode::RGB,
ColorMode::WHITE, ColorMode::COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE};
return ColorModeMask({ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE,
ColorMode::RGB, ColorMode::WHITE, ColorMode::COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE})
.get_mask();
case KEY(true, false, false, true): // rgb + white
return {ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE};
return ColorModeMask({ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE})
.get_mask();
case KEY(false, true, false, true): // rgb + ct
case KEY(true, true, false, true): // rgb + white + ct
return {ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE};
return ColorModeMask({ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}).get_mask();
case KEY(false, false, true, true): // rgb + cwww
return {ColorMode::RGB_COLD_WARM_WHITE};
return ColorModeMask({ColorMode::RGB_COLD_WARM_WHITE}).get_mask();
case KEY(false, false, false, true): // rgb only
return {ColorMode::RGB, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE};
return ColorModeMask({ColorMode::RGB, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE,
ColorMode::RGB_COLD_WARM_WHITE})
.get_mask();
default:
return {}; // conflicting flags
return 0; // conflicting flags
}
#undef KEY

View File

@@ -1,7 +1,6 @@
#pragma once
#include "light_color_values.h"
#include <set>
namespace esphome {
@@ -186,8 +185,8 @@ class LightCall {
//// Compute the color mode that should be used for this call.
ColorMode compute_color_mode_();
/// Get potential color modes for this light call.
std::set<ColorMode> get_suitable_color_modes_();
/// Get potential color modes bitmask for this light call.
color_mode_bitmask_t get_suitable_color_modes_mask_();
/// Some color modes also can be set using non-native parameters, transform those calls.
void transform_parameters_();

View File

@@ -43,7 +43,6 @@ void LightJSONSchema::dump_json(LightState &state, JsonObject root) {
}
auto values = state.remote_values;
auto traits = state.get_output()->get_traits();
const auto color_mode = values.get_color_mode();
const char *mode_str = get_color_mode_json_str(color_mode);

View File

@@ -191,11 +191,9 @@ void LightState::current_values_as_brightness(float *brightness) {
this->current_values.as_brightness(brightness, this->gamma_correct_);
}
void LightState::current_values_as_rgb(float *red, float *green, float *blue, bool color_interlock) {
auto traits = this->get_traits();
this->current_values.as_rgb(red, green, blue, this->gamma_correct_, false);
}
void LightState::current_values_as_rgbw(float *red, float *green, float *blue, float *white, bool color_interlock) {
auto traits = this->get_traits();
this->current_values.as_rgbw(red, green, blue, white, this->gamma_correct_, false);
}
void LightState::current_values_as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white,
@@ -209,7 +207,6 @@ void LightState::current_values_as_rgbct(float *red, float *green, float *blue,
white_brightness, this->gamma_correct_);
}
void LightState::current_values_as_cwww(float *cold_white, float *warm_white, bool constant_brightness) {
auto traits = this->get_traits();
this->current_values.as_cwww(cold_white, warm_white, this->gamma_correct_, constant_brightness);
}
void LightState::current_values_as_ct(float *color_temperature, float *white_brightness) {

View File

@@ -1,8 +1,7 @@
#pragma once
#include "esphome/core/helpers.h"
#include "color_mode.h"
#include <set>
#include "esphome/core/helpers.h"
namespace esphome {
@@ -19,38 +18,17 @@ class LightTraits {
public:
LightTraits() = default;
const std::set<ColorMode> &get_supported_color_modes() const { return this->supported_color_modes_; }
void set_supported_color_modes(std::set<ColorMode> supported_color_modes) {
this->supported_color_modes_ = std::move(supported_color_modes);
const ColorModeMask &get_supported_color_modes() const { return this->supported_color_modes_; }
void set_supported_color_modes(ColorModeMask supported_color_modes) {
this->supported_color_modes_ = supported_color_modes;
}
void set_supported_color_modes(std::initializer_list<ColorMode> modes) {
this->supported_color_modes_ = ColorModeMask(modes);
}
bool supports_color_mode(ColorMode color_mode) const { return this->supported_color_modes_.count(color_mode); }
bool supports_color_mode(ColorMode color_mode) const { return this->supported_color_modes_.contains(color_mode); }
bool supports_color_capability(ColorCapability color_capability) const {
for (auto mode : this->supported_color_modes_) {
if (mode & color_capability)
return true;
}
return false;
}
ESPDEPRECATED("get_supports_brightness() is deprecated, use color modes instead.", "v1.21")
bool get_supports_brightness() const { return this->supports_color_capability(ColorCapability::BRIGHTNESS); }
ESPDEPRECATED("get_supports_rgb() is deprecated, use color modes instead.", "v1.21")
bool get_supports_rgb() const { return this->supports_color_capability(ColorCapability::RGB); }
ESPDEPRECATED("get_supports_rgb_white_value() is deprecated, use color modes instead.", "v1.21")
bool get_supports_rgb_white_value() const {
return this->supports_color_mode(ColorMode::RGB_WHITE) ||
this->supports_color_mode(ColorMode::RGB_COLOR_TEMPERATURE);
}
ESPDEPRECATED("get_supports_color_temperature() is deprecated, use color modes instead.", "v1.21")
bool get_supports_color_temperature() const {
return this->supports_color_capability(ColorCapability::COLOR_TEMPERATURE);
}
ESPDEPRECATED("get_supports_color_interlock() is deprecated, use color modes instead.", "v1.21")
bool get_supports_color_interlock() const {
return this->supports_color_mode(ColorMode::RGB) &&
(this->supports_color_mode(ColorMode::WHITE) || this->supports_color_mode(ColorMode::COLD_WARM_WHITE) ||
this->supports_color_mode(ColorMode::COLOR_TEMPERATURE));
return this->supported_color_modes_.has_capability(color_capability);
}
float get_min_mireds() const { return this->min_mireds_; }
@@ -59,19 +37,9 @@ class LightTraits {
void set_max_mireds(float max_mireds) { this->max_mireds_ = max_mireds; }
protected:
#ifdef USE_API
// The API connection is a friend class to access internal methods
friend class api::APIConnection;
// This method returns a reference to the internal color modes set.
// It is used by the API to avoid copying data when encoding messages.
// Warning: Do not use this method outside of the API connection code.
// It returns a reference to internal data that can be invalidated.
const std::set<ColorMode> &get_supported_color_modes_for_api_() const { return this->supported_color_modes_; }
#endif
std::set<ColorMode> supported_color_modes_{};
float min_mireds_{0};
float max_mireds_{0};
ColorModeMask supported_color_modes_{};
};
} // namespace light

View File

@@ -68,6 +68,9 @@ static constexpr char LOG_LEVEL_LETTER_CHARS[] = {
// Maximum header size: 35 bytes fixed + 32 bytes tag + 16 bytes thread name = 83 bytes (45 byte safety margin)
static constexpr uint16_t MAX_HEADER_SIZE = 128;
// "0x" + 2 hex digits per byte + '\0'
static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1;
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
/** Enum for logging UART selection
*
@@ -177,8 +180,11 @@ class Logger : public Component {
inline void HOT format_log_to_buffer_with_terminator_(uint8_t level, const char *tag, int line, const char *format,
va_list args, char *buffer, uint16_t *buffer_at,
uint16_t buffer_size) {
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
this->write_header_to_buffer_(level, tag, line, this->get_thread_name_(), buffer, buffer_at, buffer_size);
#elif defined(USE_ZEPHYR)
char buff[MAX_POINTER_REPRESENTATION];
this->write_header_to_buffer_(level, tag, line, this->get_thread_name_(buff), buffer, buffer_at, buffer_size);
#else
this->write_header_to_buffer_(level, tag, line, nullptr, buffer, buffer_at, buffer_size);
#endif
@@ -277,7 +283,11 @@ class Logger : public Component {
#endif
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
const char *HOT get_thread_name_() {
const char *HOT get_thread_name_(
#ifdef USE_ZEPHYR
char *buff
#endif
) {
#ifdef USE_ZEPHYR
k_tid_t current_task = k_current_get();
#else
@@ -291,7 +301,13 @@ class Logger : public Component {
#elif defined(USE_LIBRETINY)
return pcTaskGetTaskName(current_task);
#elif defined(USE_ZEPHYR)
return k_thread_name_get(current_task);
const char *name = k_thread_name_get(current_task);
if (name) {
// zephyr print task names only if debug component is present
return name;
}
std::snprintf(buff, MAX_POINTER_REPRESENTATION, "%p", current_task);
return buff;
#endif
}
}

View File

@@ -13,6 +13,7 @@ from esphome.const import (
)
from esphome.core import CORE, Lambda, coroutine_with_priority
from esphome.coroutine import CoroPriority
from esphome.types import ConfigType
CODEOWNERS = ["@esphome/core"]
DEPENDENCIES = ["network"]
@@ -46,6 +47,19 @@ SERVICE_SCHEMA = cv.Schema(
}
)
def _consume_mdns_sockets(config: ConfigType) -> ConfigType:
"""Register socket needs for mDNS component."""
if config.get(CONF_DISABLED):
return config
from esphome.components import socket
# mDNS needs 2 sockets (IPv4 + IPv6 multicast)
socket.consume_sockets(2, "mdns")(config)
return config
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
@@ -55,6 +69,7 @@ CONFIG_SCHEMA = cv.All(
}
),
_remove_id_if_disabled,
_consume_mdns_sockets,
)

View File

@@ -31,18 +31,17 @@ void MDNSComponent::setup() {
mdns_instance_name_set(this->hostname_.c_str());
for (const auto &service : services) {
std::vector<mdns_txt_item_t> txt_records;
for (const auto &record : service.txt_records) {
mdns_txt_item_t it{};
auto txt_records = std::make_unique<mdns_txt_item_t[]>(service.txt_records.size());
for (size_t i = 0; i < service.txt_records.size(); i++) {
const auto &record = service.txt_records[i];
// key and value are either compile-time string literals in flash or pointers to dynamic_txt_values_
// Both remain valid for the lifetime of this function, and ESP-IDF makes internal copies
it.key = MDNS_STR_ARG(record.key);
it.value = MDNS_STR_ARG(record.value);
txt_records.push_back(it);
txt_records[i].key = MDNS_STR_ARG(record.key);
txt_records[i].value = MDNS_STR_ARG(record.value);
}
uint16_t port = const_cast<TemplatableValue<uint16_t> &>(service.port).value();
err = mdns_service_add(nullptr, MDNS_STR_ARG(service.service_type), MDNS_STR_ARG(service.proto), port,
txt_records.data(), txt_records.size());
txt_records.get(), service.txt_records.size());
if (err != ESP_OK) {
ESP_LOGW(TAG, "Failed to register service %s: %s", MDNS_STR_ARG(service.service_type), esp_err_to_name(err));

View File

@@ -77,7 +77,7 @@ void AirConditioner::control(const ClimateCall &call) {
ClimateTraits AirConditioner::traits() {
auto traits = ClimateTraits();
traits.set_supports_current_temperature(true);
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
traits.set_visual_min_temperature(17);
traits.set_visual_max_temperature(30);
traits.set_visual_temperature_step(0.5);

View File

@@ -30,6 +30,19 @@ wave_4_3 = DriverChip(
"blue": [14, 38, 18, 17, 10],
},
)
wave_4_3.extend(
"WAVESHARE-5-1024X600",
width=1024,
height=600,
hsync_back_porch=145,
hsync_front_porch=170,
hsync_pulse_width=30,
vsync_back_porch=23,
vsync_front_porch=12,
vsync_pulse_width=2,
)
wave_4_3.extend(
"ESP32-S3-TOUCH-LCD-7-800X480",
enable_pin=[{"ch422g": None, "number": 2}, {"ch422g": None, "number": 6}],

View File

@@ -52,8 +52,9 @@ const uint8_t MITSUBISHI_BYTE16 = 0x00;
climate::ClimateTraits MitsubishiClimate::traits() {
auto traits = climate::ClimateTraits();
traits.set_supports_current_temperature(this->sensor_ != nullptr);
traits.set_supports_action(false);
if (this->sensor_ != nullptr) {
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
}
traits.set_visual_min_temperature(MITSUBISHI_TEMP_MIN);
traits.set_visual_max_temperature(MITSUBISHI_TEMP_MAX);
traits.set_visual_temperature_step(1.0f);

View File

@@ -58,6 +58,7 @@ from esphome.const import (
PlatformFramework,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.types import ConfigType
DEPENDENCIES = ["network"]
@@ -210,6 +211,15 @@ def validate_fingerprint(value):
return value
def _consume_mqtt_sockets(config: ConfigType) -> ConfigType:
"""Register socket needs for MQTT component."""
from esphome.components import socket
# MQTT needs 1 socket for the broker connection
socket.consume_sockets(1, "mqtt")(config)
return config
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
@@ -306,6 +316,7 @@ CONFIG_SCHEMA = cv.All(
),
validate_config,
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX]),
_consume_mqtt_sockets,
)

View File

@@ -17,11 +17,11 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
auto traits = this->device_->get_traits();
// current_temperature_topic
if (traits.get_supports_current_temperature()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) {
root[MQTT_CURRENT_TEMPERATURE_TOPIC] = this->get_current_temperature_state_topic();
}
// current_humidity_topic
if (traits.get_supports_current_humidity()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) {
root[MQTT_CURRENT_HUMIDITY_TOPIC] = this->get_current_humidity_state_topic();
}
// mode_command_topic
@@ -45,7 +45,8 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
if (traits.supports_mode(CLIMATE_MODE_HEAT_COOL))
modes.add("heat_cool");
if (traits.get_supports_two_point_target_temperature()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
// temperature_low_command_topic
root[MQTT_TEMPERATURE_LOW_COMMAND_TOPIC] = this->get_target_temperature_low_command_topic();
// temperature_low_state_topic
@@ -61,7 +62,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
root[MQTT_TEMPERATURE_STATE_TOPIC] = this->get_target_temperature_state_topic();
}
if (traits.get_supports_target_humidity()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
// target_humidity_command_topic
root[MQTT_TARGET_HUMIDITY_COMMAND_TOPIC] = this->get_target_humidity_command_topic();
// target_humidity_state_topic
@@ -109,7 +110,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
presets.add(preset);
}
if (traits.get_supports_action()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) {
// action_topic
root[MQTT_ACTION_TOPIC] = this->get_action_state_topic();
}
@@ -174,7 +175,8 @@ void MQTTClimateComponent::setup() {
call.perform();
});
if (traits.get_supports_two_point_target_temperature()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
this->subscribe(this->get_target_temperature_low_command_topic(),
[this](const std::string &topic, const std::string &payload) {
auto val = parse_number<float>(payload);
@@ -211,7 +213,7 @@ void MQTTClimateComponent::setup() {
});
}
if (traits.get_supports_target_humidity()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
this->subscribe(this->get_target_humidity_command_topic(),
[this](const std::string &topic, const std::string &payload) {
auto val = parse_number<float>(payload);
@@ -290,12 +292,14 @@ bool MQTTClimateComponent::publish_state_() {
success = false;
int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals();
int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals();
if (traits.get_supports_current_temperature() && !std::isnan(this->device_->current_temperature)) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE) &&
!std::isnan(this->device_->current_temperature)) {
std::string payload = value_accuracy_to_string(this->device_->current_temperature, current_accuracy);
if (!this->publish(this->get_current_temperature_state_topic(), payload))
success = false;
}
if (traits.get_supports_two_point_target_temperature()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
std::string payload = value_accuracy_to_string(this->device_->target_temperature_low, target_accuracy);
if (!this->publish(this->get_target_temperature_low_state_topic(), payload))
success = false;
@@ -308,12 +312,14 @@ bool MQTTClimateComponent::publish_state_() {
success = false;
}
if (traits.get_supports_current_humidity() && !std::isnan(this->device_->current_humidity)) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY) &&
!std::isnan(this->device_->current_humidity)) {
std::string payload = value_accuracy_to_string(this->device_->current_humidity, 0);
if (!this->publish(this->get_current_humidity_state_topic(), payload))
success = false;
}
if (traits.get_supports_target_humidity() && !std::isnan(this->device_->target_humidity)) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY) &&
!std::isnan(this->device_->target_humidity)) {
std::string payload = value_accuracy_to_string(this->device_->target_humidity, 0);
if (!this->publish(this->get_target_humidity_state_topic(), payload))
success = false;
@@ -357,7 +363,7 @@ bool MQTTClimateComponent::publish_state_() {
success = false;
}
if (traits.get_supports_action()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) {
const char *payload;
switch (this->device_->action) {
case CLIMATE_ACTION_OFF:

View File

@@ -135,7 +135,8 @@ bool MQTTComponent::send_discovery_() {
if (discovery_info.object_id_generator == MQTT_DEVICE_NAME_OBJECT_ID_GENERATOR)
root[MQTT_OBJECT_ID] = node_name + "_" + this->get_default_object_id_();
const std::string &node_friendly_name = App.get_friendly_name().empty() ? node_name : App.get_friendly_name();
const std::string &friendly_name_ref = App.get_friendly_name();
const std::string &node_friendly_name = friendly_name_ref.empty() ? node_name : friendly_name_ref;
std::string node_area = App.get_area();
JsonObject device_info = root[MQTT_DEVICE].to<JsonObject>();

View File

@@ -5,7 +5,7 @@
#ifdef USE_MQTT
#ifdef USE_FAN
#include "esphome/components/fan/fan_state.h"
#include "esphome/components/fan/fan.h"
#include "mqtt_component.h"
namespace esphome {

View File

@@ -69,6 +69,12 @@ void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscovery
if (traits.supports_color_capability(ColorCapability::BRIGHTNESS))
root["brightness"] = true;
if (traits.supports_color_mode(ColorMode::COLOR_TEMPERATURE) ||
traits.supports_color_mode(ColorMode::COLD_WARM_WHITE)) {
root[MQTT_MIN_MIREDS] = traits.get_min_mireds();
root[MQTT_MAX_MIREDS] = traits.get_max_mireds();
}
if (this->state_->supports_effects()) {
root["effect"] = true;
JsonArray effect_list = root[MQTT_EFFECT_LIST].to<JsonArray>();

View File

@@ -1291,9 +1291,6 @@ void Nextion::check_pending_waveform_() {
void Nextion::set_writer(const nextion_writer_t &writer) { this->writer_ = writer; }
ESPDEPRECATED("set_wait_for_ack(bool) deprecated, no effect", "v1.20")
void Nextion::set_wait_for_ack(bool wait_for_ack) { ESP_LOGE(TAG, "Deprecated"); }
bool Nextion::is_updating() { return this->connection_state_.is_updating_; }
} // namespace nextion

View File

@@ -54,11 +54,10 @@ void PIDClimate::control(const climate::ClimateCall &call) {
}
climate::ClimateTraits PIDClimate::traits() {
auto traits = climate::ClimateTraits();
traits.set_supports_current_temperature(true);
traits.set_supports_two_point_target_temperature(false);
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE | climate::CLIMATE_SUPPORTS_ACTION);
if (this->humidity_sensor_ != nullptr)
traits.set_supports_current_humidity(true);
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY);
traits.set_supported_modes({climate::CLIMATE_MODE_OFF});
if (supports_cool_())
@@ -68,7 +67,6 @@ climate::ClimateTraits PIDClimate::traits() {
if (supports_heat_() && supports_cool_())
traits.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL);
traits.set_supports_action(true);
return traits;
}
void PIDClimate::dump_config() {

View File

@@ -62,7 +62,7 @@ CONF_WARNING_MPPT_OVERLOAD = "warning_mppt_overload"
CONF_WARNING_BATTERY_TOO_LOW_TO_CHARGE = "warning_battery_too_low_to_charge"
CONF_FAULT_DC_DC_OVER_CURRENT = "fault_dc_dc_over_current"
CONF_FAULT_CODE = "fault_code"
CONF_WARNUNG_LOW_PV_ENERGY = "warnung_low_pv_energy"
CONF_WARNING_LOW_PV_ENERGY = "warning_low_pv_energy"
CONF_WARNING_HIGH_AC_INPUT_DURING_BUS_SOFT_START = (
"warning_high_ac_input_during_bus_soft_start"
)
@@ -122,7 +122,7 @@ TYPES = [
CONF_WARNING_BATTERY_TOO_LOW_TO_CHARGE,
CONF_FAULT_DC_DC_OVER_CURRENT,
CONF_FAULT_CODE,
CONF_WARNUNG_LOW_PV_ENERGY,
CONF_WARNING_LOW_PV_ENERGY,
CONF_WARNING_HIGH_AC_INPUT_DURING_BUS_SOFT_START,
CONF_WARNING_BATTERY_EQUALIZATION,
]

View File

@@ -13,7 +13,7 @@ void PipsolarOutput::write_state(float state) {
if (std::find(this->possible_values_.begin(), this->possible_values_.end(), state) != this->possible_values_.end()) {
ESP_LOGD(TAG, "Will write: %s out of value %f / %02.0f", tmp, state, state);
this->parent_->switch_command(std::string(tmp));
this->parent_->queue_command(std::string(tmp));
} else {
ESP_LOGD(TAG, "Will not write: %s as it is not in list of allowed values", tmp);
}

File diff suppressed because it is too large Load Diff

View File

@@ -7,6 +7,7 @@
#include "esphome/components/uart/uart.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
namespace esphome {
namespace pipsolar {
@@ -28,10 +29,17 @@ struct PollingCommand {
bool needs_update;
};
#define PIPSOLAR_VALUED_ENTITY_(type, name, polling_command, value_type) \
protected: \
value_type value_##name##_; \
PIPSOLAR_ENTITY_(type, name, polling_command)
struct QFLAGValues {
esphome::optional<bool> silence_buzzer_open_buzzer;
esphome::optional<bool> overload_bypass_function;
esphome::optional<bool> lcd_escape_to_default;
esphome::optional<bool> overload_restart_function;
esphome::optional<bool> over_temperature_restart_function;
esphome::optional<bool> backlight_on;
esphome::optional<bool> alarm_on_when_primary_source_interrupt;
esphome::optional<bool> fault_code_record;
esphome::optional<bool> power_saving;
};
#define PIPSOLAR_ENTITY_(type, name, polling_command) \
protected: \
@@ -43,126 +51,123 @@ struct PollingCommand {
this->add_polling_command_(#polling_command, POLLING_##polling_command); \
}
#define PIPSOLAR_SENSOR(name, polling_command, value_type) \
PIPSOLAR_VALUED_ENTITY_(sensor::Sensor, name, polling_command, value_type)
#define PIPSOLAR_SENSOR(name, polling_command) PIPSOLAR_ENTITY_(sensor::Sensor, name, polling_command)
#define PIPSOLAR_SWITCH(name, polling_command) PIPSOLAR_ENTITY_(switch_::Switch, name, polling_command)
#define PIPSOLAR_BINARY_SENSOR(name, polling_command, value_type) \
PIPSOLAR_VALUED_ENTITY_(binary_sensor::BinarySensor, name, polling_command, value_type)
#define PIPSOLAR_VALUED_TEXT_SENSOR(name, polling_command, value_type) \
PIPSOLAR_VALUED_ENTITY_(text_sensor::TextSensor, name, polling_command, value_type)
#define PIPSOLAR_BINARY_SENSOR(name, polling_command) \
PIPSOLAR_ENTITY_(binary_sensor::BinarySensor, name, polling_command)
#define PIPSOLAR_TEXT_SENSOR(name, polling_command) PIPSOLAR_ENTITY_(text_sensor::TextSensor, name, polling_command)
class Pipsolar : public uart::UARTDevice, public PollingComponent {
// QPIGS values
PIPSOLAR_SENSOR(grid_voltage, QPIGS, float)
PIPSOLAR_SENSOR(grid_frequency, QPIGS, float)
PIPSOLAR_SENSOR(ac_output_voltage, QPIGS, float)
PIPSOLAR_SENSOR(ac_output_frequency, QPIGS, float)
PIPSOLAR_SENSOR(ac_output_apparent_power, QPIGS, int)
PIPSOLAR_SENSOR(ac_output_active_power, QPIGS, int)
PIPSOLAR_SENSOR(output_load_percent, QPIGS, int)
PIPSOLAR_SENSOR(bus_voltage, QPIGS, int)
PIPSOLAR_SENSOR(battery_voltage, QPIGS, float)
PIPSOLAR_SENSOR(battery_charging_current, QPIGS, int)
PIPSOLAR_SENSOR(battery_capacity_percent, QPIGS, int)
PIPSOLAR_SENSOR(inverter_heat_sink_temperature, QPIGS, int)
PIPSOLAR_SENSOR(pv_input_current_for_battery, QPIGS, float)
PIPSOLAR_SENSOR(pv_input_voltage, QPIGS, float)
PIPSOLAR_SENSOR(battery_voltage_scc, QPIGS, float)
PIPSOLAR_SENSOR(battery_discharge_current, QPIGS, int)
PIPSOLAR_BINARY_SENSOR(add_sbu_priority_version, QPIGS, int)
PIPSOLAR_BINARY_SENSOR(configuration_status, QPIGS, int)
PIPSOLAR_BINARY_SENSOR(scc_firmware_version, QPIGS, int)
PIPSOLAR_BINARY_SENSOR(load_status, QPIGS, int)
PIPSOLAR_BINARY_SENSOR(battery_voltage_to_steady_while_charging, QPIGS, int)
PIPSOLAR_BINARY_SENSOR(charging_status, QPIGS, int)
PIPSOLAR_BINARY_SENSOR(scc_charging_status, QPIGS, int)
PIPSOLAR_BINARY_SENSOR(ac_charging_status, QPIGS, int)
PIPSOLAR_SENSOR(battery_voltage_offset_for_fans_on, QPIGS, int) //.1 scale
PIPSOLAR_SENSOR(eeprom_version, QPIGS, int)
PIPSOLAR_SENSOR(pv_charging_power, QPIGS, int)
PIPSOLAR_BINARY_SENSOR(charging_to_floating_mode, QPIGS, int)
PIPSOLAR_BINARY_SENSOR(switch_on, QPIGS, int)
PIPSOLAR_BINARY_SENSOR(dustproof_installed, QPIGS, int)
PIPSOLAR_SENSOR(grid_voltage, QPIGS)
PIPSOLAR_SENSOR(grid_frequency, QPIGS)
PIPSOLAR_SENSOR(ac_output_voltage, QPIGS)
PIPSOLAR_SENSOR(ac_output_frequency, QPIGS)
PIPSOLAR_SENSOR(ac_output_apparent_power, QPIGS)
PIPSOLAR_SENSOR(ac_output_active_power, QPIGS)
PIPSOLAR_SENSOR(output_load_percent, QPIGS)
PIPSOLAR_SENSOR(bus_voltage, QPIGS)
PIPSOLAR_SENSOR(battery_voltage, QPIGS)
PIPSOLAR_SENSOR(battery_charging_current, QPIGS)
PIPSOLAR_SENSOR(battery_capacity_percent, QPIGS)
PIPSOLAR_SENSOR(inverter_heat_sink_temperature, QPIGS)
PIPSOLAR_SENSOR(pv_input_current_for_battery, QPIGS)
PIPSOLAR_SENSOR(pv_input_voltage, QPIGS)
PIPSOLAR_SENSOR(battery_voltage_scc, QPIGS)
PIPSOLAR_SENSOR(battery_discharge_current, QPIGS)
PIPSOLAR_BINARY_SENSOR(add_sbu_priority_version, QPIGS)
PIPSOLAR_BINARY_SENSOR(configuration_status, QPIGS)
PIPSOLAR_BINARY_SENSOR(scc_firmware_version, QPIGS)
PIPSOLAR_BINARY_SENSOR(load_status, QPIGS)
PIPSOLAR_BINARY_SENSOR(battery_voltage_to_steady_while_charging, QPIGS)
PIPSOLAR_BINARY_SENSOR(charging_status, QPIGS)
PIPSOLAR_BINARY_SENSOR(scc_charging_status, QPIGS)
PIPSOLAR_BINARY_SENSOR(ac_charging_status, QPIGS)
PIPSOLAR_SENSOR(battery_voltage_offset_for_fans_on, QPIGS) //.1 scale
PIPSOLAR_SENSOR(eeprom_version, QPIGS)
PIPSOLAR_SENSOR(pv_charging_power, QPIGS)
PIPSOLAR_BINARY_SENSOR(charging_to_floating_mode, QPIGS)
PIPSOLAR_BINARY_SENSOR(switch_on, QPIGS)
PIPSOLAR_BINARY_SENSOR(dustproof_installed, QPIGS)
// QPIRI values
PIPSOLAR_SENSOR(grid_rating_voltage, QPIRI, float)
PIPSOLAR_SENSOR(grid_rating_current, QPIRI, float)
PIPSOLAR_SENSOR(ac_output_rating_voltage, QPIRI, float)
PIPSOLAR_SENSOR(ac_output_rating_frequency, QPIRI, float)
PIPSOLAR_SENSOR(ac_output_rating_current, QPIRI, float)
PIPSOLAR_SENSOR(ac_output_rating_apparent_power, QPIRI, int)
PIPSOLAR_SENSOR(ac_output_rating_active_power, QPIRI, int)
PIPSOLAR_SENSOR(battery_rating_voltage, QPIRI, float)
PIPSOLAR_SENSOR(battery_recharge_voltage, QPIRI, float)
PIPSOLAR_SENSOR(battery_under_voltage, QPIRI, float)
PIPSOLAR_SENSOR(battery_bulk_voltage, QPIRI, float)
PIPSOLAR_SENSOR(battery_float_voltage, QPIRI, float)
PIPSOLAR_SENSOR(battery_type, QPIRI, int)
PIPSOLAR_SENSOR(current_max_ac_charging_current, QPIRI, int)
PIPSOLAR_SENSOR(current_max_charging_current, QPIRI, int)
PIPSOLAR_SENSOR(input_voltage_range, QPIRI, int)
PIPSOLAR_SENSOR(output_source_priority, QPIRI, int)
PIPSOLAR_SENSOR(charger_source_priority, QPIRI, int)
PIPSOLAR_SENSOR(parallel_max_num, QPIRI, int)
PIPSOLAR_SENSOR(machine_type, QPIRI, int)
PIPSOLAR_SENSOR(topology, QPIRI, int)
PIPSOLAR_SENSOR(output_mode, QPIRI, int)
PIPSOLAR_SENSOR(battery_redischarge_voltage, QPIRI, float)
PIPSOLAR_SENSOR(pv_ok_condition_for_parallel, QPIRI, int)
PIPSOLAR_SENSOR(pv_power_balance, QPIRI, int)
PIPSOLAR_SENSOR(grid_rating_voltage, QPIRI)
PIPSOLAR_SENSOR(grid_rating_current, QPIRI)
PIPSOLAR_SENSOR(ac_output_rating_voltage, QPIRI)
PIPSOLAR_SENSOR(ac_output_rating_frequency, QPIRI)
PIPSOLAR_SENSOR(ac_output_rating_current, QPIRI)
PIPSOLAR_SENSOR(ac_output_rating_apparent_power, QPIRI)
PIPSOLAR_SENSOR(ac_output_rating_active_power, QPIRI)
PIPSOLAR_SENSOR(battery_rating_voltage, QPIRI)
PIPSOLAR_SENSOR(battery_recharge_voltage, QPIRI)
PIPSOLAR_SENSOR(battery_under_voltage, QPIRI)
PIPSOLAR_SENSOR(battery_bulk_voltage, QPIRI)
PIPSOLAR_SENSOR(battery_float_voltage, QPIRI)
PIPSOLAR_SENSOR(battery_type, QPIRI)
PIPSOLAR_SENSOR(current_max_ac_charging_current, QPIRI)
PIPSOLAR_SENSOR(current_max_charging_current, QPIRI)
PIPSOLAR_SENSOR(input_voltage_range, QPIRI)
PIPSOLAR_SENSOR(output_source_priority, QPIRI)
PIPSOLAR_SENSOR(charger_source_priority, QPIRI)
PIPSOLAR_SENSOR(parallel_max_num, QPIRI)
PIPSOLAR_SENSOR(machine_type, QPIRI)
PIPSOLAR_SENSOR(topology, QPIRI)
PIPSOLAR_SENSOR(output_mode, QPIRI)
PIPSOLAR_SENSOR(battery_redischarge_voltage, QPIRI)
PIPSOLAR_SENSOR(pv_ok_condition_for_parallel, QPIRI)
PIPSOLAR_SENSOR(pv_power_balance, QPIRI)
// QMOD values
PIPSOLAR_VALUED_TEXT_SENSOR(device_mode, QMOD, char)
PIPSOLAR_TEXT_SENSOR(device_mode, QMOD)
// QFLAG values
PIPSOLAR_BINARY_SENSOR(silence_buzzer_open_buzzer, QFLAG, int)
PIPSOLAR_BINARY_SENSOR(overload_bypass_function, QFLAG, int)
PIPSOLAR_BINARY_SENSOR(lcd_escape_to_default, QFLAG, int)
PIPSOLAR_BINARY_SENSOR(overload_restart_function, QFLAG, int)
PIPSOLAR_BINARY_SENSOR(over_temperature_restart_function, QFLAG, int)
PIPSOLAR_BINARY_SENSOR(backlight_on, QFLAG, int)
PIPSOLAR_BINARY_SENSOR(alarm_on_when_primary_source_interrupt, QFLAG, int)
PIPSOLAR_BINARY_SENSOR(fault_code_record, QFLAG, int)
PIPSOLAR_BINARY_SENSOR(power_saving, QFLAG, int)
PIPSOLAR_BINARY_SENSOR(silence_buzzer_open_buzzer, QFLAG)
PIPSOLAR_BINARY_SENSOR(overload_bypass_function, QFLAG)
PIPSOLAR_BINARY_SENSOR(lcd_escape_to_default, QFLAG)
PIPSOLAR_BINARY_SENSOR(overload_restart_function, QFLAG)
PIPSOLAR_BINARY_SENSOR(over_temperature_restart_function, QFLAG)
PIPSOLAR_BINARY_SENSOR(backlight_on, QFLAG)
PIPSOLAR_BINARY_SENSOR(alarm_on_when_primary_source_interrupt, QFLAG)
PIPSOLAR_BINARY_SENSOR(fault_code_record, QFLAG)
PIPSOLAR_BINARY_SENSOR(power_saving, QFLAG)
// QPIWS values
PIPSOLAR_BINARY_SENSOR(warnings_present, QPIWS, bool)
PIPSOLAR_BINARY_SENSOR(faults_present, QPIWS, bool)
PIPSOLAR_BINARY_SENSOR(warning_power_loss, QPIWS, bool)
PIPSOLAR_BINARY_SENSOR(fault_inverter_fault, QPIWS, bool)
PIPSOLAR_BINARY_SENSOR(fault_bus_over, QPIWS, bool)
PIPSOLAR_BINARY_SENSOR(fault_bus_under, QPIWS, bool)
PIPSOLAR_BINARY_SENSOR(fault_bus_soft_fail, QPIWS, bool)
PIPSOLAR_BINARY_SENSOR(warning_line_fail, QPIWS, bool)
PIPSOLAR_BINARY_SENSOR(fault_opvshort, QPIWS, bool)
PIPSOLAR_BINARY_SENSOR(fault_inverter_voltage_too_low, QPIWS, bool)
PIPSOLAR_BINARY_SENSOR(fault_inverter_voltage_too_high, QPIWS, bool)
PIPSOLAR_BINARY_SENSOR(warning_over_temperature, QPIWS, bool)
PIPSOLAR_BINARY_SENSOR(warning_fan_lock, QPIWS, bool)
PIPSOLAR_BINARY_SENSOR(warning_battery_voltage_high, QPIWS, bool)
PIPSOLAR_BINARY_SENSOR(warning_battery_low_alarm, QPIWS, bool)
PIPSOLAR_BINARY_SENSOR(warning_battery_under_shutdown, QPIWS, bool)
PIPSOLAR_BINARY_SENSOR(warning_battery_derating, QPIWS, bool)
PIPSOLAR_BINARY_SENSOR(warning_over_load, QPIWS, bool)
PIPSOLAR_BINARY_SENSOR(warning_eeprom_failed, QPIWS, bool)
PIPSOLAR_BINARY_SENSOR(fault_inverter_over_current, QPIWS, bool)
PIPSOLAR_BINARY_SENSOR(fault_inverter_soft_failed, QPIWS, bool)
PIPSOLAR_BINARY_SENSOR(fault_self_test_failed, QPIWS, bool)
PIPSOLAR_BINARY_SENSOR(fault_op_dc_voltage_over, QPIWS, bool)
PIPSOLAR_BINARY_SENSOR(fault_battery_open, QPIWS, bool)
PIPSOLAR_BINARY_SENSOR(fault_current_sensor_failed, QPIWS, bool)
PIPSOLAR_BINARY_SENSOR(fault_battery_short, QPIWS, bool)
PIPSOLAR_BINARY_SENSOR(warning_power_limit, QPIWS, bool)
PIPSOLAR_BINARY_SENSOR(warning_pv_voltage_high, QPIWS, bool)
PIPSOLAR_BINARY_SENSOR(fault_mppt_overload, QPIWS, bool)
PIPSOLAR_BINARY_SENSOR(warning_mppt_overload, QPIWS, bool)
PIPSOLAR_BINARY_SENSOR(warning_battery_too_low_to_charge, QPIWS, bool)
PIPSOLAR_BINARY_SENSOR(fault_dc_dc_over_current, QPIWS, bool)
PIPSOLAR_BINARY_SENSOR(fault_code, QPIWS, int)
PIPSOLAR_BINARY_SENSOR(warnung_low_pv_energy, QPIWS, bool)
PIPSOLAR_BINARY_SENSOR(warning_high_ac_input_during_bus_soft_start, QPIWS, bool)
PIPSOLAR_BINARY_SENSOR(warning_battery_equalization, QPIWS, bool)
PIPSOLAR_BINARY_SENSOR(warnings_present, QPIWS)
PIPSOLAR_BINARY_SENSOR(faults_present, QPIWS)
PIPSOLAR_BINARY_SENSOR(warning_power_loss, QPIWS)
PIPSOLAR_BINARY_SENSOR(fault_inverter_fault, QPIWS)
PIPSOLAR_BINARY_SENSOR(fault_bus_over, QPIWS)
PIPSOLAR_BINARY_SENSOR(fault_bus_under, QPIWS)
PIPSOLAR_BINARY_SENSOR(fault_bus_soft_fail, QPIWS)
PIPSOLAR_BINARY_SENSOR(warning_line_fail, QPIWS)
PIPSOLAR_BINARY_SENSOR(fault_opvshort, QPIWS)
PIPSOLAR_BINARY_SENSOR(fault_inverter_voltage_too_low, QPIWS)
PIPSOLAR_BINARY_SENSOR(fault_inverter_voltage_too_high, QPIWS)
PIPSOLAR_BINARY_SENSOR(warning_over_temperature, QPIWS)
PIPSOLAR_BINARY_SENSOR(warning_fan_lock, QPIWS)
PIPSOLAR_BINARY_SENSOR(warning_battery_voltage_high, QPIWS)
PIPSOLAR_BINARY_SENSOR(warning_battery_low_alarm, QPIWS)
PIPSOLAR_BINARY_SENSOR(warning_battery_under_shutdown, QPIWS)
PIPSOLAR_BINARY_SENSOR(warning_battery_derating, QPIWS)
PIPSOLAR_BINARY_SENSOR(warning_over_load, QPIWS)
PIPSOLAR_BINARY_SENSOR(warning_eeprom_failed, QPIWS)
PIPSOLAR_BINARY_SENSOR(fault_inverter_over_current, QPIWS)
PIPSOLAR_BINARY_SENSOR(fault_inverter_soft_failed, QPIWS)
PIPSOLAR_BINARY_SENSOR(fault_self_test_failed, QPIWS)
PIPSOLAR_BINARY_SENSOR(fault_op_dc_voltage_over, QPIWS)
PIPSOLAR_BINARY_SENSOR(fault_battery_open, QPIWS)
PIPSOLAR_BINARY_SENSOR(fault_current_sensor_failed, QPIWS)
PIPSOLAR_BINARY_SENSOR(fault_battery_short, QPIWS)
PIPSOLAR_BINARY_SENSOR(warning_power_limit, QPIWS)
PIPSOLAR_BINARY_SENSOR(warning_pv_voltage_high, QPIWS)
PIPSOLAR_BINARY_SENSOR(fault_mppt_overload, QPIWS)
PIPSOLAR_BINARY_SENSOR(warning_mppt_overload, QPIWS)
PIPSOLAR_BINARY_SENSOR(warning_battery_too_low_to_charge, QPIWS)
PIPSOLAR_BINARY_SENSOR(fault_dc_dc_over_current, QPIWS)
PIPSOLAR_BINARY_SENSOR(fault_code, QPIWS)
PIPSOLAR_BINARY_SENSOR(warning_low_pv_energy, QPIWS)
PIPSOLAR_BINARY_SENSOR(warning_high_ac_input_during_bus_soft_start, QPIWS)
PIPSOLAR_BINARY_SENSOR(warning_battery_equalization, QPIWS)
PIPSOLAR_TEXT_SENSOR(last_qpigs, QPIGS)
PIPSOLAR_TEXT_SENSOR(last_qpiri, QPIRI)
@@ -180,14 +185,14 @@ class Pipsolar : public uart::UARTDevice, public PollingComponent {
PIPSOLAR_SWITCH(pv_ok_condition_for_parallel_switch, QPIRI)
PIPSOLAR_SWITCH(pv_power_balance_switch, QPIRI)
void switch_command(const std::string &command);
void queue_command(const std::string &command);
void setup() override;
void loop() override;
void dump_config() override;
void update() override;
protected:
static const size_t PIPSOLAR_READ_BUFFER_LENGTH = 110; // maximum supported answer length
static const size_t PIPSOLAR_READ_BUFFER_LENGTH = 128; // maximum supported answer length
static const size_t COMMAND_QUEUE_LENGTH = 10;
static const size_t COMMAND_TIMEOUT = 5000;
static const size_t POLLING_COMMANDS_MAX = 15;
@@ -198,7 +203,26 @@ class Pipsolar : public uart::UARTDevice, public PollingComponent {
uint16_t pipsolar_crc_(uint8_t *msg, uint8_t len);
bool send_next_command_();
bool send_next_poll_();
void queue_command_(const char *command, uint8_t length);
void handle_qpiri_(const char *message);
void handle_qpigs_(const char *message);
void handle_qmod_(const char *message);
void handle_qflag_(const char *message);
void handle_qpiws_(const char *message);
void handle_qt_(const char *message);
void handle_qmn_(const char *message);
void skip_start_(const char *message, size_t *pos);
void skip_field_(const char *message, size_t *pos);
std::string read_field_(const char *message, size_t *pos);
void read_float_sensor_(const char *message, size_t *pos, sensor::Sensor *sensor);
void read_int_sensor_(const char *message, size_t *pos, sensor::Sensor *sensor);
void publish_binary_sensor_(esphome::optional<bool> b, binary_sensor::BinarySensor *sensor);
esphome::optional<bool> get_bit_(std::string bits, uint8_t bit_pos);
std::string command_queue_[COMMAND_QUEUE_LENGTH];
uint8_t command_queue_position_ = 0;
uint8_t read_buffer_[PIPSOLAR_READ_BUFFER_LENGTH];
@@ -213,11 +237,10 @@ class Pipsolar : public uart::UARTDevice, public PollingComponent {
STATE_POLL_COMPLETE = 3,
STATE_COMMAND_COMPLETE = 4,
STATE_POLL_CHECKED = 5,
STATE_POLL_DECODED = 6,
};
uint8_t last_polling_command_ = 0;
PollingCommand used_polling_commands_[POLLING_COMMANDS_MAX];
PollingCommand enabled_polling_commands_[POLLING_COMMANDS_MAX];
};
} // namespace pipsolar

View File

@@ -11,11 +11,11 @@ void PipsolarSwitch::dump_config() { LOG_SWITCH("", "Pipsolar Switch", this); }
void PipsolarSwitch::write_state(bool state) {
if (state) {
if (!this->on_command_.empty()) {
this->parent_->switch_command(this->on_command_);
this->parent_->queue_command(this->on_command_);
}
} else {
if (!this->off_command_.empty()) {
this->parent_->switch_command(this->off_command_);
this->parent_->queue_command(this->off_command_);
}
}
}

View File

@@ -916,7 +916,7 @@ void PrometheusHandler::climate_row_(AsyncResponseStream *stream, climate::Clima
auto min_temp_value = value_accuracy_to_string(traits.get_visual_min_temperature(), target_accuracy);
climate_value_row_(stream, obj, area, node, friendly_name, min_temp, min_temp_value);
// now check optional traits
if (traits.get_supports_current_temperature()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) {
std::string current_temp = "current_temperature";
if (std::isnan(obj->current_temperature)) {
climate_failed_row_(stream, obj, area, node, friendly_name, current_temp, true);
@@ -927,7 +927,7 @@ void PrometheusHandler::climate_row_(AsyncResponseStream *stream, climate::Clima
climate_failed_row_(stream, obj, area, node, friendly_name, current_temp, false);
}
}
if (traits.get_supports_current_humidity()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) {
std::string current_humidity = "current_humidity";
if (std::isnan(obj->current_humidity)) {
climate_failed_row_(stream, obj, area, node, friendly_name, current_humidity, true);
@@ -938,7 +938,7 @@ void PrometheusHandler::climate_row_(AsyncResponseStream *stream, climate::Clima
climate_failed_row_(stream, obj, area, node, friendly_name, current_humidity, false);
}
}
if (traits.get_supports_target_humidity()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
std::string target_humidity = "target_humidity";
if (std::isnan(obj->target_humidity)) {
climate_failed_row_(stream, obj, area, node, friendly_name, target_humidity, true);
@@ -949,7 +949,8 @@ void PrometheusHandler::climate_row_(AsyncResponseStream *stream, climate::Clima
climate_failed_row_(stream, obj, area, node, friendly_name, target_humidity, false);
}
}
if (traits.get_supports_two_point_target_temperature()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
std::string target_temp_low = "target_temperature_low";
auto target_temp_low_value = value_accuracy_to_string(obj->target_temperature_low, target_accuracy);
climate_value_row_(stream, obj, area, node, friendly_name, target_temp_low, target_temp_low_value);
@@ -961,7 +962,7 @@ void PrometheusHandler::climate_row_(AsyncResponseStream *stream, climate::Clima
auto target_temp_value = value_accuracy_to_string(obj->target_temperature, target_accuracy);
climate_value_row_(stream, obj, area, node, friendly_name, target_temp, target_temp_value);
}
if (traits.get_supports_action()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) {
std::string climate_trait_category = "action";
const auto *climate_trait_value = climate::climate_action_to_string(obj->action);
climate_setting_row_(stream, obj, area, node, friendly_name, climate_trait_category, climate_trait_value);

View File

@@ -81,7 +81,7 @@ CONFIG_SCHEMA = (
cv.int_range(min=0, max=0xFFFF, max_included=False),
),
cv.Optional(CONF_AMBIENT_PRESSURE_COMPENSATION): cv.pressure,
cv.Optional(CONF_TEMPERATURE_OFFSET, default="4°C"): cv.temperature,
cv.Optional(CONF_TEMPERATURE_OFFSET, default="4°C"): cv.temperature_delta,
cv.Optional(CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE): cv.use_id(
sensor.Sensor
),

View File

@@ -45,13 +45,26 @@ def get_script(script_id):
def check_max_runs(value):
# Set default for queued mode to prevent unbounded queue growth
if CONF_MAX_RUNS not in value and value[CONF_MODE] == CONF_QUEUED:
value[CONF_MAX_RUNS] = 5
if CONF_MAX_RUNS not in value:
return value
if value[CONF_MODE] not in [CONF_QUEUED, CONF_PARALLEL]:
raise cv.Invalid(
"The option 'max_runs' is only valid in 'queue' and 'parallel' mode.",
"The option 'max_runs' is only valid in 'queued' and 'parallel' mode.",
path=[CONF_MAX_RUNS],
)
# Queued mode must have bounded queue (min 1), parallel mode can be unlimited (0)
if value[CONF_MODE] == CONF_QUEUED and value[CONF_MAX_RUNS] < 1:
raise cv.Invalid(
"The option 'max_runs' must be at least 1 for queued mode.",
path=[CONF_MAX_RUNS],
)
return value
@@ -106,7 +119,7 @@ CONFIG_SCHEMA = automation.validate_automation(
cv.Optional(CONF_MODE, default=CONF_SINGLE): cv.one_of(
*SCRIPT_MODES, lower=True
),
cv.Optional(CONF_MAX_RUNS): cv.positive_int,
cv.Optional(CONF_MAX_RUNS): cv.int_range(min=0, max=100),
cv.Optional(CONF_PARAMETERS, default={}): cv.Schema(
{
validate_parameter_name: validate_parameter_type,

View File

@@ -1,10 +1,11 @@
#pragma once
#include <memory>
#include <tuple>
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <queue>
namespace esphome {
namespace script {
@@ -96,23 +97,41 @@ template<typename... Ts> class RestartScript : public Script<Ts...> {
/** A script type that queues new instances that are created.
*
* Only one instance of the script can be active at a time.
*
* Ring buffer implementation:
* - num_queued_ tracks the number of queued (waiting) instances, NOT including the currently running one
* - queue_front_ points to the next item to execute (read position)
* - Buffer size is max_runs_ - 1 (max total instances minus the running one)
* - Write position is calculated as: (queue_front_ + num_queued_) % (max_runs_ - 1)
* - When an item finishes, queue_front_ advances: (queue_front_ + 1) % (max_runs_ - 1)
* - First execute() runs immediately without queuing (num_queued_ stays 0)
* - Subsequent executes while running are queued starting at position 0
* - Maximum total instances = max_runs_ (includes 1 running + (max_runs_ - 1) queued)
*/
template<typename... Ts> class QueueingScript : public Script<Ts...>, public Component {
public:
void execute(Ts... x) override {
if (this->is_action_running() || this->num_runs_ > 0) {
// num_runs_ is the number of *queued* instances, so total number of instances is
// num_runs_ + 1
if (this->max_runs_ != 0 && this->num_runs_ + 1 >= this->max_runs_) {
this->esp_logw_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' maximum number of queued runs exceeded!"),
if (this->is_action_running() || this->num_queued_ > 0) {
// num_queued_ is the number of *queued* instances (waiting, not including currently running)
// max_runs_ is the maximum *total* instances (running + queued)
// So we reject when num_queued_ + 1 >= max_runs_ (queued + running >= max)
if (this->num_queued_ + 1 >= this->max_runs_) {
this->esp_logw_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' max instances (running + queued) reached!"),
LOG_STR_ARG(this->name_));
return;
}
// Initialize queue on first queued item (after capacity check)
this->lazy_init_queue_();
this->esp_logd_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' queueing new instance (mode: queued)"),
LOG_STR_ARG(this->name_));
this->num_runs_++;
this->var_queue_.push(std::make_tuple(x...));
// Ring buffer: write to (queue_front_ + num_queued_) % queue_capacity
const size_t queue_capacity = static_cast<size_t>(this->max_runs_ - 1);
size_t write_pos = (this->queue_front_ + this->num_queued_) % queue_capacity;
// Use std::make_unique to replace the unique_ptr
this->var_queue_[write_pos] = std::make_unique<std::tuple<Ts...>>(x...);
this->num_queued_++;
return;
}
@@ -122,29 +141,46 @@ template<typename... Ts> class QueueingScript : public Script<Ts...>, public Com
}
void stop() override {
this->num_runs_ = 0;
// Clear all queued items to free memory immediately
// Resetting the array automatically destroys all unique_ptrs and their contents
this->var_queue_.reset();
this->num_queued_ = 0;
this->queue_front_ = 0;
Script<Ts...>::stop();
}
void loop() override {
if (this->num_runs_ != 0 && !this->is_action_running()) {
this->num_runs_--;
auto &vars = this->var_queue_.front();
this->var_queue_.pop();
this->trigger_tuple_(vars, typename gens<sizeof...(Ts)>::type());
if (this->num_queued_ != 0 && !this->is_action_running()) {
// Dequeue: decrement count, move tuple out (frees slot), advance read position
this->num_queued_--;
const size_t queue_capacity = static_cast<size_t>(this->max_runs_ - 1);
auto tuple_ptr = std::move(this->var_queue_[this->queue_front_]);
this->queue_front_ = (this->queue_front_ + 1) % queue_capacity;
this->trigger_tuple_(*tuple_ptr, typename gens<sizeof...(Ts)>::type());
}
}
void set_max_runs(int max_runs) { max_runs_ = max_runs; }
protected:
// Lazy init queue on first use - avoids setup() ordering issues and saves memory
// if script is never executed during this boot cycle
inline void lazy_init_queue_() {
if (!this->var_queue_) {
// Allocate array of max_runs_ - 1 slots for queued items (running item is separate)
// unique_ptr array is zero-initialized, so all slots start as nullptr
this->var_queue_ = std::make_unique<std::unique_ptr<std::tuple<Ts...>>[]>(this->max_runs_ - 1);
}
}
template<int... S> void trigger_tuple_(const std::tuple<Ts...> &tuple, seq<S...> /*unused*/) {
this->trigger(std::get<S>(tuple)...);
}
int num_runs_ = 0;
int max_runs_ = 0;
std::queue<std::tuple<Ts...>> var_queue_;
int num_queued_ = 0; // Number of queued instances (not including currently running)
int max_runs_ = 0; // Maximum total instances (running + queued)
size_t queue_front_ = 0; // Ring buffer read position (next item to execute)
std::unique_ptr<std::unique_ptr<std::tuple<Ts...>>[]> var_queue_; // Ring buffer of queued parameters
};
/** A script type that executes new instances in parallel.

View File

@@ -28,6 +28,8 @@ from esphome.const import (
CONF_ON_RAW_VALUE,
CONF_ON_VALUE,
CONF_ON_VALUE_RANGE,
CONF_OPTIMISTIC,
CONF_PERIOD,
CONF_QUANTILE,
CONF_SEND_EVERY,
CONF_SEND_FIRST_AT,
@@ -251,6 +253,9 @@ MaxFilter = sensor_ns.class_("MaxFilter", Filter)
SlidingWindowMovingAverageFilter = sensor_ns.class_(
"SlidingWindowMovingAverageFilter", Filter
)
StreamingMinFilter = sensor_ns.class_("StreamingMinFilter", Filter)
StreamingMaxFilter = sensor_ns.class_("StreamingMaxFilter", Filter)
StreamingMovingAverageFilter = sensor_ns.class_("StreamingMovingAverageFilter", Filter)
ExponentialMovingAverageFilter = sensor_ns.class_(
"ExponentialMovingAverageFilter", Filter
)
@@ -258,9 +263,12 @@ ThrottleAverageFilter = sensor_ns.class_("ThrottleAverageFilter", Filter, cg.Com
LambdaFilter = sensor_ns.class_("LambdaFilter", Filter)
OffsetFilter = sensor_ns.class_("OffsetFilter", Filter)
MultiplyFilter = sensor_ns.class_("MultiplyFilter", Filter)
FilterOutValueFilter = sensor_ns.class_("FilterOutValueFilter", Filter)
ValueListFilter = sensor_ns.class_("ValueListFilter", Filter)
FilterOutValueFilter = sensor_ns.class_("FilterOutValueFilter", ValueListFilter)
ThrottleFilter = sensor_ns.class_("ThrottleFilter", Filter)
ThrottleWithPriorityFilter = sensor_ns.class_("ThrottleWithPriorityFilter", Filter)
ThrottleWithPriorityFilter = sensor_ns.class_(
"ThrottleWithPriorityFilter", ValueListFilter
)
TimeoutFilter = sensor_ns.class_("TimeoutFilter", Filter, cg.Component)
DebounceFilter = sensor_ns.class_("DebounceFilter", Filter, cg.Component)
HeartbeatFilter = sensor_ns.class_("HeartbeatFilter", Filter, cg.Component)
@@ -452,14 +460,21 @@ async def skip_initial_filter_to_code(config, filter_id):
return cg.new_Pvariable(filter_id, config)
@FILTER_REGISTRY.register("min", MinFilter, MIN_SCHEMA)
@FILTER_REGISTRY.register("min", Filter, MIN_SCHEMA)
async def min_filter_to_code(config, filter_id):
return cg.new_Pvariable(
filter_id,
config[CONF_WINDOW_SIZE],
config[CONF_SEND_EVERY],
config[CONF_SEND_FIRST_AT],
)
window_size: int = config[CONF_WINDOW_SIZE]
send_every: int = config[CONF_SEND_EVERY]
send_first_at: int = config[CONF_SEND_FIRST_AT]
# Optimization: Use streaming filter for batch windows (window_size == send_every)
# Saves 99.98% memory for large windows (e.g., 20KB → 4 bytes for window_size=5000)
if window_size == send_every:
# Use streaming filter - O(1) memory instead of O(n)
rhs = StreamingMinFilter.new(window_size, send_first_at)
return cg.Pvariable(filter_id, rhs, StreamingMinFilter)
# Use sliding window filter - maintains ring buffer
rhs = MinFilter.new(window_size, send_every, send_first_at)
return cg.Pvariable(filter_id, rhs, MinFilter)
MAX_SCHEMA = cv.All(
@@ -474,14 +489,18 @@ MAX_SCHEMA = cv.All(
)
@FILTER_REGISTRY.register("max", MaxFilter, MAX_SCHEMA)
@FILTER_REGISTRY.register("max", Filter, MAX_SCHEMA)
async def max_filter_to_code(config, filter_id):
return cg.new_Pvariable(
filter_id,
config[CONF_WINDOW_SIZE],
config[CONF_SEND_EVERY],
config[CONF_SEND_FIRST_AT],
)
window_size: int = config[CONF_WINDOW_SIZE]
send_every: int = config[CONF_SEND_EVERY]
send_first_at: int = config[CONF_SEND_FIRST_AT]
# Optimization: Use streaming filter for batch windows (window_size == send_every)
if window_size == send_every:
rhs = StreamingMaxFilter.new(window_size, send_first_at)
return cg.Pvariable(filter_id, rhs, StreamingMaxFilter)
rhs = MaxFilter.new(window_size, send_every, send_first_at)
return cg.Pvariable(filter_id, rhs, MaxFilter)
SLIDING_AVERAGE_SCHEMA = cv.All(
@@ -498,16 +517,20 @@ SLIDING_AVERAGE_SCHEMA = cv.All(
@FILTER_REGISTRY.register(
"sliding_window_moving_average",
SlidingWindowMovingAverageFilter,
Filter,
SLIDING_AVERAGE_SCHEMA,
)
async def sliding_window_moving_average_filter_to_code(config, filter_id):
return cg.new_Pvariable(
filter_id,
config[CONF_WINDOW_SIZE],
config[CONF_SEND_EVERY],
config[CONF_SEND_FIRST_AT],
)
window_size: int = config[CONF_WINDOW_SIZE]
send_every: int = config[CONF_SEND_EVERY]
send_first_at: int = config[CONF_SEND_FIRST_AT]
# Optimization: Use streaming filter for batch windows (window_size == send_every)
if window_size == send_every:
rhs = StreamingMovingAverageFilter.new(window_size, send_first_at)
return cg.Pvariable(filter_id, rhs, StreamingMovingAverageFilter)
rhs = SlidingWindowMovingAverageFilter.new(window_size, send_every, send_first_at)
return cg.Pvariable(filter_id, rhs, SlidingWindowMovingAverageFilter)
EXPONENTIAL_AVERAGE_SCHEMA = cv.All(
@@ -623,10 +646,29 @@ async def throttle_with_priority_filter_to_code(config, filter_id):
return cg.new_Pvariable(filter_id, config[CONF_TIMEOUT], template_)
HEARTBEAT_SCHEMA = cv.Schema(
{
cv.Required(CONF_PERIOD): cv.positive_time_period_milliseconds,
cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean,
}
)
@FILTER_REGISTRY.register(
"heartbeat", HeartbeatFilter, cv.positive_time_period_milliseconds
"heartbeat",
HeartbeatFilter,
cv.Any(
cv.positive_time_period_milliseconds,
HEARTBEAT_SCHEMA,
),
)
async def heartbeat_filter_to_code(config, filter_id):
if isinstance(config, dict):
var = cg.new_Pvariable(filter_id, config[CONF_PERIOD])
await cg.register_component(var, {})
cg.add(var.set_optimistic(config[CONF_OPTIMISTIC]))
return var
var = cg.new_Pvariable(filter_id, config)
await cg.register_component(var, {})
return var

View File

@@ -32,50 +32,76 @@ void Filter::initialize(Sensor *parent, Filter *next) {
this->next_ = next;
}
// MedianFilter
MedianFilter::MedianFilter(size_t window_size, size_t send_every, size_t send_first_at)
: send_every_(send_every), send_at_(send_every - send_first_at), window_size_(window_size) {}
void MedianFilter::set_send_every(size_t send_every) { this->send_every_ = send_every; }
void MedianFilter::set_window_size(size_t window_size) { this->window_size_ = window_size; }
optional<float> MedianFilter::new_value(float value) {
while (this->queue_.size() >= this->window_size_) {
this->queue_.pop_front();
}
this->queue_.push_back(value);
ESP_LOGVV(TAG, "MedianFilter(%p)::new_value(%f)", this, value);
// SlidingWindowFilter
SlidingWindowFilter::SlidingWindowFilter(size_t window_size, size_t send_every, size_t send_first_at)
: window_size_(window_size), send_every_(send_every), send_at_(send_every - send_first_at) {
// Allocate ring buffer once at initialization
this->window_.init(window_size);
}
optional<float> SlidingWindowFilter::new_value(float value) {
// Add value to ring buffer
if (this->window_count_ < this->window_size_) {
// Buffer not yet full - just append
this->window_.push_back(value);
this->window_count_++;
} else {
// Buffer full - overwrite oldest value (ring buffer)
this->window_[this->window_head_] = value;
this->window_head_++;
if (this->window_head_ >= this->window_size_) {
this->window_head_ = 0;
}
}
// Check if we should send a result
if (++this->send_at_ >= this->send_every_) {
this->send_at_ = 0;
float median = NAN;
if (!this->queue_.empty()) {
// Copy queue without NaN values
std::vector<float> median_queue;
median_queue.reserve(this->queue_.size());
for (auto v : this->queue_) {
if (!std::isnan(v)) {
median_queue.push_back(v);
}
}
sort(median_queue.begin(), median_queue.end());
size_t queue_size = median_queue.size();
if (queue_size) {
if (queue_size % 2) {
median = median_queue[queue_size / 2];
} else {
median = (median_queue[queue_size / 2] + median_queue[(queue_size / 2) - 1]) / 2.0f;
}
}
}
ESP_LOGVV(TAG, "MedianFilter(%p)::new_value(%f) SENDING %f", this, value, median);
return median;
float result = this->compute_result();
ESP_LOGVV(TAG, "SlidingWindowFilter(%p)::new_value(%f) SENDING %f", this, value, result);
return result;
}
return {};
}
// SortedWindowFilter
FixedVector<float> SortedWindowFilter::get_window_values_() {
// Copy window without NaN values using FixedVector (no heap allocation)
// Returns unsorted values - caller will use std::nth_element for partial sorting as needed
FixedVector<float> values;
values.init(this->window_count_);
for (size_t i = 0; i < this->window_count_; i++) {
float v = this->window_[i];
if (!std::isnan(v)) {
values.push_back(v);
}
}
return values;
}
// MedianFilter
float MedianFilter::compute_result() {
FixedVector<float> values = this->get_window_values_();
if (values.empty())
return NAN;
size_t size = values.size();
size_t mid = size / 2;
if (size % 2) {
// Odd number of elements - use nth_element to find middle element
std::nth_element(values.begin(), values.begin() + mid, values.end());
return values[mid];
}
// Even number of elements - need both middle elements
// Use nth_element to find upper middle element
std::nth_element(values.begin(), values.begin() + mid, values.end());
float upper = values[mid];
// Find the maximum of the lower half (which is now everything before mid)
float lower = *std::max_element(values.begin(), values.begin() + mid);
return (lower + upper) / 2.0f;
}
// SkipInitialFilter
SkipInitialFilter::SkipInitialFilter(size_t num_to_ignore) : num_to_ignore_(num_to_ignore) {}
optional<float> SkipInitialFilter::new_value(float value) {
@@ -91,136 +117,39 @@ optional<float> SkipInitialFilter::new_value(float value) {
// QuantileFilter
QuantileFilter::QuantileFilter(size_t window_size, size_t send_every, size_t send_first_at, float quantile)
: send_every_(send_every), send_at_(send_every - send_first_at), window_size_(window_size), quantile_(quantile) {}
void QuantileFilter::set_send_every(size_t send_every) { this->send_every_ = send_every; }
void QuantileFilter::set_window_size(size_t window_size) { this->window_size_ = window_size; }
void QuantileFilter::set_quantile(float quantile) { this->quantile_ = quantile; }
optional<float> QuantileFilter::new_value(float value) {
while (this->queue_.size() >= this->window_size_) {
this->queue_.pop_front();
}
this->queue_.push_back(value);
ESP_LOGVV(TAG, "QuantileFilter(%p)::new_value(%f), quantile:%f", this, value, this->quantile_);
: SortedWindowFilter(window_size, send_every, send_first_at), quantile_(quantile) {}
if (++this->send_at_ >= this->send_every_) {
this->send_at_ = 0;
float QuantileFilter::compute_result() {
FixedVector<float> values = this->get_window_values_();
if (values.empty())
return NAN;
float result = NAN;
if (!this->queue_.empty()) {
// Copy queue without NaN values
std::vector<float> quantile_queue;
for (auto v : this->queue_) {
if (!std::isnan(v)) {
quantile_queue.push_back(v);
}
}
size_t position = ceilf(values.size() * this->quantile_) - 1;
ESP_LOGVV(TAG, "QuantileFilter(%p)::position: %zu/%zu", this, position + 1, values.size());
sort(quantile_queue.begin(), quantile_queue.end());
size_t queue_size = quantile_queue.size();
if (queue_size) {
size_t position = ceilf(queue_size * this->quantile_) - 1;
ESP_LOGVV(TAG, "QuantileFilter(%p)::position: %zu/%zu", this, position + 1, queue_size);
result = quantile_queue[position];
}
}
ESP_LOGVV(TAG, "QuantileFilter(%p)::new_value(%f) SENDING %f", this, value, result);
return result;
}
return {};
// Use nth_element to find the quantile element (O(n) instead of O(n log n))
std::nth_element(values.begin(), values.begin() + position, values.end());
return values[position];
}
// MinFilter
MinFilter::MinFilter(size_t window_size, size_t send_every, size_t send_first_at)
: send_every_(send_every), send_at_(send_every - send_first_at), window_size_(window_size) {}
void MinFilter::set_send_every(size_t send_every) { this->send_every_ = send_every; }
void MinFilter::set_window_size(size_t window_size) { this->window_size_ = window_size; }
optional<float> MinFilter::new_value(float value) {
while (this->queue_.size() >= this->window_size_) {
this->queue_.pop_front();
}
this->queue_.push_back(value);
ESP_LOGVV(TAG, "MinFilter(%p)::new_value(%f)", this, value);
if (++this->send_at_ >= this->send_every_) {
this->send_at_ = 0;
float min = NAN;
for (auto v : this->queue_) {
if (!std::isnan(v)) {
min = std::isnan(min) ? v : std::min(min, v);
}
}
ESP_LOGVV(TAG, "MinFilter(%p)::new_value(%f) SENDING %f", this, value, min);
return min;
}
return {};
}
float MinFilter::compute_result() { return this->find_extremum_<std::less<float>>(); }
// MaxFilter
MaxFilter::MaxFilter(size_t window_size, size_t send_every, size_t send_first_at)
: send_every_(send_every), send_at_(send_every - send_first_at), window_size_(window_size) {}
void MaxFilter::set_send_every(size_t send_every) { this->send_every_ = send_every; }
void MaxFilter::set_window_size(size_t window_size) { this->window_size_ = window_size; }
optional<float> MaxFilter::new_value(float value) {
while (this->queue_.size() >= this->window_size_) {
this->queue_.pop_front();
}
this->queue_.push_back(value);
ESP_LOGVV(TAG, "MaxFilter(%p)::new_value(%f)", this, value);
if (++this->send_at_ >= this->send_every_) {
this->send_at_ = 0;
float max = NAN;
for (auto v : this->queue_) {
if (!std::isnan(v)) {
max = std::isnan(max) ? v : std::max(max, v);
}
}
ESP_LOGVV(TAG, "MaxFilter(%p)::new_value(%f) SENDING %f", this, value, max);
return max;
}
return {};
}
float MaxFilter::compute_result() { return this->find_extremum_<std::greater<float>>(); }
// SlidingWindowMovingAverageFilter
SlidingWindowMovingAverageFilter::SlidingWindowMovingAverageFilter(size_t window_size, size_t send_every,
size_t send_first_at)
: send_every_(send_every), send_at_(send_every - send_first_at), window_size_(window_size) {}
void SlidingWindowMovingAverageFilter::set_send_every(size_t send_every) { this->send_every_ = send_every; }
void SlidingWindowMovingAverageFilter::set_window_size(size_t window_size) { this->window_size_ = window_size; }
optional<float> SlidingWindowMovingAverageFilter::new_value(float value) {
while (this->queue_.size() >= this->window_size_) {
this->queue_.pop_front();
}
this->queue_.push_back(value);
ESP_LOGVV(TAG, "SlidingWindowMovingAverageFilter(%p)::new_value(%f)", this, value);
if (++this->send_at_ >= this->send_every_) {
this->send_at_ = 0;
float sum = 0;
size_t valid_count = 0;
for (auto v : this->queue_) {
if (!std::isnan(v)) {
sum += v;
valid_count++;
}
float SlidingWindowMovingAverageFilter::compute_result() {
float sum = 0;
size_t valid_count = 0;
for (size_t i = 0; i < this->window_count_; i++) {
float v = this->window_[i];
if (!std::isnan(v)) {
sum += v;
valid_count++;
}
float average = NAN;
if (valid_count) {
average = sum / valid_count;
}
ESP_LOGVV(TAG, "SlidingWindowMovingAverageFilter(%p)::new_value(%f) SENDING %f", this, value, average);
return average;
}
return {};
return valid_count ? sum / valid_count : NAN;
}
// ExponentialMovingAverageFilter
@@ -299,27 +228,40 @@ MultiplyFilter::MultiplyFilter(TemplatableValue<float> multiplier) : multiplier_
optional<float> MultiplyFilter::new_value(float value) { return value * this->multiplier_.value(); }
// FilterOutValueFilter
FilterOutValueFilter::FilterOutValueFilter(std::vector<TemplatableValue<float>> values_to_filter_out)
: values_to_filter_out_(std::move(values_to_filter_out)) {}
// ValueListFilter (base class)
ValueListFilter::ValueListFilter(std::initializer_list<TemplatableValue<float>> values) : values_(values) {}
optional<float> FilterOutValueFilter::new_value(float value) {
bool ValueListFilter::value_matches_any_(float sensor_value) {
int8_t accuracy = this->parent_->get_accuracy_decimals();
float accuracy_mult = powf(10.0f, accuracy);
for (auto filter_value : this->values_to_filter_out_) {
if (std::isnan(filter_value.value())) {
if (std::isnan(value)) {
return {};
}
float rounded_sensor = roundf(accuracy_mult * sensor_value);
for (auto &filter_value : this->values_) {
float fv = filter_value.value();
// Handle NaN comparison
if (std::isnan(fv)) {
if (std::isnan(sensor_value))
return true;
continue;
}
float rounded_filter_out = roundf(accuracy_mult * filter_value.value());
float rounded_value = roundf(accuracy_mult * value);
if (rounded_filter_out == rounded_value) {
return {};
}
// Compare rounded values
if (roundf(accuracy_mult * fv) == rounded_sensor)
return true;
}
return value;
return false;
}
// FilterOutValueFilter
FilterOutValueFilter::FilterOutValueFilter(std::initializer_list<TemplatableValue<float>> values_to_filter_out)
: ValueListFilter(values_to_filter_out) {}
optional<float> FilterOutValueFilter::new_value(float value) {
if (this->value_matches_any_(value))
return {}; // Filter out
return value; // Pass through
}
// ThrottleFilter
@@ -334,33 +276,15 @@ optional<float> ThrottleFilter::new_value(float value) {
}
// ThrottleWithPriorityFilter
ThrottleWithPriorityFilter::ThrottleWithPriorityFilter(uint32_t min_time_between_inputs,
std::vector<TemplatableValue<float>> prioritized_values)
: min_time_between_inputs_(min_time_between_inputs), prioritized_values_(std::move(prioritized_values)) {}
ThrottleWithPriorityFilter::ThrottleWithPriorityFilter(
uint32_t min_time_between_inputs, std::initializer_list<TemplatableValue<float>> prioritized_values)
: ValueListFilter(prioritized_values), min_time_between_inputs_(min_time_between_inputs) {}
optional<float> ThrottleWithPriorityFilter::new_value(float value) {
bool is_prioritized_value = false;
int8_t accuracy = this->parent_->get_accuracy_decimals();
float accuracy_mult = powf(10.0f, accuracy);
const uint32_t now = App.get_loop_component_start_time();
// First, determine if the new value is one of the prioritized values
for (auto prioritized_value : this->prioritized_values_) {
if (std::isnan(prioritized_value.value())) {
if (std::isnan(value)) {
is_prioritized_value = true;
break;
}
continue;
}
float rounded_prioritized_value = roundf(accuracy_mult * prioritized_value.value());
float rounded_value = roundf(accuracy_mult * value);
if (rounded_prioritized_value == rounded_value) {
is_prioritized_value = true;
break;
}
}
// Finally, determine if the new value should be throttled and pass it through if not
if (this->last_input_ == 0 || now - this->last_input_ >= min_time_between_inputs_ || is_prioritized_value) {
// Allow value through if: no previous input, time expired, or is prioritized
if (this->last_input_ == 0 || now - this->last_input_ >= min_time_between_inputs_ ||
this->value_matches_any_(value)) {
this->last_input_ = now;
return value;
}
@@ -389,7 +313,7 @@ optional<float> DeltaFilter::new_value(float value) {
}
// OrFilter
OrFilter::OrFilter(std::vector<Filter *> filters) : filters_(std::move(filters)), phi_(this) {}
OrFilter::OrFilter(std::initializer_list<Filter *> filters) : filters_(filters), phi_(this) {}
OrFilter::PhiNode::PhiNode(OrFilter *or_parent) : or_parent_(or_parent) {}
optional<float> OrFilter::PhiNode::new_value(float value) {
@@ -402,14 +326,14 @@ optional<float> OrFilter::PhiNode::new_value(float value) {
}
optional<float> OrFilter::new_value(float value) {
this->has_value_ = false;
for (Filter *filter : this->filters_)
for (auto *filter : this->filters_)
filter->input(value);
return {};
}
void OrFilter::initialize(Sensor *parent, Filter *next) {
Filter::initialize(parent, next);
for (Filter *filter : this->filters_) {
for (auto *filter : this->filters_) {
filter->initialize(parent, &this->phi_);
}
this->phi_.initialize(parent, nullptr);
@@ -448,8 +372,12 @@ optional<float> HeartbeatFilter::new_value(float value) {
this->last_input_ = value;
this->has_value_ = true;
if (this->optimistic_) {
return value;
}
return {};
}
void HeartbeatFilter::setup() {
this->set_interval("heartbeat", this->time_period_, [this]() {
ESP_LOGVV(TAG, "HeartbeatFilter(%p)::interval(has_value=%s, last_input=%f)", this, YESNO(this->has_value_),
@@ -460,20 +388,27 @@ void HeartbeatFilter::setup() {
this->output(this->last_input_);
});
}
float HeartbeatFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
CalibrateLinearFilter::CalibrateLinearFilter(std::initializer_list<std::array<float, 3>> linear_functions)
: linear_functions_(linear_functions) {}
optional<float> CalibrateLinearFilter::new_value(float value) {
for (std::array<float, 3> f : this->linear_functions_) {
for (const auto &f : this->linear_functions_) {
if (!std::isfinite(f[2]) || value < f[2])
return (value * f[0]) + f[1];
}
return NAN;
}
CalibratePolynomialFilter::CalibratePolynomialFilter(std::initializer_list<float> coefficients)
: coefficients_(coefficients) {}
optional<float> CalibratePolynomialFilter::new_value(float value) {
float res = 0.0f;
float x = 1.0f;
for (float coefficient : this->coefficients_) {
for (const auto &coefficient : this->coefficients_) {
res += x * coefficient;
x *= value;
}
@@ -543,5 +478,78 @@ optional<float> ToNTCTemperatureFilter::new_value(float value) {
return temp;
}
// StreamingFilter (base class)
StreamingFilter::StreamingFilter(size_t window_size, size_t send_first_at)
: window_size_(window_size), send_first_at_(send_first_at) {}
optional<float> StreamingFilter::new_value(float value) {
// Process the value (child class tracks min/max/sum/etc)
this->process_value(value);
this->count_++;
// Check if we should send (handle send_first_at for first value)
bool should_send = false;
if (this->first_send_ && this->count_ >= this->send_first_at_) {
should_send = true;
this->first_send_ = false;
} else if (!this->first_send_ && this->count_ >= this->window_size_) {
should_send = true;
}
if (should_send) {
float result = this->compute_batch_result();
// Reset for next batch
this->count_ = 0;
this->reset_batch();
ESP_LOGVV(TAG, "StreamingFilter(%p)::new_value(%f) SENDING %f", this, value, result);
return result;
}
return {};
}
// StreamingMinFilter
void StreamingMinFilter::process_value(float value) {
// Update running minimum (ignore NaN values)
if (!std::isnan(value)) {
this->current_min_ = std::isnan(this->current_min_) ? value : std::min(this->current_min_, value);
}
}
float StreamingMinFilter::compute_batch_result() { return this->current_min_; }
void StreamingMinFilter::reset_batch() { this->current_min_ = NAN; }
// StreamingMaxFilter
void StreamingMaxFilter::process_value(float value) {
// Update running maximum (ignore NaN values)
if (!std::isnan(value)) {
this->current_max_ = std::isnan(this->current_max_) ? value : std::max(this->current_max_, value);
}
}
float StreamingMaxFilter::compute_batch_result() { return this->current_max_; }
void StreamingMaxFilter::reset_batch() { this->current_max_ = NAN; }
// StreamingMovingAverageFilter
void StreamingMovingAverageFilter::process_value(float value) {
// Accumulate sum (ignore NaN values)
if (!std::isnan(value)) {
this->sum_ += value;
this->valid_count_++;
}
}
float StreamingMovingAverageFilter::compute_batch_result() {
return this->valid_count_ > 0 ? this->sum_ / this->valid_count_ : NAN;
}
void StreamingMovingAverageFilter::reset_batch() {
this->sum_ = 0.0f;
this->valid_count_ = 0;
}
} // namespace sensor
} // namespace esphome

View File

@@ -44,11 +44,75 @@ class Filter {
Sensor *parent_{nullptr};
};
/** Base class for filters that use a sliding window of values.
*
* Uses a ring buffer to efficiently maintain a fixed-size sliding window without
* reallocations or pop_front() overhead. Eliminates deque fragmentation issues.
*/
class SlidingWindowFilter : public Filter {
public:
SlidingWindowFilter(size_t window_size, size_t send_every, size_t send_first_at);
optional<float> new_value(float value) final;
protected:
/// Called by new_value() to compute the filtered result from the current window
virtual float compute_result() = 0;
/// Access the sliding window values (ring buffer implementation)
/// Use: for (size_t i = 0; i < window_count_; i++) { float val = window_[i]; }
FixedVector<float> window_;
size_t window_head_{0}; ///< Index where next value will be written
size_t window_count_{0}; ///< Number of valid values in window (0 to window_size_)
size_t window_size_; ///< Maximum window size
size_t send_every_; ///< Send result every N values
size_t send_at_; ///< Counter for send_every
};
/** Base class for Min/Max filters.
*
* Provides a templated helper to find extremum values efficiently.
*/
class MinMaxFilter : public SlidingWindowFilter {
public:
using SlidingWindowFilter::SlidingWindowFilter;
protected:
/// Helper to find min or max value in window, skipping NaN values
/// Usage: find_extremum_<std::less<float>>() for min, find_extremum_<std::greater<float>>() for max
template<typename Compare> float find_extremum_() {
float result = NAN;
Compare comp;
for (size_t i = 0; i < this->window_count_; i++) {
float v = this->window_[i];
if (!std::isnan(v)) {
result = std::isnan(result) ? v : (comp(v, result) ? v : result);
}
}
return result;
}
};
/** Base class for filters that need a sorted window (Median, Quantile).
*
* Extends SlidingWindowFilter to provide a helper that filters out NaN values.
* Derived classes use std::nth_element for efficient partial sorting.
*/
class SortedWindowFilter : public SlidingWindowFilter {
public:
using SlidingWindowFilter::SlidingWindowFilter;
protected:
/// Helper to get non-NaN values from the window (not sorted - caller will use nth_element)
/// Returns empty FixedVector if all values are NaN
FixedVector<float> get_window_values_();
};
/** Simple quantile filter.
*
* Takes the quantile of the last <send_every> values and pushes it out every <send_every>.
* Takes the quantile of the last <window_size> values and pushes it out every <send_every>.
*/
class QuantileFilter : public Filter {
class QuantileFilter : public SortedWindowFilter {
public:
/** Construct a QuantileFilter.
*
@@ -61,25 +125,18 @@ class QuantileFilter : public Filter {
*/
explicit QuantileFilter(size_t window_size, size_t send_every, size_t send_first_at, float quantile);
optional<float> new_value(float value) override;
void set_send_every(size_t send_every);
void set_window_size(size_t window_size);
void set_quantile(float quantile);
void set_quantile(float quantile) { this->quantile_ = quantile; }
protected:
std::deque<float> queue_;
size_t send_every_;
size_t send_at_;
size_t window_size_;
float compute_result() override;
float quantile_;
};
/** Simple median filter.
*
* Takes the median of the last <send_every> values and pushes it out every <send_every>.
* Takes the median of the last <window_size> values and pushes it out every <send_every>.
*/
class MedianFilter : public Filter {
class MedianFilter : public SortedWindowFilter {
public:
/** Construct a MedianFilter.
*
@@ -89,18 +146,10 @@ class MedianFilter : public Filter {
* on startup being published on the first *raw* value, so with no filter applied. Must be less than or equal to
* send_every.
*/
explicit MedianFilter(size_t window_size, size_t send_every, size_t send_first_at);
optional<float> new_value(float value) override;
void set_send_every(size_t send_every);
void set_window_size(size_t window_size);
using SortedWindowFilter::SortedWindowFilter;
protected:
std::deque<float> queue_;
size_t send_every_;
size_t send_at_;
size_t window_size_;
float compute_result() override;
};
/** Simple skip filter.
@@ -123,9 +172,9 @@ class SkipInitialFilter : public Filter {
/** Simple min filter.
*
* Takes the min of the last <send_every> values and pushes it out every <send_every>.
* Takes the min of the last <window_size> values and pushes it out every <send_every>.
*/
class MinFilter : public Filter {
class MinFilter : public MinMaxFilter {
public:
/** Construct a MinFilter.
*
@@ -135,25 +184,17 @@ class MinFilter : public Filter {
* on startup being published on the first *raw* value, so with no filter applied. Must be less than or equal to
* send_every.
*/
explicit MinFilter(size_t window_size, size_t send_every, size_t send_first_at);
optional<float> new_value(float value) override;
void set_send_every(size_t send_every);
void set_window_size(size_t window_size);
using MinMaxFilter::MinMaxFilter;
protected:
std::deque<float> queue_;
size_t send_every_;
size_t send_at_;
size_t window_size_;
float compute_result() override;
};
/** Simple max filter.
*
* Takes the max of the last <send_every> values and pushes it out every <send_every>.
* Takes the max of the last <window_size> values and pushes it out every <send_every>.
*/
class MaxFilter : public Filter {
class MaxFilter : public MinMaxFilter {
public:
/** Construct a MaxFilter.
*
@@ -163,18 +204,10 @@ class MaxFilter : public Filter {
* on startup being published on the first *raw* value, so with no filter applied. Must be less than or equal to
* send_every.
*/
explicit MaxFilter(size_t window_size, size_t send_every, size_t send_first_at);
optional<float> new_value(float value) override;
void set_send_every(size_t send_every);
void set_window_size(size_t window_size);
using MinMaxFilter::MinMaxFilter;
protected:
std::deque<float> queue_;
size_t send_every_;
size_t send_at_;
size_t window_size_;
float compute_result() override;
};
/** Simple sliding window moving average filter.
@@ -182,7 +215,7 @@ class MaxFilter : public Filter {
* Essentially just takes takes the average of the last window_size values and pushes them out
* every send_every.
*/
class SlidingWindowMovingAverageFilter : public Filter {
class SlidingWindowMovingAverageFilter : public SlidingWindowFilter {
public:
/** Construct a SlidingWindowMovingAverageFilter.
*
@@ -192,18 +225,10 @@ class SlidingWindowMovingAverageFilter : public Filter {
* on startup being published on the first *raw* value, so with no filter applied. Must be less than or equal to
* send_every.
*/
explicit SlidingWindowMovingAverageFilter(size_t window_size, size_t send_every, size_t send_first_at);
optional<float> new_value(float value) override;
void set_send_every(size_t send_every);
void set_window_size(size_t window_size);
using SlidingWindowFilter::SlidingWindowFilter;
protected:
std::deque<float> queue_;
size_t send_every_;
size_t send_at_;
size_t window_size_;
float compute_result() override;
};
/** Simple exponential moving average filter.
@@ -292,15 +317,28 @@ class MultiplyFilter : public Filter {
TemplatableValue<float> multiplier_;
};
/** Base class for filters that compare sensor values against a list of configured values.
*
* This base class provides common functionality for filters that need to check if a sensor
* value matches any value in a configured list, with proper handling of NaN values and
* accuracy-based rounding for comparisons.
*/
class ValueListFilter : public Filter {
protected:
explicit ValueListFilter(std::initializer_list<TemplatableValue<float>> values);
/// Check if sensor value matches any configured value (with accuracy rounding)
bool value_matches_any_(float sensor_value);
FixedVector<TemplatableValue<float>> values_;
};
/// A simple filter that only forwards the filter chain if it doesn't receive `value_to_filter_out`.
class FilterOutValueFilter : public Filter {
class FilterOutValueFilter : public ValueListFilter {
public:
explicit FilterOutValueFilter(std::vector<TemplatableValue<float>> values_to_filter_out);
explicit FilterOutValueFilter(std::initializer_list<TemplatableValue<float>> values_to_filter_out);
optional<float> new_value(float value) override;
protected:
std::vector<TemplatableValue<float>> values_to_filter_out_;
};
class ThrottleFilter : public Filter {
@@ -315,17 +353,16 @@ class ThrottleFilter : public Filter {
};
/// Same as 'throttle' but will immediately publish values contained in `value_to_prioritize`.
class ThrottleWithPriorityFilter : public Filter {
class ThrottleWithPriorityFilter : public ValueListFilter {
public:
explicit ThrottleWithPriorityFilter(uint32_t min_time_between_inputs,
std::vector<TemplatableValue<float>> prioritized_values);
std::initializer_list<TemplatableValue<float>> prioritized_values);
optional<float> new_value(float value) override;
protected:
uint32_t last_input_{0};
uint32_t min_time_between_inputs_;
std::vector<TemplatableValue<float>> prioritized_values_;
};
class TimeoutFilter : public Filter, public Component {
@@ -359,15 +396,16 @@ class HeartbeatFilter : public Filter, public Component {
explicit HeartbeatFilter(uint32_t time_period);
void setup() override;
optional<float> new_value(float value) override;
float get_setup_priority() const override;
void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
protected:
uint32_t time_period_;
float last_input_;
bool has_value_{false};
bool optimistic_{false};
};
class DeltaFilter : public Filter {
@@ -385,7 +423,7 @@ class DeltaFilter : public Filter {
class OrFilter : public Filter {
public:
explicit OrFilter(std::vector<Filter *> filters);
explicit OrFilter(std::initializer_list<Filter *> filters);
void initialize(Sensor *parent, Filter *next) override;
@@ -401,28 +439,27 @@ class OrFilter : public Filter {
OrFilter *or_parent_;
};
std::vector<Filter *> filters_;
FixedVector<Filter *> filters_;
PhiNode phi_;
bool has_value_{false};
};
class CalibrateLinearFilter : public Filter {
public:
CalibrateLinearFilter(std::vector<std::array<float, 3>> linear_functions)
: linear_functions_(std::move(linear_functions)) {}
explicit CalibrateLinearFilter(std::initializer_list<std::array<float, 3>> linear_functions);
optional<float> new_value(float value) override;
protected:
std::vector<std::array<float, 3>> linear_functions_;
FixedVector<std::array<float, 3>> linear_functions_;
};
class CalibratePolynomialFilter : public Filter {
public:
CalibratePolynomialFilter(std::vector<float> coefficients) : coefficients_(std::move(coefficients)) {}
explicit CalibratePolynomialFilter(std::initializer_list<float> coefficients);
optional<float> new_value(float value) override;
protected:
std::vector<float> coefficients_;
FixedVector<float> coefficients_;
};
class ClampFilter : public Filter {
@@ -476,5 +513,81 @@ class ToNTCTemperatureFilter : public Filter {
double c_;
};
/** Base class for streaming filters (batch windows where window_size == send_every).
*
* When window_size equals send_every, we don't need a sliding window.
* This base class handles the common batching logic.
*/
class StreamingFilter : public Filter {
public:
StreamingFilter(size_t window_size, size_t send_first_at);
optional<float> new_value(float value) final;
protected:
/// Called by new_value() to process each value in the batch
virtual void process_value(float value) = 0;
/// Called by new_value() to compute the result after collecting window_size values
virtual float compute_batch_result() = 0;
/// Called by new_value() to reset internal state after sending a result
virtual void reset_batch() = 0;
size_t window_size_;
size_t count_{0};
size_t send_first_at_;
bool first_send_{true};
};
/** Streaming min filter for batch windows (window_size == send_every).
*
* Uses O(1) memory instead of O(n) by tracking only the minimum value.
*/
class StreamingMinFilter : public StreamingFilter {
public:
using StreamingFilter::StreamingFilter;
protected:
void process_value(float value) override;
float compute_batch_result() override;
void reset_batch() override;
float current_min_{NAN};
};
/** Streaming max filter for batch windows (window_size == send_every).
*
* Uses O(1) memory instead of O(n) by tracking only the maximum value.
*/
class StreamingMaxFilter : public StreamingFilter {
public:
using StreamingFilter::StreamingFilter;
protected:
void process_value(float value) override;
float compute_batch_result() override;
void reset_batch() override;
float current_max_{NAN};
};
/** Streaming moving average filter for batch windows (window_size == send_every).
*
* Uses O(1) memory instead of O(n) by tracking only sum and count.
*/
class StreamingMovingAverageFilter : public StreamingFilter {
public:
using StreamingFilter::StreamingFilter;
protected:
void process_value(float value) override;
float compute_batch_result() override;
void reset_batch() override;
float sum_{0.0f};
size_t valid_count_{0};
};
} // namespace sensor
} // namespace esphome

View File

@@ -107,12 +107,12 @@ void Sensor::add_filter(Filter *filter) {
}
filter->initialize(this, nullptr);
}
void Sensor::add_filters(const std::vector<Filter *> &filters) {
void Sensor::add_filters(std::initializer_list<Filter *> filters) {
for (Filter *filter : filters) {
this->add_filter(filter);
}
}
void Sensor::set_filters(const std::vector<Filter *> &filters) {
void Sensor::set_filters(std::initializer_list<Filter *> filters) {
this->clear_filters();
this->add_filters(filters);
}

View File

@@ -6,7 +6,7 @@
#include "esphome/core/log.h"
#include "esphome/components/sensor/filter.h"
#include <vector>
#include <initializer_list>
#include <memory>
namespace esphome {
@@ -77,10 +77,10 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa
* SlidingWindowMovingAverageFilter(15, 15), // average over last 15 values
* });
*/
void add_filters(const std::vector<Filter *> &filters);
void add_filters(std::initializer_list<Filter *> filters);
/// Clear the filters and replace them by filters.
void set_filters(const std::vector<Filter *> &filters);
void set_filters(std::initializer_list<Filter *> filters);
/// Clear the entire filter chain.
void clear_filters();

View File

@@ -1,3 +1,5 @@
from collections.abc import Callable, MutableMapping
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.core import CORE
@@ -9,6 +11,32 @@ IMPLEMENTATION_LWIP_TCP = "lwip_tcp"
IMPLEMENTATION_LWIP_SOCKETS = "lwip_sockets"
IMPLEMENTATION_BSD_SOCKETS = "bsd_sockets"
# Socket tracking infrastructure
# Components register their socket needs and platforms read this to configure appropriately
KEY_SOCKET_CONSUMERS = "socket_consumers"
def consume_sockets(
value: int, consumer: str
) -> Callable[[MutableMapping], MutableMapping]:
"""Register socket usage for a component.
Args:
value: Number of sockets needed by the component
consumer: Name of the component consuming the sockets
Returns:
A validator function that records the socket usage
"""
def _consume_sockets(config: MutableMapping) -> MutableMapping:
consumers: dict[str, int] = CORE.data.setdefault(KEY_SOCKET_CONSUMERS, {})
consumers[consumer] = consumers.get(consumer, 0) + value
return config
return _consume_sockets
CONFIG_SCHEMA = cv.Schema(
{
cv.SplitDefault(

View File

@@ -28,21 +28,6 @@
namespace esphome {
namespace statsd {
using sensor_type_t = enum { TYPE_SENSOR, TYPE_BINARY_SENSOR };
using sensors_t = struct {
const char *name;
sensor_type_t type;
union {
#ifdef USE_SENSOR
esphome::sensor::Sensor *sensor;
#endif
#ifdef USE_BINARY_SENSOR
esphome::binary_sensor::BinarySensor *binary_sensor;
#endif
};
};
class StatsdComponent : public PollingComponent {
public:
~StatsdComponent();
@@ -71,6 +56,20 @@ class StatsdComponent : public PollingComponent {
const char *prefix_;
uint16_t port_;
using sensor_type_t = enum { TYPE_SENSOR, TYPE_BINARY_SENSOR };
using sensors_t = struct {
const char *name;
sensor_type_t type;
union {
#ifdef USE_SENSOR
esphome::sensor::Sensor *sensor;
#endif
#ifdef USE_BINARY_SENSOR
esphome::binary_sensor::BinarySensor *binary_sensor;
#endif
};
};
std::vector<sensors_t> sensors_;
#ifdef USE_ESP8266

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