1
0
mirror of https://github.com/esphome/esphome.git synced 2025-11-07 18:41:53 +00:00

Compare commits

...

90 Commits

Author SHA1 Message Date
J. Nick Koston
4c9e4d30e9 tweak 2025-10-19 14:56:40 -10:00
J. Nick Koston
db42983f0c wip 2025-10-19 14:54:31 -10:00
J. Nick Koston
20c65f70ed wip 2025-10-19 14:47:19 -10:00
J. Nick Koston
38e31e328c wip 2025-10-19 14:46:25 -10:00
J. Nick Koston
58cecff778 wip 2025-10-19 14:44:04 -10:00
J. Nick Koston
1946656ea8 wip 2025-10-19 14:40:47 -10:00
J. Nick Koston
c9700a0450 wip 2025-10-19 14:35:09 -10:00
J. Nick Koston
0eab64ffe5 cache github downloads 2025-10-19 14:33:26 -10: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
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
J. Nick Koston
85f1019d90 [tests] Migrate remote_transmitter/receiver to common bus definitions (#11325) 2025-10-17 17:21:38 -10:00
Leonardo Rivera
bfeade1e2b [remote_base] Add Symphony IR protocol (encode/decode) with command_repeats support (#10777) 2025-10-17 21:13:33 -04:00
Niall Douglas
b134d42e3b [xgzp68xx] Add oversampling config and tidy up implementation. (#10306)
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>
2025-10-18 00:47:18 +00:00
J. Nick Koston
b3b65316f0 [ci] Fix test_build_components missing test files with hyphen naming pattern (#11329) 2025-10-17 13:05:44 -10:00
dependabot[bot]
b61cec8e77 Bump github/codeql-action from 4.30.8 to 4.30.9 (#11326)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-17 09:14:45 -10:00
Jonathan Swoboda
24243fb22c [tests] Add i2c_id to mcp47a1 & mcp4725 and remove from isolation (#11324) 2025-10-17 08:23:49 -10:00
J. Nick Koston
ba6c8c87c2 [dashboard] Fix binary download with packages using secrets after Path migration (#11313) 2025-10-17 08:20:55 -10:00
J. Nick Koston
f5774cc138 [debug] Replace std::map with struct array for ESP32 chip features (#11307) 2025-10-17 08:20:31 -10:00
B48D81EFCC
6d09e68b2e [bh1900nux] Add bh1900nux temperature Sensor (#8631)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Andreas Riehl <andreas.riehl@acp.de>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-10-17 11:11:51 -04:00
tomaszduda23
fe9db75c27 [nrf52] add xiao_ble board (#10698)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-10-17 09:02:37 -04:00
mrtoy-me
2b832e9ee8 [cap1188] remove delays in setup (#11317)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-10-17 08:55:07 -04:00
exotime
661e9f9991 [toshiba] Add support for RAS-2819T air conditioner (#9490)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
2025-10-17 03:33:50 -05:00
esphomebot
39e23c323d Synchronise Device Classes from Home Assistant (#11285) 2025-10-17 07:49:10 +00:00
J. Nick Koston
bdfbac0301 [tests] Fix ESP32-C3 component test binary size by using larger partition table (#11319) 2025-10-17 20:20:00 +13:00
Daniel Stiner
9646653e57 [const] Add CONF_OPENTHREAD (#11318) 2025-10-16 21:02:28 -10:00
Joshua M. Boniface
c6c202e4f7 [ina2xx_base] add device reset-on-boot disablement option (#10787)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-10-16 21:57:19 -04:00
Jonathan Swoboda
62f73c768e [esp32] Reduce tx power on IDF in the event of a brownout (#11306) 2025-10-16 17:43:15 -04:00
Jonathan Swoboda
cd1215347e [esp32] Reduce tx power on Arduino (#11304) 2025-10-16 16:55:30 -04:00
dependabot[bot]
b8353b3117 Bump ruff from 0.14.0 to 0.14.1 (#11303)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-10-16 20:52:22 +00:00
Jonathan Swoboda
5d3574c81f [htu21d] Revert register address change (#11291) 2025-10-16 11:29:05 -04:00
Jonathan Swoboda
364e5ffd79 [core] Add ESP32 ROM functions to reserved ids (#11293) 2025-10-16 11:28:52 -04:00
Jesse Hills
c38c2a1daf Merge branch 'release' into dev 2025-10-16 23:00:45 +13:00
Jesse Hills
070b0882b8 Merge pull request #11284 from esphome/bump-2025.10.1
2025.10.1
2025-10-16 23:00:00 +13:00
J. Nick Koston
7e2ccb7bc3 [datetime] Fix DateTimeStateTrigger compilation when time component is not used (#11287) 2025-10-15 23:45:42 -10:00
Jesse Hills
7f1173fcba Bump version to 2025.10.1 2025-10-16 17:17:07 +13:00
J. Nick Koston
a75ccf841c [substitutions] Fix AttributeError when using packages with substitutions (#11274)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-16 17:17:07 +13:00
Jonathan Swoboda
56eb605ec9 [wifi] Fix enterprise wifi (#11276)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-10-16 17:17:07 +13:00
J. Nick Koston
2c4818de00 [tests] Fix OTA password test assertions after merge collision (#11275) 2025-10-16 17:17:06 +13:00
Jesse Hills
2b94de8732 [ota.esphome] Handle blank password the same as no password defined (#11271) 2025-10-16 17:17:06 +13:00
J. Nick Koston
f71aed3a5c [ota] Fix MQTT resolution when static IP appears first in device list (#11272) 2025-10-16 17:17:06 +13:00
Clyde Stubbs
353e097085 [mipi_spi] Rotation fixes (#11226) 2025-10-16 17:17:06 +13:00
J. Nick Koston
14d76e9e4e [ci] Merge components with different buses to reduce CI time (#11251)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-10-16 16:36:03 +13:00
J. Nick Koston
f2e0a412db [substitutions] Fix AttributeError when using packages with substitutions (#11274)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-16 16:23:20 +13:00
J. Nick Koston
6943b1d985 [api] Use FixedVector for ExecuteServiceRequest/Argument arrays to eliminate reallocations (#11270) 2025-10-16 16:22:08 +13:00
J. Nick Koston
18062d154f [esp32_ble_tracker] Refactor to use CORE.data instead of module-level globals (#11220) 2025-10-16 16:18:30 +13:00
J. Nick Koston
2b0b82b2fb [esp32_ble] Refactor to use CORE.data instead of module-level globals (#11222) 2025-10-16 16:17:16 +13:00
J. Nick Koston
3e1c8f37c5 [i2s_audio] Refactor to use CORE.data instead of module-level globals (#11223) 2025-10-16 16:16:28 +13:00
Keith Burzinski
236ca12d3e [api, climate, thermostat] Implement feature_flags for climate (#10987)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-10-15 21:59:55 -05:00
J. Nick Koston
42f1b61e31 [git] Automatically recover from broken git repositories in external_components (#11246) 2025-10-16 15:58:58 +13:00
J. Nick Koston
708f8a95e5 [api] Use FixedVector for HomeAssistantServiceCallAction to reduce flash usage and avoid realloc (#11277) 2025-10-16 02:48:38 +00:00
J. Nick Koston
10ca86ae8d [api] Use std::unique_ptr for fixed-size byte buffers in Noise protocol (#11278) 2025-10-16 02:41:25 +00:00
Jonathan Swoboda
22056e0809 [wifi] Fix enterprise wifi (#11276)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-10-16 01:24:56 +00:00
J. Nick Koston
fe4857fabb [tests] Fix OTA password test assertions after merge collision (#11275) 2025-10-16 13:28:19 +13:00
Jesse Hills
3054c2bc29 [ota.esphome] Handle blank password the same as no password defined (#11271) 2025-10-16 13:07:37 +13:00
J. Nick Koston
b190f37ae7 [ota] Fix MQTT resolution when static IP appears first in device list (#11272) 2025-10-16 13:06:02 +13:00
372 changed files with 13252 additions and 1788 deletions

View File

@@ -1,4 +1,5 @@
[run]
omit =
esphome/components/*
esphome/analyze_memory/*
tests/integration/*

View File

@@ -0,0 +1,108 @@
---
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
head_sha="${{ github.event.workflow_run.head_sha }}"
pr_data=$(gh api "/repos/${{ github.repository }}/commits/$head_sha/pulls" \
--jq '.[0] | {number: .number, base_ref: .base.ref}')
if [ -z "$pr_data" ] || [ "$pr_data" == "null" ]; 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

@@ -175,6 +175,7 @@ jobs:
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 }}
memory_impact: ${{ steps.determine.outputs.memory-impact }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
@@ -204,6 +205,7 @@ jobs:
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 "memory-impact=$(echo "$output" | jq -c '.memory_impact')" >> $GITHUB_OUTPUT
integration-tests:
name: Run integration tests
@@ -379,7 +381,16 @@ jobs:
# Use intelligent splitter that groups components with same bus configs
components='${{ needs.determine-jobs.outputs.changed-components-with-tests }}'
directly_changed='${{ needs.determine-jobs.outputs.directly-changed-components-with-tests }}'
# Only isolate directly changed components when targeting dev branch
# For beta/release branches, group everything for faster CI
if [[ "${{ github.base_ref }}" == beta* ]] || [[ "${{ github.base_ref }}" == release* ]]; then
directly_changed='[]'
echo "Target branch: ${{ github.base_ref }} - grouping all components"
else
directly_changed='${{ needs.determine-jobs.outputs.directly-changed-components-with-tests }}'
echo "Target branch: ${{ github.base_ref }} - isolating directly changed components"
fi
echo "Splitting components intelligently..."
output=$(python3 script/split_components_for_ci.py --components "$components" --directly-changed "$directly_changed" --batch-size 40 --output github)
@@ -396,7 +407,7 @@ jobs:
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0
strategy:
fail-fast: false
max-parallel: ${{ (github.base_ref == 'beta' || github.base_ref == 'release') && 8 || 4 }}
max-parallel: ${{ (startsWith(github.base_ref, 'beta') || startsWith(github.base_ref, 'release')) && 8 || 4 }}
matrix:
components: ${{ fromJson(needs.test-build-components-splitter.outputs.matrix) }}
steps:
@@ -421,21 +432,49 @@ jobs:
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-test-${{ 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-test-${{ hashFiles('platformio.ini') }}
- name: Validate and compile components with intelligent grouping
run: |
. venv/bin/activate
# Use /mnt for build files (70GB available vs ~29GB on /)
# Bind mount PlatformIO directory to /mnt (tools, packages, build cache all go there)
sudo mkdir -p /mnt/platformio
sudo chown $USER:$USER /mnt/platformio
mkdir -p ~/.platformio
sudo mount --bind /mnt/platformio ~/.platformio
# Bind mount test build directory to /mnt
sudo mkdir -p /mnt/test_build_components_build
sudo chown $USER:$USER /mnt/test_build_components_build
mkdir -p tests/test_build_components/build
sudo mount --bind /mnt/test_build_components_build tests/test_build_components/build
# Check if /mnt has more free space than / before bind mounting
# Extract available space in KB for comparison
root_avail=$(df -k / | awk 'NR==2 {print $4}')
mnt_avail=$(df -k /mnt 2>/dev/null | awk 'NR==2 {print $4}')
echo "Available space: / has ${root_avail}KB, /mnt has ${mnt_avail}KB"
# Only use /mnt if it has more space than /
if [ -n "$mnt_avail" ] && [ "$mnt_avail" -gt "$root_avail" ]; then
echo "Using /mnt for build files (more space available)"
# Bind mount PlatformIO directory to /mnt (tools, packages, build cache all go there)
sudo mkdir -p /mnt/platformio
sudo chown $USER:$USER /mnt/platformio
mkdir -p ~/.platformio
sudo mount --bind /mnt/platformio ~/.platformio
# Bind mount test build directory to /mnt
sudo mkdir -p /mnt/test_build_components_build
sudo chown $USER:$USER /mnt/test_build_components_build
mkdir -p tests/test_build_components/build
sudo mount --bind /mnt/test_build_components_build tests/test_build_components/build
else
echo "Using / for build files (more space available than /mnt or /mnt unavailable)"
fi
# Convert space-separated components to comma-separated for Python script
components_csv=$(echo "${{ matrix.components }}" | tr ' ' ',')
@@ -448,7 +487,7 @@ jobs:
# - This catches pin conflicts and other issues in directly changed code
# - Grouped tests use --testing-mode to allow config merging (disables some checks)
# - Dependencies are safe to group since they weren't modified in this PR
if [ "${{ github.base_ref }}" = "beta" ] || [ "${{ github.base_ref }}" = "release" ]; then
if [[ "${{ github.base_ref }}" == beta* ]] || [[ "${{ github.base_ref }}" == release* ]]; then
directly_changed_csv=""
echo "Testing components: $components_csv"
echo "Target branch: ${{ github.base_ref }} - grouping all components"
@@ -459,6 +498,11 @@ jobs:
fi
echo ""
# Show disk space before validation (after bind mounts setup)
echo "Disk space before config validation:"
df -h
echo ""
# Run config validation with grouping and isolation
python3 script/test_build_components.py -e config -c "$components_csv" -f --isolate "$directly_changed_csv"
@@ -466,6 +510,11 @@ jobs:
echo "Config validation passed! Starting compilation..."
echo ""
# Show disk space before compilation
echo "Disk space before compilation:"
df -h
echo ""
# Run compilation with grouping and isolation
python3 script/test_build_components.py -e compile -c "$components_csv" -f --isolate "$directly_changed_csv"
@@ -474,7 +523,7 @@ jobs:
runs-on: ubuntu-latest
needs:
- common
if: github.event_name == 'pull_request' && github.base_ref != 'beta' && github.base_ref != 'release'
if: github.event_name == 'pull_request' && !startsWith(github.base_ref, 'beta') && !startsWith(github.base_ref, 'release')
steps:
- name: Check out code from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
@@ -489,6 +538,271 @@ jobs:
- uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0
if: always()
memory-impact-target-branch:
name: Build target branch for memory impact
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.memory_impact).should_run == 'true'
outputs:
ram_usage: ${{ steps.extract.outputs.ram_usage }}
flash_usage: ${{ steps.extract.outputs.flash_usage }}
cache_hit: ${{ steps.cache-memory-analysis.outputs.cache-hit }}
skip: ${{ steps.check-script.outputs.skip }}
steps:
- name: Check out target branch
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
ref: ${{ github.base_ref }}
# Check if memory impact extraction script exists on target branch
# If not, skip the analysis (this handles older branches that don't have the feature)
- name: Check for memory impact script
id: check-script
run: |
if [ -f "script/ci_memory_impact_extract.py" ]; then
echo "skip=false" >> $GITHUB_OUTPUT
else
echo "skip=true" >> $GITHUB_OUTPUT
echo "::warning::ci_memory_impact_extract.py not found on target branch, skipping memory impact analysis"
fi
# All remaining steps only run if script exists
- name: Generate cache key
id: cache-key
if: steps.check-script.outputs.skip != 'true'
run: |
# Get the commit SHA of the target branch
target_sha=$(git rev-parse HEAD)
# Hash the build infrastructure files (all files that affect build/analysis)
infra_hash=$(cat \
script/test_build_components.py \
script/ci_memory_impact_extract.py \
script/analyze_component_buses.py \
script/merge_component_configs.py \
script/ci_helpers.py \
.github/workflows/ci.yml \
| sha256sum | cut -d' ' -f1)
# Get platform and components from job inputs
platform="${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}"
components='${{ toJSON(fromJSON(needs.determine-jobs.outputs.memory_impact).components) }}'
components_hash=$(echo "$components" | sha256sum | cut -d' ' -f1)
# Combine into cache key
cache_key="memory-analysis-target-${target_sha}-${infra_hash}-${platform}-${components_hash}"
echo "cache-key=${cache_key}" >> $GITHUB_OUTPUT
echo "Cache key: ${cache_key}"
- name: Restore cached memory analysis
id: cache-memory-analysis
if: steps.check-script.outputs.skip != 'true'
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: memory-analysis-target.json
key: ${{ steps.cache-key.outputs.cache-key }}
- name: Cache status
if: steps.check-script.outputs.skip != 'true'
run: |
if [ "${{ steps.cache-memory-analysis.outputs.cache-hit }}" == "true" ]; then
echo "✓ Cache hit! Using cached memory analysis results."
echo " Skipping build step to save time."
else
echo "✗ Cache miss. Will build and analyze memory usage."
fi
- name: Restore Python
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache platformio
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.platformio
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
- name: Build, compile, and analyze memory
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
id: build
run: |
. venv/bin/activate
components='${{ toJSON(fromJSON(needs.determine-jobs.outputs.memory_impact).components) }}'
platform="${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}"
echo "Building with test_build_components.py for $platform with components:"
echo "$components" | jq -r '.[]' | sed 's/^/ - /'
# Use test_build_components.py which handles grouping automatically
# Pass components as comma-separated list
component_list=$(echo "$components" | jq -r 'join(",")')
echo "Compiling with test_build_components.py..."
# Run build and extract memory with auto-detection of build directory for detailed analysis
# Use tee to show output in CI while also piping to extraction script
python script/test_build_components.py \
-e compile \
-c "$component_list" \
-t "$platform" 2>&1 | \
tee /dev/stderr | \
python script/ci_memory_impact_extract.py \
--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
with:
path: memory-analysis-target.json
key: ${{ steps.cache-key.outputs.cache-key }}
- name: Extract memory usage for outputs
id: extract
if: steps.check-script.outputs.skip != 'true'
run: |
if [ -f memory-analysis-target.json ]; then
ram=$(jq -r '.ram_bytes' memory-analysis-target.json)
flash=$(jq -r '.flash_bytes' memory-analysis-target.json)
echo "ram_usage=${ram}" >> $GITHUB_OUTPUT
echo "flash_usage=${flash}" >> $GITHUB_OUTPUT
echo "RAM: ${ram} bytes, Flash: ${flash} bytes"
else
echo "Error: memory-analysis-target.json not found"
exit 1
fi
- name: Upload memory analysis JSON
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: memory-analysis-target
path: memory-analysis-target.json
if-no-files-found: warn
retention-days: 1
memory-impact-pr-branch:
name: Build PR branch for memory impact
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.memory_impact).should_run == 'true'
outputs:
ram_usage: ${{ steps.extract.outputs.ram_usage }}
flash_usage: ${{ steps.extract.outputs.flash_usage }}
steps:
- name: Check out PR branch
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache platformio
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.platformio
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
- name: Build, compile, and analyze memory
id: extract
run: |
. venv/bin/activate
components='${{ toJSON(fromJSON(needs.determine-jobs.outputs.memory_impact).components) }}'
platform="${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}"
echo "Building with test_build_components.py for $platform with components:"
echo "$components" | jq -r '.[]' | sed 's/^/ - /'
# Use test_build_components.py which handles grouping automatically
# Pass components as comma-separated list
component_list=$(echo "$components" | jq -r 'join(",")')
echo "Compiling with test_build_components.py..."
# Run build and extract memory with auto-detection of build directory for detailed analysis
# Use tee to show output in CI while also piping to extraction script
python script/test_build_components.py \
-e compile \
-c "$component_list" \
-t "$platform" 2>&1 | \
tee /dev/stderr | \
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:
name: memory-analysis-pr
path: memory-analysis-pr.json
if-no-files-found: warn
retention-days: 1
memory-impact-comment:
name: Comment memory impact
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
- memory-impact-target-branch
- memory-impact-pr-branch
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
- name: Restore Python
uses: ./.github/actions/restore-python
with:
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
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
with:
name: memory-analysis-pr
path: ./memory-analysis
continue-on-error: true
- name: Post or update PR comment
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
. venv/bin/activate
# 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 "$PR_NUMBER" \
--target-json ./memory-analysis/memory-analysis-target.json \
--pr-json ./memory-analysis/memory-analysis-pr.json
ci-status:
name: CI Status
runs-on: ubuntu-24.04
@@ -503,6 +817,9 @@ jobs:
- test-build-components-splitter
- test-build-components-split
- pre-commit-ci-lite
- memory-impact-target-branch
- memory-impact-pr-branch
- memory-impact-comment
if: always()
steps:
- name: Success

View File

@@ -58,7 +58,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8
uses: github/codeql-action/init@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -86,6 +86,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8
uses: github/codeql-action/analyze@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
with:
category: "/language:${{matrix.language}}"

View File

@@ -11,7 +11,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.14.0
rev: v0.14.1
hooks:
# Run the linter.
- id: ruff

View File

@@ -62,6 +62,7 @@ esphome/components/bedjet/fan/* @jhansche
esphome/components/bedjet/sensor/* @javawizard @jhansche
esphome/components/beken_spi_led_strip/* @Mat931
esphome/components/bh1750/* @OttoWinter
esphome/components/bh1900nux/* @B48D81EFCC
esphome/components/binary_sensor/* @esphome/core
esphome/components/bk72xx/* @kuba2k2
esphome/components/bl0906/* @athom-tech @jesserockz @tarontop
@@ -69,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

View File

@@ -117,6 +117,17 @@ class Purpose(StrEnum):
LOGGING = "logging"
class PortType(StrEnum):
SERIAL = "SERIAL"
NETWORK = "NETWORK"
MQTT = "MQTT"
MQTTIP = "MQTTIP"
# Magic MQTT port types that require special handling
_MQTT_PORT_TYPES = frozenset({PortType.MQTT, PortType.MQTTIP})
def _resolve_with_cache(address: str, purpose: Purpose) -> list[str]:
"""Resolve an address using cache if available, otherwise return the address itself."""
if CORE.address_cache and (cached := CORE.address_cache.get_addresses(address)):
@@ -174,7 +185,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
@@ -280,16 +293,67 @@ def mqtt_get_ip(config: ConfigType, username: str, password: str, client_id: str
return mqtt.get_esphome_device_ip(config, username, password, client_id)
_PORT_TO_PORT_TYPE = {
"MQTT": "MQTT",
"MQTTIP": "MQTTIP",
}
def _resolve_network_devices(
devices: list[str], config: ConfigType, args: ArgsProtocol
) -> list[str]:
"""Resolve device list, converting MQTT magic strings to actual IP addresses.
This function filters the devices list to:
- Replace MQTT/MQTTIP magic strings with actual IP addresses via MQTT lookup
- Deduplicate addresses while preserving order
- Only resolve MQTT once even if multiple MQTT strings are present
- If MQTT resolution fails, log a warning and continue with other devices
Args:
devices: List of device identifiers (IPs, hostnames, or magic strings)
config: ESPHome configuration
args: Command-line arguments containing MQTT credentials
Returns:
List of network addresses suitable for connection attempts
"""
network_devices: list[str] = []
mqtt_resolved: bool = False
for device in devices:
port_type = get_port_type(device)
if port_type in _MQTT_PORT_TYPES:
# Only resolve MQTT once, even if multiple MQTT entries
if not mqtt_resolved:
try:
mqtt_ips = mqtt_get_ip(
config, args.username, args.password, args.client_id
)
network_devices.extend(mqtt_ips)
except EsphomeError as err:
_LOGGER.warning(
"MQTT IP discovery failed (%s), will try other devices if available",
err,
)
mqtt_resolved = True
elif device not in network_devices:
# Regular network address or IP - add if not already present
network_devices.append(device)
return network_devices
def get_port_type(port: str) -> str:
def get_port_type(port: str) -> PortType:
"""Determine the type of port/device identifier.
Returns:
PortType.SERIAL for serial ports (/dev/ttyUSB0, COM1, etc.)
PortType.MQTT for MQTT logging
PortType.MQTTIP for MQTT IP lookup
PortType.NETWORK for IP addresses, hostnames, or mDNS names
"""
if port.startswith("/") or port.startswith("COM"):
return "SERIAL"
return _PORT_TO_PORT_TYPE.get(port, "NETWORK")
return PortType.SERIAL
if port == "MQTT":
return PortType.MQTT
if port == "MQTTIP":
return PortType.MQTTIP
return PortType.NETWORK
def run_miniterm(config: ConfigType, port: str, args) -> int:
@@ -404,7 +468,9 @@ def write_cpp_file() -> int:
def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
from esphome import platformio_api
_LOGGER.info("Compiling app...")
# NOTE: "Build path:" format is parsed by script/ci_memory_impact_extract.py
# If you change this format, update the regex in that script as well
_LOGGER.info("Compiling app... Build path: %s", CORE.build_path)
rc = platformio_api.run_compile(config, CORE.verbose)
if rc != 0:
return rc
@@ -489,7 +555,7 @@ def upload_using_platformio(config: ConfigType, port: str):
def check_permissions(port: str):
if os.name == "posix" and get_port_type(port) == "SERIAL":
if os.name == "posix" and get_port_type(port) == PortType.SERIAL:
# Check if we can open selected serial port
if not os.access(port, os.F_OK):
raise EsphomeError(
@@ -517,7 +583,7 @@ def upload_program(
except AttributeError:
pass
if get_port_type(host) == "SERIAL":
if get_port_type(host) == PortType.SERIAL:
check_permissions(host)
exit_code = 1
@@ -544,17 +610,16 @@ def upload_program(
from esphome import espota2
remote_port = int(ota_conf[CONF_PORT])
password = ota_conf.get(CONF_PASSWORD, "")
password = ota_conf.get(CONF_PASSWORD)
if getattr(args, "file", None) is not None:
binary = Path(args.file)
else:
binary = CORE.firmware_bin
# MQTT address resolution
if get_port_type(host) in ("MQTT", "MQTTIP"):
devices = mqtt_get_ip(config, args.username, args.password, args.client_id)
# Resolve MQTT magic strings to actual IP addresses
network_devices = _resolve_network_devices(devices, config, args)
return espota2.run_ota(devices, remote_port, password, binary)
return espota2.run_ota(network_devices, remote_port, password, binary)
def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int | None:
@@ -569,33 +634,22 @@ def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int
raise EsphomeError("Logger is not configured!")
port = devices[0]
port_type = get_port_type(port)
if get_port_type(port) == "SERIAL":
if port_type == PortType.SERIAL:
check_permissions(port)
return run_miniterm(config, port, args)
port_type = get_port_type(port)
# Check if we should use API for logging
if has_api():
addresses_to_use: list[str] | None = None
# Resolve MQTT magic strings to actual IP addresses
if has_api() and (
network_devices := _resolve_network_devices(devices, config, args)
):
from esphome.components.api.client import run_logs
if port_type == "NETWORK":
# Network addresses (IPs, mDNS names, or regular DNS hostnames) can be used
# The resolve_ip_address() function in helpers.py handles all types
addresses_to_use = devices
elif port_type in ("MQTT", "MQTTIP") and has_mqtt_ip_lookup():
# Use MQTT IP lookup for MQTT/MQTTIP types
addresses_to_use = mqtt_get_ip(
config, args.username, args.password, args.client_id
)
return run_logs(config, network_devices)
if addresses_to_use is not None:
from esphome.components.api.client import run_logs
return run_logs(config, addresses_to_use)
if port_type in ("NETWORK", "MQTT") and has_mqtt_logging():
if port_type in (PortType.NETWORK, PortType.MQTT) and has_mqtt_logging():
from esphome import mqtt
return mqtt.show_logs(

View File

@@ -0,0 +1,502 @@
"""Memory usage analyzer for ESPHome compiled binaries."""
from collections import defaultdict
from dataclasses import dataclass, field
import logging
from pathlib import Path
import re
import subprocess
from typing import TYPE_CHECKING
from .const import (
CORE_SUBCATEGORY_PATTERNS,
DEMANGLED_PATTERNS,
ESPHOME_COMPONENT_PATTERN,
SECTION_TO_ATTR,
SYMBOL_PATTERNS,
)
from .helpers import (
get_component_class_patterns,
get_esphome_components,
map_section_name,
parse_symbol_line,
)
if TYPE_CHECKING:
from esphome.platformio_api import IDEData
_LOGGER = logging.getLogger(__name__)
# GCC global constructor/destructor prefix annotations
_GCC_PREFIX_ANNOTATIONS = {
"_GLOBAL__sub_I_": "global constructor for",
"_GLOBAL__sub_D_": "global destructor for",
}
# GCC optimization suffix pattern (e.g., $isra$0, $part$1, $constprop$2)
_GCC_OPTIMIZATION_SUFFIX_PATTERN = re.compile(r"(\$(?:isra|part|constprop)\$\d+)")
# C++ runtime patterns for categorization
_CPP_RUNTIME_PATTERNS = frozenset(["vtable", "typeinfo", "thunk"])
# libc printf/scanf family base names (used to detect variants like _printf_r, vfprintf, etc.)
_LIBC_PRINTF_SCANF_FAMILY = frozenset(["printf", "fprintf", "sprintf", "scanf"])
# Regex pattern for parsing readelf section headers
# Format: [ #] name type addr off size
_READELF_SECTION_PATTERN = re.compile(
r"\s*\[\s*\d+\]\s+([\.\w]+)\s+\w+\s+[\da-fA-F]+\s+[\da-fA-F]+\s+([\da-fA-F]+)"
)
# Component category prefixes
_COMPONENT_PREFIX_ESPHOME = "[esphome]"
_COMPONENT_PREFIX_EXTERNAL = "[external]"
_COMPONENT_CORE = f"{_COMPONENT_PREFIX_ESPHOME}core"
_COMPONENT_API = f"{_COMPONENT_PREFIX_ESPHOME}api"
# C++ namespace prefixes
_NAMESPACE_ESPHOME = "esphome::"
_NAMESPACE_STD = "std::"
# Type alias for symbol information: (symbol_name, size, component)
SymbolInfoType = tuple[str, int, str]
@dataclass
class MemorySection:
"""Represents a memory section with its symbols."""
name: str
symbols: list[SymbolInfoType] = field(default_factory=list)
total_size: int = 0
@dataclass
class ComponentMemory:
"""Tracks memory usage for a component."""
name: str
text_size: int = 0 # Code in flash
rodata_size: int = 0 # Read-only data in flash
data_size: int = 0 # Initialized data (flash + ram)
bss_size: int = 0 # Uninitialized data (ram only)
symbol_count: int = 0
@property
def flash_total(self) -> int:
"""Total flash usage (text + rodata + data)."""
return self.text_size + self.rodata_size + self.data_size
@property
def ram_total(self) -> int:
"""Total RAM usage (data + bss)."""
return self.data_size + self.bss_size
class MemoryAnalyzer:
"""Analyzes memory usage from ELF files."""
def __init__(
self,
elf_path: str,
objdump_path: str | None = None,
readelf_path: str | None = None,
external_components: set[str] | None = None,
idedata: "IDEData | None" = None,
) -> None:
"""Initialize memory analyzer.
Args:
elf_path: Path to ELF file to analyze
objdump_path: Path to objdump binary (auto-detected from idedata if not provided)
readelf_path: Path to readelf binary (auto-detected from idedata if not provided)
external_components: Set of external component names
idedata: Optional PlatformIO IDEData object to auto-detect toolchain paths
"""
self.elf_path = Path(elf_path)
if not self.elf_path.exists():
raise FileNotFoundError(f"ELF file not found: {elf_path}")
# Auto-detect toolchain paths from idedata if not provided
if idedata is not None and (objdump_path is None or readelf_path is None):
objdump_path = objdump_path or idedata.objdump_path
readelf_path = readelf_path or idedata.readelf_path
_LOGGER.debug("Using toolchain paths from PlatformIO idedata")
self.objdump_path = objdump_path or "objdump"
self.readelf_path = readelf_path or "readelf"
self.external_components = external_components or set()
self.sections: dict[str, MemorySection] = {}
self.components: dict[str, ComponentMemory] = defaultdict(
lambda: ComponentMemory("")
)
self._demangle_cache: dict[str, str] = {}
self._uncategorized_symbols: list[tuple[str, str, int]] = []
self._esphome_core_symbols: list[
tuple[str, str, int]
] = [] # Track core symbols
self._component_symbols: dict[str, list[tuple[str, str, int]]] = defaultdict(
list
) # Track symbols for all components
def analyze(self) -> dict[str, ComponentMemory]:
"""Analyze the ELF file and return component memory usage."""
self._parse_sections()
self._parse_symbols()
self._categorize_symbols()
return dict(self.components)
def _parse_sections(self) -> None:
"""Parse section headers from ELF file."""
result = subprocess.run(
[self.readelf_path, "-S", str(self.elf_path)],
capture_output=True,
text=True,
check=True,
)
# Parse section headers
for line in result.stdout.splitlines():
# Look for section entries
if not (match := _READELF_SECTION_PATTERN.match(line)):
continue
section_name = match.group(1)
size_hex = match.group(2)
size = int(size_hex, 16)
# Map to standard section name
mapped_section = map_section_name(section_name)
if not mapped_section:
continue
if mapped_section not in self.sections:
self.sections[mapped_section] = MemorySection(mapped_section)
self.sections[mapped_section].total_size += size
def _parse_symbols(self) -> None:
"""Parse symbols from ELF file."""
result = subprocess.run(
[self.objdump_path, "-t", str(self.elf_path)],
capture_output=True,
text=True,
check=True,
)
# Track seen addresses to avoid duplicates
seen_addresses: set[str] = set()
for line in result.stdout.splitlines():
if not (symbol_info := parse_symbol_line(line)):
continue
section, name, size, address = symbol_info
# Skip duplicate symbols at the same address (e.g., C1/C2 constructors)
if address in seen_addresses or section not in self.sections:
continue
self.sections[section].symbols.append((name, size, ""))
seen_addresses.add(address)
def _categorize_symbols(self) -> None:
"""Categorize symbols by component."""
# First, collect all unique symbol names for batch demangling
all_symbols = {
symbol_name
for section in self.sections.values()
for symbol_name, _, _ in section.symbols
}
# Batch demangle all symbols at once
self._batch_demangle_symbols(list(all_symbols))
# Now categorize with cached demangled names
for section_name, section in self.sections.items():
for symbol_name, size, _ in section.symbols:
component = self._identify_component(symbol_name)
if component not in self.components:
self.components[component] = ComponentMemory(component)
comp_mem = self.components[component]
comp_mem.symbol_count += 1
# Update the appropriate size attribute based on section
if attr_name := SECTION_TO_ATTR.get(section_name):
setattr(comp_mem, attr_name, getattr(comp_mem, attr_name) + size)
# Track uncategorized symbols
if component == "other" and size > 0:
demangled = self._demangle_symbol(symbol_name)
self._uncategorized_symbols.append((symbol_name, demangled, size))
# Track ESPHome core symbols for detailed analysis
if component == _COMPONENT_CORE and size > 0:
demangled = self._demangle_symbol(symbol_name)
self._esphome_core_symbols.append((symbol_name, demangled, size))
# Track all component symbols for detailed analysis
if size > 0:
demangled = self._demangle_symbol(symbol_name)
self._component_symbols[component].append(
(symbol_name, demangled, size)
)
def _identify_component(self, symbol_name: str) -> str:
"""Identify which component a symbol belongs to."""
# Demangle C++ names if needed
demangled = self._demangle_symbol(symbol_name)
# Check for special component classes first (before namespace pattern)
# This handles cases like esphome::ESPHomeOTAComponent which should map to ota
if _NAMESPACE_ESPHOME in demangled:
# Check for special component classes that include component name in the class
# For example: esphome::ESPHomeOTAComponent -> ota component
for component_name in get_esphome_components():
patterns = get_component_class_patterns(component_name)
if any(pattern in demangled for pattern in patterns):
return f"{_COMPONENT_PREFIX_ESPHOME}{component_name}"
# Check for ESPHome component namespaces
match = ESPHOME_COMPONENT_PATTERN.search(demangled)
if match:
component_name = match.group(1)
# Strip trailing underscore if present (e.g., switch_ -> switch)
component_name = component_name.rstrip("_")
# Check if this is an actual component in the components directory
if component_name in get_esphome_components():
return f"{_COMPONENT_PREFIX_ESPHOME}{component_name}"
# Check if this is a known external component from the config
if component_name in self.external_components:
return f"{_COMPONENT_PREFIX_EXTERNAL}{component_name}"
# Everything else in esphome:: namespace is core
return _COMPONENT_CORE
# Check for esphome core namespace (no component namespace)
if _NAMESPACE_ESPHOME in demangled:
# If no component match found, it's core
return _COMPONENT_CORE
# Check against symbol patterns
for component, patterns in SYMBOL_PATTERNS.items():
if any(pattern in symbol_name for pattern in patterns):
return component
# Check against demangled patterns
for component, patterns in DEMANGLED_PATTERNS.items():
if any(pattern in demangled for pattern in patterns):
return component
# Special cases that need more complex logic
# Check if spi_flash vs spi_driver
if "spi_" in symbol_name or "SPI" in symbol_name:
return "spi_flash" if "spi_flash" in symbol_name else "spi_driver"
# libc special printf variants
if (
symbol_name.startswith("_")
and symbol_name[1:].replace("_r", "").replace("v", "").replace("s", "")
in _LIBC_PRINTF_SCANF_FAMILY
):
return "libc"
# Track uncategorized symbols for analysis
return "other"
def _batch_demangle_symbols(self, symbols: list[str]) -> None:
"""Batch demangle C++ symbol names for efficiency."""
if not symbols:
return
# Try to find the appropriate c++filt for the platform
cppfilt_cmd = "c++filt"
_LOGGER.info("Demangling %d symbols", len(symbols))
_LOGGER.debug("objdump_path = %s", self.objdump_path)
# Check if we have a toolchain-specific c++filt
if self.objdump_path and self.objdump_path != "objdump":
# Replace objdump with c++filt in the path
potential_cppfilt = self.objdump_path.replace("objdump", "c++filt")
_LOGGER.info("Checking for toolchain c++filt at: %s", potential_cppfilt)
if Path(potential_cppfilt).exists():
cppfilt_cmd = potential_cppfilt
_LOGGER.info("✓ Using toolchain c++filt: %s", cppfilt_cmd)
else:
_LOGGER.info(
"✗ Toolchain c++filt not found at %s, using system c++filt",
potential_cppfilt,
)
else:
_LOGGER.info("✗ Using system c++filt (objdump_path=%s)", self.objdump_path)
# Strip GCC optimization suffixes and prefixes before demangling
# Suffixes like $isra$0, $part$0, $constprop$0 confuse c++filt
# Prefixes like _GLOBAL__sub_I_ need to be removed and tracked
symbols_stripped: list[str] = []
symbols_prefixes: list[str] = [] # Track removed prefixes
for symbol in symbols:
# Remove GCC optimization markers
stripped = _GCC_OPTIMIZATION_SUFFIX_PATTERN.sub("", symbol)
# Handle GCC global constructor/initializer prefixes
# _GLOBAL__sub_I_<mangled> -> extract <mangled> for demangling
prefix = ""
for gcc_prefix in _GCC_PREFIX_ANNOTATIONS:
if stripped.startswith(gcc_prefix):
prefix = gcc_prefix
stripped = stripped[len(prefix) :]
break
symbols_stripped.append(stripped)
symbols_prefixes.append(prefix)
try:
# Send all symbols to c++filt at once
result = subprocess.run(
[cppfilt_cmd],
input="\n".join(symbols_stripped),
capture_output=True,
text=True,
check=False,
)
except (subprocess.SubprocessError, OSError, UnicodeDecodeError) as e:
# On error, cache originals
_LOGGER.warning("Failed to batch demangle symbols: %s", e)
for symbol in symbols:
self._demangle_cache[symbol] = symbol
return
if result.returncode != 0:
_LOGGER.warning(
"c++filt exited with code %d: %s",
result.returncode,
result.stderr[:200] if result.stderr else "(no error output)",
)
# Cache originals on failure
for symbol in symbols:
self._demangle_cache[symbol] = symbol
return
# Process demangled output
self._process_demangled_output(
symbols, symbols_stripped, symbols_prefixes, result.stdout, cppfilt_cmd
)
def _process_demangled_output(
self,
symbols: list[str],
symbols_stripped: list[str],
symbols_prefixes: list[str],
demangled_output: str,
cppfilt_cmd: str,
) -> None:
"""Process demangled symbol output and populate cache.
Args:
symbols: Original symbol names
symbols_stripped: Stripped symbol names sent to c++filt
symbols_prefixes: Removed prefixes to restore
demangled_output: Output from c++filt
cppfilt_cmd: Path to c++filt command (for logging)
"""
demangled_lines = demangled_output.strip().split("\n")
failed_count = 0
for original, stripped, prefix, demangled in zip(
symbols, symbols_stripped, symbols_prefixes, demangled_lines
):
# Add back any prefix that was removed
demangled = self._restore_symbol_prefix(prefix, stripped, demangled)
# If we stripped a suffix, add it back to the demangled name for clarity
if original != stripped and not prefix:
demangled = self._restore_symbol_suffix(original, demangled)
self._demangle_cache[original] = demangled
# Log symbols that failed to demangle (stayed the same as stripped version)
if stripped == demangled and stripped.startswith("_Z"):
failed_count += 1
if failed_count <= 5: # Only log first 5 failures
_LOGGER.warning("Failed to demangle: %s", original)
if failed_count == 0:
_LOGGER.info("Successfully demangled all %d symbols", len(symbols))
return
_LOGGER.warning(
"Failed to demangle %d/%d symbols using %s",
failed_count,
len(symbols),
cppfilt_cmd,
)
@staticmethod
def _restore_symbol_prefix(prefix: str, stripped: str, demangled: str) -> str:
"""Restore prefix that was removed before demangling.
Args:
prefix: Prefix that was removed (e.g., "_GLOBAL__sub_I_")
stripped: Stripped symbol name
demangled: Demangled symbol name
Returns:
Demangled name with prefix restored/annotated
"""
if not prefix:
return demangled
# Successfully demangled - add descriptive prefix
if demangled != stripped and (
annotation := _GCC_PREFIX_ANNOTATIONS.get(prefix)
):
return f"[{annotation}: {demangled}]"
# Failed to demangle - restore original prefix
return prefix + demangled
@staticmethod
def _restore_symbol_suffix(original: str, demangled: str) -> str:
"""Restore GCC optimization suffix that was removed before demangling.
Args:
original: Original symbol name with suffix
demangled: Demangled symbol name without suffix
Returns:
Demangled name with suffix annotation
"""
if suffix_match := _GCC_OPTIMIZATION_SUFFIX_PATTERN.search(original):
return f"{demangled} [{suffix_match.group(1)}]"
return demangled
def _demangle_symbol(self, symbol: str) -> str:
"""Get demangled C++ symbol name from cache."""
return self._demangle_cache.get(symbol, symbol)
def _categorize_esphome_core_symbol(self, demangled: str) -> str:
"""Categorize ESPHome core symbols into subcategories."""
# Special patterns that need to be checked separately
if any(pattern in demangled for pattern in _CPP_RUNTIME_PATTERNS):
return "C++ Runtime (vtables/RTTI)"
if demangled.startswith(_NAMESPACE_STD):
return "C++ STL"
# Check against patterns from const.py
for category, patterns in CORE_SUBCATEGORY_PATTERNS.items():
if any(pattern in demangled for pattern in patterns):
return category
return "Other Core"
if __name__ == "__main__":
from .cli import main
main()

View File

@@ -0,0 +1,6 @@
"""Main entry point for running the memory analyzer as a module."""
from .cli import main
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,408 @@
"""CLI interface for memory analysis with report generation."""
from collections import defaultdict
import sys
from . import (
_COMPONENT_API,
_COMPONENT_CORE,
_COMPONENT_PREFIX_ESPHOME,
_COMPONENT_PREFIX_EXTERNAL,
MemoryAnalyzer,
)
class MemoryAnalyzerCLI(MemoryAnalyzer):
"""Memory analyzer with CLI-specific report generation."""
# Column width constants
COL_COMPONENT: int = 29
COL_FLASH_TEXT: int = 14
COL_FLASH_DATA: int = 14
COL_RAM_DATA: int = 12
COL_RAM_BSS: int = 12
COL_TOTAL_FLASH: int = 15
COL_TOTAL_RAM: int = 12
COL_SEPARATOR: int = 3 # " | "
# Core analysis column widths
COL_CORE_SUBCATEGORY: int = 30
COL_CORE_SIZE: int = 12
COL_CORE_COUNT: int = 6
COL_CORE_PERCENT: int = 10
# Calculate table width once at class level
TABLE_WIDTH: int = (
COL_COMPONENT
+ COL_SEPARATOR
+ COL_FLASH_TEXT
+ COL_SEPARATOR
+ COL_FLASH_DATA
+ COL_SEPARATOR
+ COL_RAM_DATA
+ COL_SEPARATOR
+ COL_RAM_BSS
+ COL_SEPARATOR
+ COL_TOTAL_FLASH
+ COL_SEPARATOR
+ COL_TOTAL_RAM
)
@staticmethod
def _make_separator_line(*widths: int) -> str:
"""Create a separator line with given column widths.
Args:
widths: Column widths to create separators for
Returns:
Separator line like "----+---------+-----"
"""
return "-+-".join("-" * width for width in widths)
# Pre-computed separator lines
MAIN_TABLE_SEPARATOR: str = _make_separator_line(
COL_COMPONENT,
COL_FLASH_TEXT,
COL_FLASH_DATA,
COL_RAM_DATA,
COL_RAM_BSS,
COL_TOTAL_FLASH,
COL_TOTAL_RAM,
)
CORE_TABLE_SEPARATOR: str = _make_separator_line(
COL_CORE_SUBCATEGORY,
COL_CORE_SIZE,
COL_CORE_COUNT,
COL_CORE_PERCENT,
)
def generate_report(self, detailed: bool = False) -> str:
"""Generate a formatted memory report."""
components = sorted(
self.components.items(), key=lambda x: x[1].flash_total, reverse=True
)
# Calculate totals
total_flash = sum(c.flash_total for _, c in components)
total_ram = sum(c.ram_total for _, c in components)
# Build report
lines: list[str] = []
lines.append("=" * self.TABLE_WIDTH)
lines.append("Component Memory Analysis".center(self.TABLE_WIDTH))
lines.append("=" * self.TABLE_WIDTH)
lines.append("")
# Main table - fixed column widths
lines.append(
f"{'Component':<{self.COL_COMPONENT}} | {'Flash (text)':>{self.COL_FLASH_TEXT}} | {'Flash (data)':>{self.COL_FLASH_DATA}} | {'RAM (data)':>{self.COL_RAM_DATA}} | {'RAM (bss)':>{self.COL_RAM_BSS}} | {'Total Flash':>{self.COL_TOTAL_FLASH}} | {'Total RAM':>{self.COL_TOTAL_RAM}}"
)
lines.append(self.MAIN_TABLE_SEPARATOR)
for name, mem in components:
if mem.flash_total > 0 or mem.ram_total > 0:
flash_rodata = mem.rodata_size + mem.data_size
lines.append(
f"{name:<{self.COL_COMPONENT}} | {mem.text_size:>{self.COL_FLASH_TEXT - 2},} B | {flash_rodata:>{self.COL_FLASH_DATA - 2},} B | "
f"{mem.data_size:>{self.COL_RAM_DATA - 2},} B | {mem.bss_size:>{self.COL_RAM_BSS - 2},} B | "
f"{mem.flash_total:>{self.COL_TOTAL_FLASH - 2},} B | {mem.ram_total:>{self.COL_TOTAL_RAM - 2},} B"
)
lines.append(self.MAIN_TABLE_SEPARATOR)
lines.append(
f"{'TOTAL':<{self.COL_COMPONENT}} | {' ':>{self.COL_FLASH_TEXT}} | {' ':>{self.COL_FLASH_DATA}} | "
f"{' ':>{self.COL_RAM_DATA}} | {' ':>{self.COL_RAM_BSS}} | "
f"{total_flash:>{self.COL_TOTAL_FLASH - 2},} B | {total_ram:>{self.COL_TOTAL_RAM - 2},} B"
)
# Top consumers
lines.append("")
lines.append("Top Flash Consumers:")
for i, (name, mem) in enumerate(components[:25]):
if mem.flash_total > 0:
percentage = (
(mem.flash_total / total_flash * 100) if total_flash > 0 else 0
)
lines.append(
f"{i + 1}. {name} ({mem.flash_total:,} B) - {percentage:.1f}% of analyzed flash"
)
lines.append("")
lines.append("Top RAM Consumers:")
ram_components = sorted(components, key=lambda x: x[1].ram_total, reverse=True)
for i, (name, mem) in enumerate(ram_components[:25]):
if mem.ram_total > 0:
percentage = (mem.ram_total / total_ram * 100) if total_ram > 0 else 0
lines.append(
f"{i + 1}. {name} ({mem.ram_total:,} B) - {percentage:.1f}% of analyzed RAM"
)
lines.append("")
lines.append(
"Note: This analysis covers symbols in the ELF file. Some runtime allocations may not be included."
)
lines.append("=" * self.TABLE_WIDTH)
# Add ESPHome core detailed analysis if there are core symbols
if self._esphome_core_symbols:
lines.append("")
lines.append("=" * self.TABLE_WIDTH)
lines.append(
f"{_COMPONENT_CORE} Detailed Analysis".center(self.TABLE_WIDTH)
)
lines.append("=" * self.TABLE_WIDTH)
lines.append("")
# Group core symbols by subcategory
core_subcategories: dict[str, list[tuple[str, str, int]]] = defaultdict(
list
)
for symbol, demangled, size in self._esphome_core_symbols:
# Categorize based on demangled name patterns
subcategory = self._categorize_esphome_core_symbol(demangled)
core_subcategories[subcategory].append((symbol, demangled, size))
# Sort subcategories by total size
sorted_subcategories = sorted(
[
(name, symbols, sum(s[2] for s in symbols))
for name, symbols in core_subcategories.items()
],
key=lambda x: x[2],
reverse=True,
)
lines.append(
f"{'Subcategory':<{self.COL_CORE_SUBCATEGORY}} | {'Size':>{self.COL_CORE_SIZE}} | "
f"{'Count':>{self.COL_CORE_COUNT}} | {'% of Core':>{self.COL_CORE_PERCENT}}"
)
lines.append(self.CORE_TABLE_SEPARATOR)
core_total = sum(size for _, _, size in self._esphome_core_symbols)
for subcategory, symbols, total_size in sorted_subcategories:
percentage = (total_size / core_total * 100) if core_total > 0 else 0
lines.append(
f"{subcategory:<{self.COL_CORE_SUBCATEGORY}} | {total_size:>{self.COL_CORE_SIZE - 2},} B | "
f"{len(symbols):>{self.COL_CORE_COUNT}} | {percentage:>{self.COL_CORE_PERCENT - 1}.1f}%"
)
# Top 15 largest core symbols
lines.append("")
lines.append(f"Top 15 Largest {_COMPONENT_CORE} Symbols:")
sorted_core_symbols = sorted(
self._esphome_core_symbols, key=lambda x: x[2], reverse=True
)
for i, (symbol, demangled, size) in enumerate(sorted_core_symbols[:15]):
lines.append(f"{i + 1}. {demangled} ({size:,} B)")
lines.append("=" * self.TABLE_WIDTH)
# Add detailed analysis for top ESPHome and external components
esphome_components = [
(name, mem)
for name, mem in components
if name.startswith(_COMPONENT_PREFIX_ESPHOME) and name != _COMPONENT_CORE
]
external_components = [
(name, mem)
for name, mem in components
if name.startswith(_COMPONENT_PREFIX_EXTERNAL)
]
top_esphome_components = sorted(
esphome_components, key=lambda x: x[1].flash_total, reverse=True
)[:30]
# Include all external components (they're usually important)
top_external_components = sorted(
external_components, key=lambda x: x[1].flash_total, reverse=True
)
# Check if API component exists and ensure it's included
api_component = None
for name, mem in components:
if name == _COMPONENT_API:
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
)
if api_component and api_component not in components_to_analyze:
components_to_analyze.append(api_component)
if components_to_analyze:
for comp_name, comp_mem in components_to_analyze:
if not (comp_symbols := self._component_symbols.get(comp_name, [])):
continue
lines.append("")
lines.append("=" * self.TABLE_WIDTH)
lines.append(f"{comp_name} Detailed Analysis".center(self.TABLE_WIDTH))
lines.append("=" * self.TABLE_WIDTH)
lines.append("")
# Sort symbols by size
sorted_symbols = sorted(comp_symbols, key=lambda x: x[2], reverse=True)
lines.append(f"Total symbols: {len(sorted_symbols)}")
lines.append(f"Total size: {comp_mem.flash_total:,} B")
lines.append("")
# Show all symbols > 100 bytes for better visibility
large_symbols = [
(sym, dem, size) for sym, dem, size in sorted_symbols if size > 100
]
lines.append(
f"{comp_name} Symbols > 100 B ({len(large_symbols)} symbols):"
)
for i, (symbol, demangled, size) in enumerate(large_symbols):
lines.append(f"{i + 1}. {demangled} ({size:,} B)")
lines.append("=" * self.TABLE_WIDTH)
return "\n".join(lines)
def dump_uncategorized_symbols(self, output_file: str | None = None) -> None:
"""Dump uncategorized symbols for analysis."""
# Sort by size descending
sorted_symbols = sorted(
self._uncategorized_symbols, key=lambda x: x[2], reverse=True
)
lines = ["Uncategorized Symbols Analysis", "=" * 80]
lines.append(f"Total uncategorized symbols: {len(sorted_symbols)}")
lines.append(
f"Total uncategorized size: {sum(s[2] for s in sorted_symbols):,} bytes"
)
lines.append("")
lines.append(f"{'Size':>10} | {'Symbol':<60} | Demangled")
lines.append("-" * 10 + "-+-" + "-" * 60 + "-+-" + "-" * 40)
for symbol, demangled, size in sorted_symbols[:100]: # Top 100
demangled_display = (
demangled[:100] if symbol != demangled else "[not demangled]"
)
lines.append(f"{size:>10,} | {symbol[:60]:<60} | {demangled_display}")
if len(sorted_symbols) > 100:
lines.append(f"\n... and {len(sorted_symbols) - 100} more symbols")
content = "\n".join(lines)
if output_file:
with open(output_file, "w", encoding="utf-8") as f:
f.write(content)
else:
print(content)
def analyze_elf(
elf_path: str,
objdump_path: str | None = None,
readelf_path: str | None = None,
detailed: bool = False,
external_components: set[str] | None = None,
) -> str:
"""Analyze an ELF file and return a memory report."""
analyzer = MemoryAnalyzerCLI(
elf_path, objdump_path, readelf_path, external_components
)
analyzer.analyze()
return analyzer.generate_report(detailed)
def main():
"""CLI entrypoint for memory analysis."""
if len(sys.argv) < 2:
print("Usage: python -m esphome.analyze_memory <build_directory>")
print("\nAnalyze memory usage from an ESPHome build directory.")
print("The build directory should contain firmware.elf and idedata will be")
print("loaded from ~/.esphome/.internal/idedata/<device>.json")
print("\nExamples:")
print(" python -m esphome.analyze_memory ~/.esphome/build/my-device")
print(" python -m esphome.analyze_memory .esphome/build/my-device")
print(" python -m esphome.analyze_memory my-device # Short form")
sys.exit(1)
build_dir = sys.argv[1]
# Load build directory
import json
from pathlib import Path
from esphome.platformio_api import IDEData
build_path = Path(build_dir)
# If no path separator in name, assume it's a device name
if "/" not in build_dir and not build_path.is_dir():
# Try current directory first
cwd_path = Path.cwd() / ".esphome" / "build" / build_dir
if cwd_path.is_dir():
build_path = cwd_path
print(f"Using build directory: {build_path}", file=sys.stderr)
else:
# Fall back to home directory
build_path = Path.home() / ".esphome" / "build" / build_dir
print(f"Using build directory: {build_path}", file=sys.stderr)
if not build_path.is_dir():
print(f"Error: {build_path} is not a directory", file=sys.stderr)
sys.exit(1)
# Find firmware.elf
elf_file = None
for elf_candidate in [
build_path / "firmware.elf",
build_path / ".pioenvs" / build_path.name / "firmware.elf",
]:
if elf_candidate.exists():
elf_file = str(elf_candidate)
break
if not elf_file:
print(f"Error: firmware.elf not found in {build_dir}", file=sys.stderr)
sys.exit(1)
# Find idedata.json - check current directory first, then home
device_name = build_path.name
idedata_candidates = [
Path.cwd() / ".esphome" / "idedata" / f"{device_name}.json",
Path.home() / ".esphome" / "idedata" / f"{device_name}.json",
]
idedata = None
for idedata_path in idedata_candidates:
if not idedata_path.exists():
continue
try:
with open(idedata_path, encoding="utf-8") as f:
raw_data = json.load(f)
idedata = IDEData(raw_data)
print(f"Loaded idedata from: {idedata_path}", file=sys.stderr)
break
except (json.JSONDecodeError, OSError) as e:
print(f"Warning: Failed to load idedata: {e}", file=sys.stderr)
if not idedata:
print(
f"Warning: idedata not found (searched {idedata_candidates[0]} and {idedata_candidates[1]})",
file=sys.stderr,
)
analyzer = MemoryAnalyzerCLI(elf_file, idedata=idedata)
analyzer.analyze()
report = analyzer.generate_report()
print(report)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,903 @@
"""Constants for memory analysis symbol pattern matching."""
import re
# Pattern to extract ESPHome component namespaces dynamically
ESPHOME_COMPONENT_PATTERN = re.compile(r"esphome::([a-zA-Z0-9_]+)::")
# Section mapping for ELF file sections
# Maps standard section names to their various platform-specific variants
SECTION_MAPPING = {
".text": frozenset([".text", ".iram"]),
".rodata": frozenset([".rodata"]),
".data": frozenset([".data", ".dram"]),
".bss": frozenset([".bss"]),
}
# Section to ComponentMemory attribute mapping
# Maps section names to the attribute name in ComponentMemory dataclass
SECTION_TO_ATTR = {
".text": "text_size",
".rodata": "rodata_size",
".data": "data_size",
".bss": "bss_size",
}
# Component identification rules
# Symbol patterns: patterns found in raw symbol names
SYMBOL_PATTERNS = {
"freertos": [
"vTask",
"xTask",
"xQueue",
"pvPort",
"vPort",
"uxTask",
"pcTask",
"prvTimerTask",
"prvAddNewTaskToReadyList",
"pxReadyTasksLists",
"prvAddCurrentTaskToDelayedList",
"xEventGroupWaitBits",
"xRingbufferSendFromISR",
"prvSendItemDoneNoSplit",
"prvReceiveGeneric",
"prvSendAcquireGeneric",
"prvCopyItemAllowSplit",
"xEventGroup",
"xRingbuffer",
"prvSend",
"prvReceive",
"prvCopy",
"xPort",
"ulTaskGenericNotifyTake",
"prvIdleTask",
"prvInitialiseNewTask",
"prvIsYieldRequiredSMP",
"prvGetItemByteBuf",
"prvInitializeNewRingbuffer",
"prvAcquireItemNoSplit",
"prvNotifyQueueSetContainer",
"ucStaticTimerQueueStorage",
"eTaskGetState",
"main_task",
"do_system_init_fn",
"xSemaphoreCreateGenericWithCaps",
"vListInsert",
"uxListRemove",
"vRingbufferReturnItem",
"vRingbufferReturnItemFromISR",
"prvCheckItemFitsByteBuffer",
"prvGetCurMaxSizeAllowSplit",
"tick_hook",
"sys_sem_new",
"sys_arch_mbox_fetch",
"sys_arch_sem_wait",
"prvDeleteTCB",
"vQueueDeleteWithCaps",
"vRingbufferDeleteWithCaps",
"vSemaphoreDeleteWithCaps",
"prvCheckItemAvail",
"prvCheckTaskCanBeScheduledSMP",
"prvGetCurMaxSizeNoSplit",
"prvResetNextTaskUnblockTime",
"prvReturnItemByteBuf",
"vApplicationStackOverflowHook",
"vApplicationGetIdleTaskMemory",
"sys_init",
"sys_mbox_new",
"sys_arch_mbox_tryfetch",
],
"xtensa": ["xt_", "_xt_", "xPortEnterCriticalTimeout"],
"heap": ["heap_", "multi_heap"],
"spi_flash": ["spi_flash"],
"rtc": ["rtc_", "rtcio_ll_"],
"gpio_driver": ["gpio_", "pins"],
"uart_driver": ["uart", "_uart", "UART"],
"timer": ["timer_", "esp_timer"],
"peripherals": ["periph_", "periman"],
"network_stack": [
"vj_compress",
"raw_sendto",
"raw_input",
"etharp_",
"icmp_input",
"socket_ipv6",
"ip_napt",
"socket_ipv4_multicast",
"socket_ipv6_multicast",
"netconn_",
"recv_raw",
"accept_function",
"netconn_recv_data",
"netconn_accept",
"netconn_write_vectors_partly",
"netconn_drain",
"raw_connect",
"raw_bind",
"icmp_send_response",
"sockets",
"icmp_dest_unreach",
"inet_chksum_pseudo",
"alloc_socket",
"done_socket",
"set_global_fd_sets",
"inet_chksum_pbuf",
"tryget_socket_unconn_locked",
"tryget_socket_unconn",
"cs_create_ctrl_sock",
"netbuf_alloc",
],
"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",
],
"bluetooth": ["bt_", "ble_", "l2c_", "gatt_", "gap_", "hci_", "BT_init"],
"wifi_bt_coex": ["coex"],
"bluetooth_rom": ["r_ble", "r_lld", "r_llc", "r_llm"],
"bluedroid_bt": [
"bluedroid",
"btc_",
"bta_",
"btm_",
"btu_",
"BTM_",
"GATT",
"L2CA_",
"smp_",
"gatts_",
"attp_",
"l2cu_",
"l2cb",
"smp_cb",
"BTA_GATTC_",
"SMP_",
"BTU_",
"BTA_Dm",
"GAP_Ble",
"BT_tx_if",
"host_recv_pkt_cb",
"saved_local_oob_data",
"string_to_bdaddr",
"string_is_bdaddr",
"CalConnectParamTimeout",
"transmit_fragment",
"transmit_data",
"event_command_ready",
"read_command_complete_header",
"parse_read_local_extended_features_response",
"parse_read_local_version_info_response",
"should_request_high",
"btdm_wakeup_request",
"BTA_SetAttributeValue",
"BTA_EnableBluetooth",
"transmit_command_futured",
"transmit_command",
"get_waiting_command",
"make_command",
"transmit_downward",
"host_recv_adv_packet",
"copy_extra_byte_in_db",
"parse_read_local_supported_commands_response",
],
"crypto_math": [
"ecp_",
"bignum_",
"mpi_",
"sswu",
"modp",
"dragonfly_",
"gcm_mult",
"__multiply",
"quorem",
"__mdiff",
"__lshift",
"__mprec_tens",
"ECC_",
"multiprecision_",
"mix_sub_columns",
"sbox",
"gfm2_sbox",
"gfm3_sbox",
"curve_p256",
"curve",
"p_256_init_curve",
"shift_sub_rows",
"rshift",
],
"hw_crypto": ["esp_aes", "esp_sha", "esp_rsa", "esp_bignum", "esp_mpi"],
"libc": [
"printf",
"scanf",
"malloc",
"free",
"memcpy",
"memset",
"strcpy",
"strlen",
"_dtoa",
"_fopen",
"__sfvwrite_r",
"qsort",
"__sf",
"__sflush_r",
"__srefill_r",
"_impure_data",
"_reclaim_reent",
"_open_r",
"strncpy",
"_strtod_l",
"__gethex",
"__hexnan",
"_setenv_r",
"_tzset_unlocked_r",
"__tzcalc_limits",
"select",
"scalbnf",
"strtof",
"strtof_l",
"__d2b",
"__b2d",
"__s2b",
"_Balloc",
"__multadd",
"__lo0bits",
"__atexit0",
"__smakebuf_r",
"__swhatbuf_r",
"_sungetc_r",
"_close_r",
"_link_r",
"_unsetenv_r",
"_rename_r",
"__month_lengths",
"tzinfo",
"__ratio",
"__hi0bits",
"__ulp",
"__any_on",
"__copybits",
"L_shift",
"_fcntl_r",
"_lseek_r",
"_read_r",
"_write_r",
"_unlink_r",
"_fstat_r",
"access",
"fsync",
"tcsetattr",
"tcgetattr",
"tcflush",
"tcdrain",
"__ssrefill_r",
"_stat_r",
"__hexdig_fun",
"__mcmp",
"_fwalk_sglue",
"__fpclassifyf",
"_setlocale_r",
"_mbrtowc_r",
"fcntl",
"__match",
"_lock_close",
"__c$",
"__func__$",
"__FUNCTION__$",
"DAYS_IN_MONTH",
"_DAYS_BEFORE_MONTH",
"CSWTCH$",
"dst$",
"sulp",
],
"string_ops": ["strcmp", "strncmp", "strchr", "strstr", "strtok", "strdup"],
"memory_alloc": ["malloc", "calloc", "realloc", "free", "_sbrk"],
"file_io": [
"fread",
"fwrite",
"fopen",
"fclose",
"fseek",
"ftell",
"fflush",
"s_fd_table",
],
"string_formatting": [
"snprintf",
"vsnprintf",
"sprintf",
"vsprintf",
"sscanf",
"vsscanf",
],
"cpp_anonymous": ["_GLOBAL__N_", "n$"],
"cpp_runtime": ["__cxx", "_ZN", "_ZL", "_ZSt", "__gxx_personality", "_Z16"],
"exception_handling": ["__cxa_", "_Unwind_", "__gcc_personality", "uw_frame_state"],
"static_init": ["_GLOBAL__sub_I_"],
"mdns_lib": ["mdns"],
"phy_radio": [
"phy_",
"rf_",
"chip_",
"register_chipv7",
"pbus_",
"bb_",
"fe_",
"rfcal_",
"ram_rfcal",
"tx_pwctrl",
"rx_chan",
"set_rx_gain",
"set_chan",
"agc_reg",
"ram_txiq",
"ram_txdc",
"ram_gen_rx_gain",
"rx_11b_opt",
"set_rx_sense",
"set_rx_gain_cal",
"set_chan_dig_gain",
"tx_pwctrl_init_cal",
"rfcal_txiq",
"set_tx_gain_table",
"correct_rfpll_offset",
"pll_correct_dcap",
"txiq_cal_init",
"pwdet_sar",
"pwdet_sar2_init",
"ram_iq_est_enable",
"ram_rfpll_set_freq",
"ant_wifirx_cfg",
"ant_btrx_cfg",
"force_txrxoff",
"force_txrx_off",
"tx_paon_set",
"opt_11b_resart",
"rfpll_1p2_opt",
"ram_dc_iq_est",
"ram_start_tx_tone",
"ram_en_pwdet",
"ram_cbw2040_cfg",
"rxdc_est_min",
"i2cmst_reg_init",
"temprature_sens_read",
"ram_restart_cal",
"ram_write_gain_mem",
"ram_wait_rfpll_cal_end",
"txcal_debuge_mode",
"ant_wifitx_cfg",
"reg_init_begin",
],
"wifi_phy_pp": ["pp_", "ppT", "ppR", "ppP", "ppInstall", "ppCalTxAMPDULength"],
"wifi_lmac": ["lmac"],
"wifi_device": ["wdev", "wDev_"],
"power_mgmt": [
"pm_",
"sleep",
"rtc_sleep",
"light_sleep",
"deep_sleep",
"power_down",
"g_pm",
],
"memory_mgmt": [
"mem_",
"memory_",
"tlsf_",
"memp_",
"pbuf_",
"pbuf_alloc",
"pbuf_copy_partial_pbuf",
],
"hal_layer": ["hal_"],
"clock_mgmt": [
"clk_",
"clock_",
"rtc_clk",
"apb_",
"cpu_freq",
"setCpuFrequencyMhz",
],
"cache_mgmt": ["cache"],
"flash_ops": ["flash", "image_load"],
"interrupt_handlers": [
"isr",
"interrupt",
"intr_",
"exc_",
"exception",
"port_IntStack",
],
"wrapper_functions": ["_wrapper"],
"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_",
"phy_tlk110",
"phy_lan87",
"phy_ip101",
"phy_rtl",
"phy_dp83",
"phy_ksz",
"lan87xx_",
"rtl8201_",
"ip101_",
"ksz80xx_",
"jl1101_",
"dp83848_",
"eth_on_state_changed",
],
"threading": ["pthread_", "thread_", "_task_"],
"pthread": ["pthread"],
"synchronization": ["mutex", "semaphore", "spinlock", "portMUX"],
"math_lib": [
"sin",
"cos",
"tan",
"sqrt",
"pow",
"exp",
"log",
"atan",
"asin",
"acos",
"floor",
"ceil",
"fabs",
"round",
],
"random": ["rand", "random", "rng_", "prng"],
"time_lib": [
"time",
"clock",
"gettimeofday",
"settimeofday",
"localtime",
"gmtime",
"mktime",
"strftime",
],
"console_io": ["console_", "uart_tx", "uart_rx", "puts", "putchar", "getchar"],
"rom_functions": ["r_", "rom_"],
"compiler_runtime": [
"__divdi3",
"__udivdi3",
"__moddi3",
"__muldi3",
"__ashldi3",
"__ashrdi3",
"__lshrdi3",
"__cmpdi2",
"__fixdfdi",
"__floatdidf",
],
"libgcc": ["libgcc", "_divdi3", "_udivdi3"],
"boot_startup": ["boot", "start_cpu", "call_start", "startup", "bootloader"],
"bootloader": ["bootloader_", "esp_bootloader"],
"app_framework": ["app_", "initArduino", "setup", "loop", "Update"],
"weak_symbols": ["__weak_"],
"compiler_builtins": ["__builtin_"],
"vfs": ["vfs_", "VFS"],
"esp32_sdk": ["esp32_", "esp32c", "esp32s"],
"usb": ["usb_", "USB", "cdc_", "CDC"],
"i2c_driver": ["i2c_", "I2C"],
"i2s_driver": ["i2s_", "I2S"],
"spi_driver": ["spi_", "SPI"],
"adc_driver": ["adc_", "ADC"],
"dac_driver": ["dac_", "DAC"],
"touch_driver": ["touch_", "TOUCH"],
"pwm_driver": ["pwm_", "PWM", "ledc_", "LEDC"],
"rmt_driver": ["rmt_", "RMT"],
"pcnt_driver": ["pcnt_", "PCNT"],
"can_driver": ["can_", "CAN", "twai_", "TWAI"],
"sdmmc_driver": ["sdmmc_", "SDMMC", "sdcard", "sd_card"],
"temp_sensor": ["temp_sensor", "tsens_"],
"watchdog": ["wdt_", "WDT", "watchdog"],
"brownout": ["brownout", "bod_"],
"ulp": ["ulp_", "ULP"],
"psram": ["psram", "PSRAM", "spiram", "SPIRAM"],
"efuse": ["efuse", "EFUSE"],
"partition": ["partition", "esp_partition"],
"esp_event": ["esp_event", "event_loop", "event_callback"],
"esp_console": ["esp_console", "console_"],
"chip_specific": ["chip_", "esp_chip"],
"esp_system_utils": ["esp_system", "esp_hw", "esp_clk", "esp_sleep"],
"ipc": ["esp_ipc", "ipc_"],
"wifi_config": [
"g_cnxMgr",
"gChmCxt",
"g_ic",
"TxRxCxt",
"s_dp",
"s_ni",
"s_reg_dump",
"packet$",
"d_mult_table",
"K",
"fcstab",
],
"smartconfig": ["sc_ack_send"],
"rc_calibration": ["rc_cal", "rcUpdate"],
"noise_floor": ["noise_check"],
"rf_calibration": [
"set_rx_sense",
"set_rx_gain_cal",
"set_chan_dig_gain",
"tx_pwctrl_init_cal",
"rfcal_txiq",
"set_tx_gain_table",
"correct_rfpll_offset",
"pll_correct_dcap",
"txiq_cal_init",
"pwdet_sar",
"rx_11b_opt",
],
"wifi_crypto": [
"pk_use_ecparams",
"process_segments",
"ccmp_",
"rc4_",
"aria_",
"mgf_mask",
"dh_group",
"ccmp_aad_nonce",
"ccmp_encrypt",
"rc4_skip",
"aria_sb1",
"aria_sb2",
"aria_is1",
"aria_is2",
"aria_sl",
"aria_a",
],
"radio_control": ["fsm_input", "fsm_sconfreq"],
"pbuf": [
"pbuf_",
],
"event_group": ["xEventGroup"],
"ringbuffer": ["xRingbuffer", "prvSend", "prvReceive", "prvCopy"],
"provisioning": ["prov_", "prov_stop_and_notify"],
"scan": ["gScanStruct"],
"port": ["xPort"],
"elf_loader": [
"elf_add",
"elf_add_note",
"elf_add_segment",
"process_image",
"read_encoded",
"read_encoded_value",
"read_encoded_value_with_base",
"process_image_header",
],
"socket_api": [
"sockets",
"netconn_",
"accept_function",
"recv_raw",
"socket_ipv4_multicast",
"socket_ipv6_multicast",
],
"igmp": ["igmp_", "igmp_send", "igmp_input"],
"icmp6": ["icmp6_"],
"arp": ["arp_table"],
"ampdu": [
"ampdu_",
"rcAmpdu",
"trc_onAmpduOp",
"rcAmpduLowerRate",
"ampdu_dispatch_upto",
],
"ieee802_11": ["ieee802_11_", "ieee802_11_parse_elems"],
"rate_control": ["rssi_margin", "rcGetSched", "get_rate_fcc_index"],
"nan": ["nan_dp_", "nan_dp_post_tx", "nan_dp_delete_peer"],
"channel_mgmt": ["chm_init", "chm_set_current_channel"],
"trace": ["trc_init", "trc_onAmpduOp"],
"country_code": ["country_info", "country_info_24ghz"],
"multicore": ["do_multicore_settings"],
"Update_lib": ["Update"],
"stdio": [
"__sf",
"__sflush_r",
"__srefill_r",
"_impure_data",
"_reclaim_reent",
"_open_r",
],
"strncpy_ops": ["strncpy"],
"math_internal": ["__mdiff", "__lshift", "__mprec_tens", "quorem"],
"character_class": ["__chclass"],
"camellia": ["camellia_", "camellia_feistel"],
"crypto_tables": ["FSb", "FSb2", "FSb3", "FSb4"],
"event_buffer": ["g_eb_list_desc", "eb_space"],
"base_node": ["base_node_", "base_node_add_handler"],
"file_descriptor": ["s_fd_table"],
"tx_delay": ["tx_delay_cfg"],
"deinit": ["deinit_functions"],
"lcp_echo": ["LcpEchoCheck"],
"raw_api": ["raw_bind", "raw_connect"],
"checksum": ["process_checksum"],
"entry_management": ["add_entry"],
"esp_ota": ["esp_ota", "ota_", "read_otadata"],
"http_server": [
"httpd_",
"parse_url_char",
"cb_headers_complete",
"delete_entry",
"validate_structure",
"config_save",
"config_new",
"verify_url",
"cb_url",
],
"misc_system": [
"alarm_cbs",
"start_up",
"tokens",
"unhex",
"osi_funcs_ro",
"enum_function",
"fragment_and_dispatch",
"alarm_set",
"osi_alarm_new",
"config_set_string",
"config_update_newest_section",
"config_remove_key",
"method_strings",
"interop_match",
"interop_database",
"__state_table",
"__action_table",
"s_stub_table",
"s_context",
"s_mmu_ctx",
"s_get_bus_mask",
"hli_queue_put",
"list_remove",
"list_delete",
"lock_acquire_generic",
"is_vect_desc_usable",
"io_mode_str",
"__c$20233",
"interface",
"read_id_core",
"subscribe_idle",
"unsubscribe_idle",
"s_clkout_handle",
"lock_release_generic",
"config_set_int",
"config_get_int",
"config_get_string",
"config_has_key",
"config_remove_section",
"osi_alarm_init",
"osi_alarm_deinit",
"fixed_queue_enqueue",
"fixed_queue_dequeue",
"fixed_queue_new",
"fixed_pkt_queue_enqueue",
"fixed_pkt_queue_new",
"list_append",
"list_prepend",
"list_insert_after",
"list_contains",
"list_get_node",
"hash_function_blob",
"cb_no_body",
"cb_on_body",
"profile_tab",
"get_arg",
"trim",
"buf$",
"process_appended_hash_and_sig$constprop$0",
"uuidType",
"allocate_svc_db_buf",
"_hostname_is_ours",
"s_hli_handlers",
"tick_cb",
"idle_cb",
"input",
"entry_find",
"section_find",
"find_bucket_entry_",
"config_has_section",
"hli_queue_create",
"hli_queue_get",
"hli_c_handler",
"future_ready",
"future_await",
"future_new",
"pkt_queue_enqueue",
"pkt_queue_dequeue",
"pkt_queue_cleanup",
"pkt_queue_create",
"pkt_queue_destroy",
"fixed_pkt_queue_dequeue",
"osi_alarm_cancel",
"osi_alarm_is_active",
"osi_sem_take",
"osi_event_create",
"osi_event_bind",
"alarm_cb_handler",
"list_foreach",
"list_back",
"list_front",
"list_clear",
"fixed_queue_try_peek_first",
"translate_path",
"get_idx",
"find_key",
"init",
"end",
"start",
"set_read_value",
"copy_address_list",
"copy_and_key",
"sdk_cfg_opts",
"leftshift_onebit",
"config_section_end",
"config_section_begin",
"find_entry_and_check_all_reset",
"image_validate",
"xPendingReadyList",
"vListInitialise",
"lock_init_generic",
"ant_bttx_cfg",
"ant_dft_cfg",
"cs_send_to_ctrl_sock",
"config_llc_util_funcs_reset",
"make_set_adv_report_flow_control",
"make_set_event_mask",
"raw_new",
"raw_remove",
"BTE_InitStack",
"parse_read_local_supported_features_response",
"__math_invalidf",
"tinytens",
"__mprec_tinytens",
"__mprec_bigtens",
"vRingbufferDelete",
"vRingbufferDeleteWithCaps",
"vRingbufferReturnItem",
"vRingbufferReturnItemFromISR",
"get_acl_data_size_ble",
"get_features_ble",
"get_features_classic",
"get_acl_packet_size_ble",
"get_acl_packet_size_classic",
"supports_extended_inquiry_response",
"supports_rssi_with_inquiry_results",
"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"],
"network_stack": [
"lwip",
"tcp",
"udp",
"ip4",
"ip6",
"dhcp",
"dns",
"netif",
"ethernet",
"ppp",
"slip",
],
"wifi_stack": ["NetworkInterface"],
"nimble_bt": [
"nimble",
"NimBLE",
"ble_hs",
"ble_gap",
"ble_gatt",
"ble_att",
"ble_l2cap",
"ble_sm",
],
"crypto": ["mbedtls", "crypto", "sha", "aes", "rsa", "ecc", "tls", "ssl"],
"cpp_stdlib": ["std::", "__gnu_cxx::", "__cxxabiv"],
"static_init": ["__static_initialization"],
"rtti": ["__type_info", "__class_type_info"],
"web_server_lib": ["AsyncWebServer", "AsyncWebHandler", "WebServer"],
"async_tcp": ["AsyncClient", "AsyncServer"],
"mdns_lib": ["mdns"],
"json_lib": [
"ArduinoJson",
"JsonDocument",
"JsonArray",
"JsonObject",
"deserialize",
"serialize",
],
"http_lib": ["HTTP", "http_", "Request", "Response", "Uri", "WebSocket"],
"logging": ["log", "Log", "print", "Print", "diag_"],
"authentication": ["checkDigestAuthentication"],
"libgcc": ["libgcc"],
"esp_system": ["esp_", "ESP"],
"arduino": ["arduino"],
"nvs": ["nvs_", "_ZTVN3nvs", "nvs::"],
"filesystem": ["spiffs", "vfs"],
"libc": ["newlib"],
}
# Patterns for categorizing ESPHome core symbols into subcategories
CORE_SUBCATEGORY_PATTERNS = {
"Component Framework": ["Component"],
"Application Core": ["Application"],
"Scheduler": ["Scheduler"],
"Component Iterator": ["ComponentIterator"],
"Helper Functions": ["Helpers", "helpers"],
"Preferences/Storage": ["Preferences", "ESPPreferences"],
"I/O Utilities": ["HighFrequencyLoopRequester"],
"String Utilities": ["str_"],
"Bit Utilities": ["reverse_bits"],
"Data Conversion": ["convert_"],
"Network Utilities": ["network", "IPAddress"],
"API Protocol": ["api::"],
"WiFi Manager": ["wifi::"],
"MQTT Client": ["mqtt::"],
"Logger": ["logger::"],
"OTA Updates": ["ota::"],
"Web Server": ["web_server::"],
"Time Management": ["time::"],
"Sensor Framework": ["sensor::"],
"Binary Sensor": ["binary_sensor::"],
"Switch Framework": ["switch_::"],
"Light Framework": ["light::"],
"Climate Framework": ["climate::"],
"Cover Framework": ["cover::"],
}

View File

@@ -0,0 +1,121 @@
"""Helper functions for memory analysis."""
from functools import cache
from pathlib import Path
from .const import SECTION_MAPPING
# Import namespace constant from parent module
# Note: This would create a circular import if done at module level,
# so we'll define it locally here as well
_NAMESPACE_ESPHOME = "esphome::"
# Get the list of actual ESPHome components by scanning the components directory
@cache
def get_esphome_components():
"""Get set of actual ESPHome components from the components directory."""
# Find the components directory relative to this file
# Go up two levels from analyze_memory/helpers.py to esphome/
current_dir = Path(__file__).parent.parent
components_dir = current_dir / "components"
if not components_dir.exists() or not components_dir.is_dir():
return frozenset()
return frozenset(
item.name
for item in components_dir.iterdir()
if item.is_dir()
and not item.name.startswith(".")
and not item.name.startswith("__")
)
@cache
def get_component_class_patterns(component_name: str) -> list[str]:
"""Generate component class name patterns for symbol matching.
Args:
component_name: The component name (e.g., "ota", "wifi", "api")
Returns:
List of pattern strings to match against demangled symbols
"""
component_upper = component_name.upper()
component_camel = component_name.replace("_", "").title()
return [
f"{_NAMESPACE_ESPHOME}{component_upper}Component", # e.g., esphome::OTAComponent
f"{_NAMESPACE_ESPHOME}ESPHome{component_upper}Component", # e.g., esphome::ESPHomeOTAComponent
f"{_NAMESPACE_ESPHOME}{component_camel}Component", # e.g., esphome::OtaComponent
f"{_NAMESPACE_ESPHOME}ESPHome{component_camel}Component", # e.g., esphome::ESPHomeOtaComponent
]
def map_section_name(raw_section: str) -> str | None:
"""Map raw section name to standard section.
Args:
raw_section: Raw section name from ELF file (e.g., ".iram0.text", ".rodata.str1.1")
Returns:
Standard section name (".text", ".rodata", ".data", ".bss") or None
"""
for standard_section, patterns in SECTION_MAPPING.items():
if any(pattern in raw_section for pattern in patterns):
return standard_section
return None
def parse_symbol_line(line: str) -> tuple[str, str, int, str] | None:
"""Parse a single symbol line from objdump output.
Args:
line: Line from objdump -t output
Returns:
Tuple of (section, name, size, address) or None if not a valid symbol.
Format: address l/g w/d F/O section size name
Example: 40084870 l F .iram0.text 00000000 _xt_user_exc
"""
parts = line.split()
if len(parts) < 5:
return None
try:
# Validate and extract address
address = parts[0]
int(address, 16)
except ValueError:
return None
# Look for F (function) or O (object) flag
if "F" not in parts and "O" not in parts:
return None
# Find section, size, and name
for i, part in enumerate(parts):
if not part.startswith("."):
continue
section = map_section_name(part)
if not section:
break
# Need at least size field after section
if i + 1 >= len(parts):
break
try:
size = int(parts[i + 1], 16)
except ValueError:
break
# Need symbol name and non-zero size
if i + 2 >= len(parts) or size == 0:
break
name = " ".join(parts[i + 2 :])
return (section, name, size, address)
return None

View File

@@ -380,12 +380,19 @@ async def homeassistant_service_to_code(
var = cg.new_Pvariable(action_id, template_arg, serv, False)
templ = await cg.templatable(config[CONF_ACTION], args, None)
cg.add(var.set_service(templ))
# Initialize FixedVectors with exact sizes from config
cg.add(var.init_data(len(config[CONF_DATA])))
for key, value in config[CONF_DATA].items():
templ = await cg.templatable(value, args, None)
cg.add(var.add_data(key, templ))
cg.add(var.init_data_template(len(config[CONF_DATA_TEMPLATE])))
for key, value in config[CONF_DATA_TEMPLATE].items():
templ = await cg.templatable(value, args, None)
cg.add(var.add_data_template(key, templ))
cg.add(var.init_variables(len(config[CONF_VARIABLES])))
for key, value in config[CONF_VARIABLES].items():
templ = await cg.templatable(value, args, None)
cg.add(var.add_variable(key, templ))
@@ -458,15 +465,23 @@ async def homeassistant_event_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg, serv, True)
templ = await cg.templatable(config[CONF_EVENT], args, None)
cg.add(var.set_service(templ))
# Initialize FixedVectors with exact sizes from config
cg.add(var.init_data(len(config[CONF_DATA])))
for key, value in config[CONF_DATA].items():
templ = await cg.templatable(value, args, None)
cg.add(var.add_data(key, templ))
cg.add(var.init_data_template(len(config[CONF_DATA_TEMPLATE])))
for key, value in config[CONF_DATA_TEMPLATE].items():
templ = await cg.templatable(value, args, None)
cg.add(var.add_data_template(key, templ))
cg.add(var.init_variables(len(config[CONF_VARIABLES])))
for key, value in config[CONF_VARIABLES].items():
templ = await cg.templatable(value, args, None)
cg.add(var.add_variable(key, templ))
return var
@@ -489,6 +504,8 @@ async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, arg
serv = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, serv, True)
cg.add(var.set_service("esphome.tag_scanned"))
# Initialize FixedVector with exact size (1 data field)
cg.add(var.init_data(1))
templ = await cg.templatable(config[CONF_TAG], args, cg.std_string)
cg.add(var.add_data("tag_id", templ))
return var

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];
@@ -876,10 +876,10 @@ message ExecuteServiceArgument {
string string_ = 4;
// ESPHome 1.14 (api v1.3) make int a signed value
sint32 int_ = 5;
repeated bool bool_array = 6 [packed=false];
repeated sint32 int_array = 7 [packed=false];
repeated float float_array = 8 [packed=false];
repeated string string_array = 9;
repeated bool bool_array = 6 [packed=false, (fixed_vector) = true];
repeated sint32 int_array = 7 [packed=false, (fixed_vector) = true];
repeated float float_array = 8 [packed=false, (fixed_vector) = true];
repeated string string_array = 9 [(fixed_vector) = true];
}
message ExecuteServiceRequest {
option (id) = 42;
@@ -888,7 +888,7 @@ message ExecuteServiceRequest {
option (ifdef) = "USE_API_SERVICES";
fixed32 key = 1;
repeated ExecuteServiceArgument args = 2;
repeated ExecuteServiceArgument args = 2 [(fixed_vector) = true];
}
// ==================== CAMERA ====================
@@ -987,8 +987,8 @@ message ListEntitiesClimateResponse {
string name = 3;
reserved 4; // Deprecated: was string unique_id
bool supports_current_temperature = 5;
bool supports_two_point_target_temperature = 6;
bool supports_current_temperature = 5; // Deprecated: use feature_flags
bool supports_two_point_target_temperature = 6; // Deprecated: use feature_flags
repeated ClimateMode supported_modes = 7 [(container_pointer) = "std::set<climate::ClimateMode>"];
float visual_min_temperature = 8;
float visual_max_temperature = 9;
@@ -997,7 +997,7 @@ message ListEntitiesClimateResponse {
// is if CLIMATE_PRESET_AWAY exists is supported_presets
// Deprecated in API version 1.5
bool legacy_supports_away = 11 [deprecated=true];
bool supports_action = 12;
bool supports_action = 12; // Deprecated: use feature_flags
repeated ClimateFanMode supported_fan_modes = 13 [(container_pointer) = "std::set<climate::ClimateFanMode>"];
repeated ClimateSwingMode supported_swing_modes = 14 [(container_pointer) = "std::set<climate::ClimateSwingMode>"];
repeated string supported_custom_fan_modes = 15 [(container_pointer) = "std::set"];
@@ -1007,11 +1007,12 @@ message ListEntitiesClimateResponse {
string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON"];
EntityCategory entity_category = 20;
float visual_current_temperature_step = 21;
bool supports_current_humidity = 22;
bool supports_target_humidity = 23;
bool supports_current_humidity = 22; // Deprecated: use feature_flags
bool supports_target_humidity = 23; // Deprecated: use feature_flags
float visual_min_humidity = 24;
float visual_max_humidity = 25;
uint32 device_id = 26 [(field_ifdef) = "USE_DEVICES"];
uint32 feature_flags = 27;
}
message ClimateStateResponse {
option (id) = 47;

View File

@@ -27,6 +27,9 @@
#ifdef USE_BLUETOOTH_PROXY
#include "esphome/components/bluetooth_proxy/bluetooth_proxy.h"
#endif
#ifdef USE_CLIMATE
#include "esphome/components/climate/climate_mode.h"
#endif
#ifdef USE_VOICE_ASSISTANT
#include "esphome/components/voice_assistant/voice_assistant.h"
#endif
@@ -450,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();
@@ -474,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();
@@ -623,9 +626,10 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection
auto traits = climate->get_traits();
resp.mode = static_cast<enums::ClimateMode>(climate->mode);
resp.action = static_cast<enums::ClimateAction>(climate->action);
if (traits.get_supports_current_temperature())
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE))
resp.current_temperature = climate->current_temperature;
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)) {
resp.target_temperature_low = climate->target_temperature_low;
resp.target_temperature_high = climate->target_temperature_high;
} else {
@@ -644,9 +648,9 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection
}
if (traits.get_supports_swing_modes())
resp.swing_mode = static_cast<enums::ClimateSwingMode>(climate->swing_mode);
if (traits.get_supports_current_humidity())
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY))
resp.current_humidity = climate->current_humidity;
if (traits.get_supports_target_humidity())
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY))
resp.target_humidity = climate->target_humidity;
return fill_and_encode_entity_state(climate, resp, ClimateStateResponse::MESSAGE_TYPE, conn, remaining_size,
is_single);
@@ -656,10 +660,15 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection
auto *climate = static_cast<climate::Climate *>(entity);
ListEntitiesClimateResponse msg;
auto traits = climate->get_traits();
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();
// Flags set for backward compatibility, deprecated in 2025.11.0
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_();
msg.visual_min_temperature = traits.get_visual_min_temperature();
msg.visual_max_temperature = traits.get_visual_max_temperature();
@@ -667,7 +676,6 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection
msg.visual_current_temperature_step = traits.get_visual_current_temperature_step();
msg.visual_min_humidity = traits.get_visual_min_humidity();
msg.visual_max_humidity = traits.get_visual_max_humidity();
msg.supports_action = traits.get_supports_action();
msg.supported_fan_modes = &traits.get_supported_fan_modes_for_api_();
msg.supported_custom_fan_modes = &traits.get_supported_custom_fan_modes_for_api_();
msg.supported_presets = &traits.get_supported_presets_for_api_();
@@ -1074,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
}
@@ -1406,7 +1409,7 @@ bool APIConnection::send_hello_response(const HelloRequest &msg) {
HelloResponse resp;
resp.api_version_major = 1;
resp.api_version_minor = 12;
resp.api_version_minor = 13;
// Send only the version string - the client only logs this for debugging and doesn't use it otherwise
resp.set_server_info(ESPHOME_VERSION_REF);
resp.set_name(StringRef(App.get_name()));

View File

@@ -242,7 +242,6 @@ APIError APINoiseFrameHelper::state_action_() {
const std::string &name = App.get_name();
const std::string &mac = get_mac_address();
std::vector<uint8_t> msg;
// Calculate positions and sizes
size_t name_len = name.size() + 1; // including null terminator
size_t mac_len = mac.size() + 1; // including null terminator
@@ -250,17 +249,17 @@ APIError APINoiseFrameHelper::state_action_() {
size_t mac_offset = name_offset + name_len;
size_t total_size = 1 + name_len + mac_len;
msg.resize(total_size);
auto msg = std::make_unique<uint8_t[]>(total_size);
// chosen proto
msg[0] = 0x01;
// node name, terminated by null byte
std::memcpy(msg.data() + name_offset, name.c_str(), name_len);
std::memcpy(msg.get() + name_offset, name.c_str(), name_len);
// node mac, terminated by null byte
std::memcpy(msg.data() + mac_offset, mac.c_str(), mac_len);
std::memcpy(msg.get() + mac_offset, mac.c_str(), mac_len);
aerr = write_frame_(msg.data(), msg.size());
aerr = write_frame_(msg.get(), total_size);
if (aerr != APIError::OK)
return aerr;
@@ -339,32 +338,32 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reaso
#ifdef USE_STORE_LOG_STR_IN_FLASH
// On ESP8266 with flash strings, we need to use PROGMEM-aware functions
size_t reason_len = strlen_P(reinterpret_cast<PGM_P>(reason));
std::vector<uint8_t> data;
data.resize(reason_len + 1);
size_t data_size = reason_len + 1;
auto data = std::make_unique<uint8_t[]>(data_size);
data[0] = 0x01; // failure
// Copy error message from PROGMEM
if (reason_len > 0) {
memcpy_P(data.data() + 1, reinterpret_cast<PGM_P>(reason), reason_len);
memcpy_P(data.get() + 1, reinterpret_cast<PGM_P>(reason), reason_len);
}
#else
// Normal memory access
const char *reason_str = LOG_STR_ARG(reason);
size_t reason_len = strlen(reason_str);
std::vector<uint8_t> data;
data.resize(reason_len + 1);
size_t data_size = reason_len + 1;
auto data = std::make_unique<uint8_t[]>(data_size);
data[0] = 0x01; // failure
// Copy error message in bulk
if (reason_len > 0) {
std::memcpy(data.data() + 1, reason_str, reason_len);
std::memcpy(data.get() + 1, reason_str, reason_len);
}
#endif
// temporarily remove failed state
auto orig_state = state_;
state_ = State::EXPLICIT_REJECT;
write_frame_(data.data(), data.size());
write_frame_(data.get(), data_size);
state_ = orig_state;
}
APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {

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

@@ -1064,6 +1064,17 @@ bool ExecuteServiceArgument::decode_32bit(uint32_t field_id, Proto32Bit value) {
}
return true;
}
void ExecuteServiceArgument::decode(const uint8_t *buffer, size_t length) {
uint32_t count_bool_array = ProtoDecodableMessage::count_repeated_field(buffer, length, 6);
this->bool_array.init(count_bool_array);
uint32_t count_int_array = ProtoDecodableMessage::count_repeated_field(buffer, length, 7);
this->int_array.init(count_int_array);
uint32_t count_float_array = ProtoDecodableMessage::count_repeated_field(buffer, length, 8);
this->float_array.init(count_float_array);
uint32_t count_string_array = ProtoDecodableMessage::count_repeated_field(buffer, length, 9);
this->string_array.init(count_string_array);
ProtoDecodableMessage::decode(buffer, length);
}
bool ExecuteServiceRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 2:
@@ -1085,6 +1096,11 @@ bool ExecuteServiceRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
}
return true;
}
void ExecuteServiceRequest::decode(const uint8_t *buffer, size_t length) {
uint32_t count_args = ProtoDecodableMessage::count_repeated_field(buffer, length, 2);
this->args.init(count_args);
ProtoDecodableMessage::decode(buffer, length);
}
#endif
#ifdef USE_CAMERA
void ListEntitiesCameraResponse::encode(ProtoWriteBuffer buffer) const {
@@ -1185,6 +1201,7 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const {
#ifdef USE_DEVICES
buffer.encode_uint32(26, this->device_id);
#endif
buffer.encode_uint32(27, this->feature_flags);
}
void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const {
size.add_length(1, this->object_id_ref_.size());
@@ -1239,6 +1256,7 @@ void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const {
#ifdef USE_DEVICES
size.add_uint32(2, this->device_id);
#endif
size.add_uint32(2, this->feature_flags);
}
void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_fixed32(1, this->key);

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{};
@@ -1279,10 +1279,11 @@ class ExecuteServiceArgument final : public ProtoDecodableMessage {
float float_{0.0f};
std::string string_{};
int32_t int_{0};
std::vector<bool> bool_array{};
std::vector<int32_t> int_array{};
std::vector<float> float_array{};
std::vector<std::string> string_array{};
FixedVector<bool> bool_array{};
FixedVector<int32_t> int_array{};
FixedVector<float> float_array{};
FixedVector<std::string> string_array{};
void decode(const uint8_t *buffer, size_t length) override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1300,7 +1301,8 @@ class ExecuteServiceRequest final : public ProtoDecodableMessage {
const char *message_name() const override { return "execute_service_request"; }
#endif
uint32_t key{0};
std::vector<ExecuteServiceArgument> args{};
FixedVector<ExecuteServiceArgument> args{};
void decode(const uint8_t *buffer, size_t length) override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1369,7 +1371,7 @@ class CameraImageRequest final : public ProtoDecodableMessage {
class ListEntitiesClimateResponse final : public InfoResponseProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 46;
static constexpr uint8_t ESTIMATED_SIZE = 145;
static constexpr uint8_t ESTIMATED_SIZE = 150;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_climate_response"; }
#endif
@@ -1390,6 +1392,7 @@ class ListEntitiesClimateResponse final : public InfoResponseProtoMessage {
bool supports_target_humidity{false};
float visual_min_humidity{0.0f};
float visual_max_humidity{0.0f};
uint32_t feature_flags{0};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP

View File

@@ -1292,6 +1292,7 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const {
#ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id);
#endif
dump_field(out, "feature_flags", this->feature_flags);
}
void ClimateStateResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "ClimateStateResponse");

View File

@@ -41,10 +41,14 @@ template<typename... X> class TemplatableStringValue : public TemplatableValue<s
template<typename... Ts> class TemplatableKeyValuePair {
public:
// Default constructor needed for FixedVector::emplace_back()
TemplatableKeyValuePair() = default;
// Keys are always string literals from YAML dictionary keys (e.g., "code", "event")
// and never templatable values or lambdas. Only the value parameter can be a lambda/template.
// Using pass-by-value with std::move allows optimal performance for both lvalues and rvalues.
template<typename T> TemplatableKeyValuePair(std::string key, T value) : key(std::move(key)), value(value) {}
std::string key;
TemplatableStringValue<Ts...> value;
};
@@ -93,15 +97,22 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
template<typename T> void set_service(T service) { this->service_ = service; }
// Initialize FixedVector members - called from Python codegen with compile-time known sizes.
// Must be called before any add_* methods; capacity must match the number of subsequent add_* calls.
void init_data(size_t count) { this->data_.init(count); }
void init_data_template(size_t count) { this->data_template_.init(count); }
void init_variables(size_t count) { this->variables_.init(count); }
// Keys are always string literals from the Python code generation (e.g., cg.add(var.add_data("tag_id", templ))).
// The value parameter can be a lambda/template, but keys are never templatable.
// Using pass-by-value allows the compiler to optimize for both lvalues and rvalues.
template<typename T> void add_data(std::string key, T value) { this->data_.emplace_back(std::move(key), value); }
template<typename T> void add_data_template(std::string key, T value) {
this->data_template_.emplace_back(std::move(key), value);
template<typename K, typename V> void add_data(K &&key, V &&value) {
this->add_kv_(this->data_, std::forward<K>(key), std::forward<V>(value));
}
template<typename T> void add_variable(std::string key, T value) {
this->variables_.emplace_back(std::move(key), value);
template<typename K, typename V> void add_data_template(K &&key, V &&value) {
this->add_kv_(this->data_template_, std::forward<K>(key), std::forward<V>(value));
}
template<typename K, typename V> void add_variable(K &&key, V &&value) {
this->add_kv_(this->variables_, std::forward<K>(key), std::forward<V>(value));
}
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
@@ -174,6 +185,13 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
}
protected:
// Helper to add key-value pairs to FixedVectors with perfect forwarding to avoid copies
template<typename K, typename V> void add_kv_(FixedVector<TemplatableKeyValuePair<Ts...>> &vec, K &&key, V &&value) {
auto &kv = vec.emplace_back();
kv.key = std::forward<K>(key);
kv.value = std::forward<V>(value);
}
template<typename VectorType, typename SourceType>
static void populate_service_map(VectorType &dest, SourceType &source, Ts... x) {
dest.init(source.size());
@@ -186,9 +204,9 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
APIServer *parent_;
TemplatableStringValue<Ts...> service_{};
std::vector<TemplatableKeyValuePair<Ts...>> data_;
std::vector<TemplatableKeyValuePair<Ts...>> data_template_;
std::vector<TemplatableKeyValuePair<Ts...>> variables_;
FixedVector<TemplatableKeyValuePair<Ts...>> data_;
FixedVector<TemplatableKeyValuePair<Ts...>> data_template_;
FixedVector<TemplatableKeyValuePair<Ts...>> variables_;
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
TemplatableStringValue<Ts...> response_template_{""};

View File

@@ -7,6 +7,69 @@ namespace esphome::api {
static const char *const TAG = "api.proto";
uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size_t length, uint32_t target_field_id) {
uint32_t count = 0;
const uint8_t *ptr = buffer;
const uint8_t *end = buffer + length;
while (ptr < end) {
uint32_t consumed;
// Parse field header (tag)
auto res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
if (!res.has_value()) {
break; // Invalid data, stop counting
}
uint32_t tag = res->as_uint32();
uint32_t field_type = tag & WIRE_TYPE_MASK;
uint32_t field_id = tag >> 3;
ptr += consumed;
// Count if this is the target field
if (field_id == target_field_id) {
count++;
}
// Skip field data based on wire type
switch (field_type) {
case WIRE_TYPE_VARINT: { // VarInt - parse and skip
res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
if (!res.has_value()) {
return count; // Invalid data, return what we have
}
ptr += consumed;
break;
}
case WIRE_TYPE_LENGTH_DELIMITED: { // Length-delimited - parse length and skip data
res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
if (!res.has_value()) {
return count;
}
uint32_t field_length = res->as_uint32();
ptr += consumed;
if (ptr + field_length > end) {
return count; // Out of bounds
}
ptr += field_length;
break;
}
case WIRE_TYPE_FIXED32: { // 32-bit - skip 4 bytes
if (ptr + 4 > end) {
return count;
}
ptr += 4;
break;
}
default:
// Unknown wire type, can't continue
return count;
}
}
return count;
}
void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
const uint8_t *ptr = buffer;
const uint8_t *end = buffer + length;
@@ -22,12 +85,12 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
}
uint32_t tag = res->as_uint32();
uint32_t field_type = tag & 0b111;
uint32_t field_type = tag & WIRE_TYPE_MASK;
uint32_t field_id = tag >> 3;
ptr += consumed;
switch (field_type) {
case 0: { // VarInt
case WIRE_TYPE_VARINT: { // VarInt
res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
if (!res.has_value()) {
ESP_LOGV(TAG, "Invalid VarInt at offset %ld", (long) (ptr - buffer));
@@ -39,7 +102,7 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
ptr += consumed;
break;
}
case 2: { // Length-delimited
case WIRE_TYPE_LENGTH_DELIMITED: { // Length-delimited
res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
if (!res.has_value()) {
ESP_LOGV(TAG, "Invalid Length Delimited at offset %ld", (long) (ptr - buffer));
@@ -57,7 +120,7 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
ptr += field_length;
break;
}
case 5: { // 32-bit
case WIRE_TYPE_FIXED32: { // 32-bit
if (ptr + 4 > end) {
ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at offset %ld", (long) (ptr - buffer));
return;

View File

@@ -15,6 +15,13 @@
namespace esphome::api {
// Protocol Buffer wire type constants
// See https://protobuf.dev/programming-guides/encoding/#structure
constexpr uint8_t WIRE_TYPE_VARINT = 0; // int32, int64, uint32, uint64, sint32, sint64, bool, enum
constexpr uint8_t WIRE_TYPE_LENGTH_DELIMITED = 2; // string, bytes, embedded messages, packed repeated fields
constexpr uint8_t WIRE_TYPE_FIXED32 = 5; // fixed32, sfixed32, float
constexpr uint8_t WIRE_TYPE_MASK = 0b111; // Mask to extract wire type from tag
// Helper functions for ZigZag encoding/decoding
inline constexpr uint32_t encode_zigzag32(int32_t value) {
return (static_cast<uint32_t>(value) << 1) ^ (static_cast<uint32_t>(value >> 31));
@@ -241,7 +248,7 @@ class ProtoWriteBuffer {
* Following https://protobuf.dev/programming-guides/encoding/#structure
*/
void encode_field_raw(uint32_t field_id, uint32_t type) {
uint32_t val = (field_id << 3) | (type & 0b111);
uint32_t val = (field_id << 3) | (type & WIRE_TYPE_MASK);
this->encode_varint_raw(val);
}
void encode_string(uint32_t field_id, const char *string, size_t len, bool force = false) {
@@ -354,7 +361,18 @@ class ProtoMessage {
// Base class for messages that support decoding
class ProtoDecodableMessage : public ProtoMessage {
public:
void decode(const uint8_t *buffer, size_t length);
virtual void decode(const uint8_t *buffer, size_t length);
/**
* Count occurrences of a repeated field in a protobuf buffer.
* This is a lightweight scan that only parses tags and skips field data.
*
* @param buffer Pointer to the protobuf buffer
* @param length Length of the buffer in bytes
* @param target_field_id The field ID to count
* @return Number of times the field appears in the buffer
*/
static uint32_t count_repeated_field(const uint8_t *buffer, size_t length, uint32_t target_field_id);
protected:
virtual bool decode_varint(uint32_t field_id, ProtoVarInt value) { return false; }
@@ -482,7 +500,7 @@ class ProtoSize {
* @return The number of bytes needed to encode the field ID and wire type
*/
static constexpr uint32_t field(uint32_t field_id, uint32_t type) {
uint32_t tag = (field_id << 3) | (type & 0b111);
uint32_t tag = (field_id << 3) | (type & WIRE_TYPE_MASK);
return varint(tag);
}

View File

@@ -12,16 +12,16 @@ template<> int32_t get_execute_arg_value<int32_t>(const ExecuteServiceArgument &
template<> float get_execute_arg_value<float>(const ExecuteServiceArgument &arg) { return arg.float_; }
template<> std::string get_execute_arg_value<std::string>(const ExecuteServiceArgument &arg) { return arg.string_; }
template<> std::vector<bool> get_execute_arg_value<std::vector<bool>>(const ExecuteServiceArgument &arg) {
return arg.bool_array;
return std::vector<bool>(arg.bool_array.begin(), arg.bool_array.end());
}
template<> std::vector<int32_t> get_execute_arg_value<std::vector<int32_t>>(const ExecuteServiceArgument &arg) {
return arg.int_array;
return std::vector<int32_t>(arg.int_array.begin(), arg.int_array.end());
}
template<> std::vector<float> get_execute_arg_value<std::vector<float>>(const ExecuteServiceArgument &arg) {
return arg.float_array;
return std::vector<float>(arg.float_array.begin(), arg.float_array.end());
}
template<> std::vector<std::string> get_execute_arg_value<std::vector<std::string>>(const ExecuteServiceArgument &arg) {
return arg.string_array;
return std::vector<std::string>(arg.string_array.begin(), arg.string_array.end());
}
template<> enums::ServiceArgType to_service_arg_type<bool>() { return enums::SERVICE_ARG_TYPE_BOOL; }

View File

@@ -55,7 +55,7 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
protected:
virtual void execute(Ts... x) = 0;
template<int... S> void execute_(const std::vector<ExecuteServiceArgument> &args, seq<S...> type) {
template<typename ArgsContainer, int... S> void execute_(const ArgsContainer &args, seq<S...> type) {
this->execute((get_execute_arg_value<Ts>(args[S]))...);
}

View File

View File

@@ -0,0 +1,54 @@
#include "esphome/core/log.h"
#include "bh1900nux.h"
namespace esphome {
namespace bh1900nux {
static const char *const TAG = "bh1900nux.sensor";
// I2C Registers
static const uint8_t TEMPERATURE_REG = 0x00;
static const uint8_t CONFIG_REG = 0x01; // Not used and supported yet
static const uint8_t TEMPERATURE_LOW_REG = 0x02; // Not used and supported yet
static const uint8_t TEMPERATURE_HIGH_REG = 0x03; // Not used and supported yet
static const uint8_t SOFT_RESET_REG = 0x04;
// I2C Command payloads
static const uint8_t SOFT_RESET_PAYLOAD = 0x01; // Soft Reset value
static const float SENSOR_RESOLUTION = 0.0625f; // Sensor resolution per bit in degrees celsius
void BH1900NUXSensor::setup() {
// Initialize I2C device
i2c::ErrorCode result_code =
this->write_register(SOFT_RESET_REG, &SOFT_RESET_PAYLOAD, 1); // Software Reset to check communication
if (result_code != i2c::ERROR_OK) {
this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
return;
}
}
void BH1900NUXSensor::update() {
uint8_t temperature_raw[2];
if (this->read_register(TEMPERATURE_REG, temperature_raw, 2) != i2c::ERROR_OK) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
return;
}
// Combined raw value, unsigned and unaligned 16 bit
// Temperature is represented in just 12 bits, shift needed
int16_t raw_temperature_register_value = encode_uint16(temperature_raw[0], temperature_raw[1]);
raw_temperature_register_value >>= 4;
float temperature_value = raw_temperature_register_value * SENSOR_RESOLUTION; // Apply sensor resolution
this->publish_state(temperature_value);
}
void BH1900NUXSensor::dump_config() {
LOG_SENSOR("", "BH1900NUX", this);
LOG_I2C_DEVICE(this);
LOG_UPDATE_INTERVAL(this);
}
} // namespace bh1900nux
} // namespace esphome

View File

@@ -0,0 +1,18 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome {
namespace bh1900nux {
class BH1900NUXSensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice {
public:
void setup() override;
void update() override;
void dump_config() override;
};
} // namespace bh1900nux
} // namespace esphome

View File

@@ -0,0 +1,34 @@
import esphome.codegen as cg
from esphome.components import i2c, sensor
import esphome.config_validation as cv
from esphome.const import (
DEVICE_CLASS_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
)
DEPENDENCIES = ["i2c"]
CODEOWNERS = ["@B48D81EFCC"]
sensor_ns = cg.esphome_ns.namespace("bh1900nux")
BH1900NUXSensor = sensor_ns.class_(
"BH1900NUXSensor", cg.PollingComponent, i2c.I2CDevice
)
CONFIG_SCHEMA = (
sensor.sensor_schema(
BH1900NUXSensor,
accuracy_decimals=1,
unit_of_measurement=UNIT_CELSIUS,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x48))
)
async def to_code(config):
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)

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

@@ -8,17 +8,30 @@ namespace cap1188 {
static const char *const TAG = "cap1188";
void CAP1188Component::setup() {
// Reset device using the reset pin
if (this->reset_pin_ != nullptr) {
this->reset_pin_->setup();
this->reset_pin_->digital_write(false);
delay(100); // NOLINT
this->reset_pin_->digital_write(true);
delay(100); // NOLINT
this->reset_pin_->digital_write(false);
delay(100); // NOLINT
this->disable_loop();
// no reset pin
if (this->reset_pin_ == nullptr) {
this->finish_setup_();
return;
}
// reset pin configured so reset before finishing setup
this->reset_pin_->setup();
this->reset_pin_->digital_write(false);
// delay after reset pin write
this->set_timeout(100, [this]() {
this->reset_pin_->digital_write(true);
// delay after reset pin write
this->set_timeout(100, [this]() {
this->reset_pin_->digital_write(false);
// delay after reset pin write
this->set_timeout(100, [this]() { this->finish_setup_(); });
});
});
}
void CAP1188Component::finish_setup_() {
// Check if CAP1188 is actually connected
this->read_byte(CAP1188_PRODUCT_ID, &this->cap1188_product_id_);
this->read_byte(CAP1188_MANUFACTURE_ID, &this->cap1188_manufacture_id_);
@@ -44,6 +57,9 @@ void CAP1188Component::setup() {
// Speed up a bit
this->write_byte(CAP1188_STAND_BY_CONFIGURATION, 0x30);
// Setup successful, so enable loop
this->enable_loop();
}
void CAP1188Component::dump_config() {

View File

@@ -49,6 +49,8 @@ class CAP1188Component : public Component, public i2c::I2CDevice {
void loop() override;
protected:
void finish_setup_();
std::vector<CAP1188Channel *> channels_{};
uint8_t touch_threshold_{0x20};
uint8_t allow_multiple_touches_{0x80};

View File

@@ -96,7 +96,8 @@ void ClimateCall::validate_() {
}
if (this->target_temperature_.has_value()) {
auto target = *this->target_temperature_;
if (traits.get_supports_two_point_target_temperature()) {
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!");
this->target_temperature_.reset();
@@ -106,7 +107,8 @@ void ClimateCall::validate_() {
}
}
if (this->target_temperature_low_.has_value() || this->target_temperature_high_.has_value()) {
if (!traits.get_supports_two_point_target_temperature()) {
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!");
this->target_temperature_low_.reset();
this->target_temperature_high_.reset();
@@ -350,13 +352,14 @@ void Climate::save_state_() {
state.mode = this->mode;
auto traits = this->get_traits();
if (traits.get_supports_two_point_target_temperature()) {
if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
state.target_temperature_low = this->target_temperature_low;
state.target_temperature_high = this->target_temperature_high;
} else {
state.target_temperature = this->target_temperature;
}
if (traits.get_supports_target_humidity()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
state.target_humidity = this->target_humidity;
}
if (traits.get_supports_fan_modes() && fan_mode.has_value()) {
@@ -400,7 +403,7 @@ void Climate::publish_state() {
auto traits = this->get_traits();
ESP_LOGD(TAG, " Mode: %s", LOG_STR_ARG(climate_mode_to_string(this->mode)));
if (traits.get_supports_action()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) {
ESP_LOGD(TAG, " Action: %s", LOG_STR_ARG(climate_action_to_string(this->action)));
}
if (traits.get_supports_fan_modes() && this->fan_mode.has_value()) {
@@ -418,19 +421,20 @@ void Climate::publish_state() {
if (traits.get_supports_swing_modes()) {
ESP_LOGD(TAG, " Swing Mode: %s", LOG_STR_ARG(climate_swing_mode_to_string(this->swing_mode)));
}
if (traits.get_supports_current_temperature()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) {
ESP_LOGD(TAG, " Current Temperature: %.2f°C", this->current_temperature);
}
if (traits.get_supports_two_point_target_temperature()) {
if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
ESP_LOGD(TAG, " Target Temperature: Low: %.2f°C High: %.2f°C", this->target_temperature_low,
this->target_temperature_high);
} else {
ESP_LOGD(TAG, " Target Temperature: %.2f°C", this->target_temperature);
}
if (traits.get_supports_current_humidity()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) {
ESP_LOGD(TAG, " Current Humidity: %.0f%%", this->current_humidity);
}
if (traits.get_supports_target_humidity()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
ESP_LOGD(TAG, " Target Humidity: %.0f%%", this->target_humidity);
}
@@ -485,13 +489,14 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) {
auto call = climate->make_call();
auto traits = climate->get_traits();
call.set_mode(this->mode);
if (traits.get_supports_two_point_target_temperature()) {
if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
call.set_target_temperature_low(this->target_temperature_low);
call.set_target_temperature_high(this->target_temperature_high);
} else {
call.set_target_temperature(this->target_temperature);
}
if (traits.get_supports_target_humidity()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
call.set_target_humidity(this->target_humidity);
}
if (traits.get_supports_fan_modes() || !traits.get_supported_custom_fan_modes().empty()) {
@@ -508,13 +513,14 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) {
void ClimateDeviceRestoreState::apply(Climate *climate) {
auto traits = climate->get_traits();
climate->mode = this->mode;
if (traits.get_supports_two_point_target_temperature()) {
if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
climate->target_temperature_low = this->target_temperature_low;
climate->target_temperature_high = this->target_temperature_high;
} else {
climate->target_temperature = this->target_temperature;
}
if (traits.get_supports_target_humidity()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
climate->target_humidity = this->target_humidity;
}
if (traits.get_supports_fan_modes() && !this->uses_custom_fan_mode) {
@@ -580,28 +586,30 @@ void Climate::dump_traits_(const char *tag) {
" Target: %.1f",
traits.get_visual_min_temperature(), traits.get_visual_max_temperature(),
traits.get_visual_target_temperature_step());
if (traits.get_supports_current_temperature()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) {
ESP_LOGCONFIG(tag, " Current: %.1f", traits.get_visual_current_temperature_step());
}
if (traits.get_supports_target_humidity() || traits.get_supports_current_humidity()) {
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",
traits.get_visual_min_humidity(), traits.get_visual_max_humidity());
}
if (traits.get_supports_two_point_target_temperature()) {
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");
}
if (traits.get_supports_current_temperature()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) {
ESP_LOGCONFIG(tag, " [x] Supports current temperature");
}
if (traits.get_supports_target_humidity()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
ESP_LOGCONFIG(tag, " [x] Supports target humidity");
}
if (traits.get_supports_current_humidity()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) {
ESP_LOGCONFIG(tag, " [x] Supports current humidity");
}
if (traits.get_supports_action()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) {
ESP_LOGCONFIG(tag, " [x] Supports action");
}
if (!traits.get_supported_modes().empty()) {

View File

@@ -98,6 +98,21 @@ enum ClimatePreset : uint8_t {
CLIMATE_PRESET_ACTIVITY = 7,
};
enum ClimateFeature : uint32_t {
// Reporting current temperature is supported
CLIMATE_SUPPORTS_CURRENT_TEMPERATURE = 1 << 0,
// Setting two target temperatures is supported (used in conjunction with CLIMATE_MODE_HEAT_COOL)
CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE = 1 << 1,
// Single-point mode is NOT supported (UI always displays two handles, setting 'target_temperature' is not supported)
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE = 1 << 2,
// Reporting current humidity is supported
CLIMATE_SUPPORTS_CURRENT_HUMIDITY = 1 << 3,
// Setting a target humidity is supported
CLIMATE_SUPPORTS_TARGET_HUMIDITY = 1 << 4,
// Reporting current climate action is supported
CLIMATE_SUPPORTS_ACTION = 1 << 5,
};
/// Convert the given ClimateMode to a human-readable string.
const LogString *climate_mode_to_string(ClimateMode mode);

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 {
@@ -21,91 +21,100 @@ namespace climate {
* - Target Temperature
*
* All other properties and modes are optional and the integration must mark
* each of them as supported by setting the appropriate flag here.
* each of them as supported by setting the appropriate flag(s) here.
*
* - supports current temperature - if the climate device supports reporting a current temperature
* - supports two point target temperature - if the climate device's target temperature should be
* split in target_temperature_low and target_temperature_high instead of just the single target_temperature
* - feature flags: see ClimateFeatures enum in climate_mode.h
* - supports modes:
* - auto mode (automatic control)
* - cool mode (lowers current temperature)
* - heat mode (increases current temperature)
* - dry mode (removes humidity from air)
* - fan mode (only turns on fan)
* - supports action - if the climate device supports reporting the active
* current action of the device with the action property.
* - supports fan modes - optionally, if it has a fan which can be configured in different ways:
* - on, off, auto, high, medium, low, middle, focus, diffuse, quiet
* - supports swing modes - optionally, if it has a swing which can be configured in different ways:
* - off, both, vertical, horizontal
*
* This class also contains static data for the climate device display:
* - visual min/max temperature - tells the frontend what range of temperatures the climate device
* should display (gauge min/max values)
* - visual min/max temperature/humidity - tells the frontend what range of temperature/humidity the
* climate device should display (gauge min/max values)
* - temperature step - the step with which to increase/decrease target temperature.
* This also affects with how many decimal places the temperature is shown
*/
class ClimateTraits {
public:
bool get_supports_current_temperature() const { return this->supports_current_temperature_; }
/// Get/set feature flags (see ClimateFeatures enum in climate_mode.h)
uint32_t get_feature_flags() const { return this->feature_flags_; }
void add_feature_flags(uint32_t feature_flags) { this->feature_flags_ |= feature_flags; }
void clear_feature_flags(uint32_t feature_flags) { this->feature_flags_ &= ~feature_flags; }
bool has_feature_flags(uint32_t feature_flags) const { return this->feature_flags_ & feature_flags; }
void set_feature_flags(uint32_t feature_flags) { this->feature_flags_ = feature_flags; }
ESPDEPRECATED("This method is deprecated, use get_feature_flags() instead", "2025.11.0")
bool get_supports_current_temperature() const {
return this->has_feature_flags(CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
}
ESPDEPRECATED("This method is deprecated, use add_feature_flags() instead", "2025.11.0")
void set_supports_current_temperature(bool supports_current_temperature) {
this->supports_current_temperature_ = supports_current_temperature;
if (supports_current_temperature) {
this->add_feature_flags(CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
} else {
this->clear_feature_flags(CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
}
}
bool get_supports_current_humidity() const { return this->supports_current_humidity_; }
ESPDEPRECATED("This method is deprecated, use get_feature_flags() instead", "2025.11.0")
bool get_supports_current_humidity() const { return this->has_feature_flags(CLIMATE_SUPPORTS_CURRENT_HUMIDITY); }
ESPDEPRECATED("This method is deprecated, use add_feature_flags() instead", "2025.11.0")
void set_supports_current_humidity(bool supports_current_humidity) {
this->supports_current_humidity_ = supports_current_humidity;
if (supports_current_humidity) {
this->add_feature_flags(CLIMATE_SUPPORTS_CURRENT_HUMIDITY);
} else {
this->clear_feature_flags(CLIMATE_SUPPORTS_CURRENT_HUMIDITY);
}
}
bool get_supports_two_point_target_temperature() const { return this->supports_two_point_target_temperature_; }
ESPDEPRECATED("This method is deprecated, use get_feature_flags() instead", "2025.11.0")
bool get_supports_two_point_target_temperature() const {
return this->has_feature_flags(CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE);
}
ESPDEPRECATED("This method is deprecated, use add_feature_flags() instead", "2025.11.0")
void set_supports_two_point_target_temperature(bool supports_two_point_target_temperature) {
this->supports_two_point_target_temperature_ = supports_two_point_target_temperature;
if (supports_two_point_target_temperature)
// Use CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE to mimic previous behavior
{
this->add_feature_flags(CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE);
} else {
this->clear_feature_flags(CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE);
}
}
bool get_supports_target_humidity() const { return this->supports_target_humidity_; }
ESPDEPRECATED("This method is deprecated, use get_feature_flags() instead", "2025.11.0")
bool get_supports_target_humidity() const { return this->has_feature_flags(CLIMATE_SUPPORTS_TARGET_HUMIDITY); }
ESPDEPRECATED("This method is deprecated, use add_feature_flags() instead", "2025.11.0")
void set_supports_target_humidity(bool supports_target_humidity) {
this->supports_target_humidity_ = supports_target_humidity;
if (supports_target_humidity) {
this->add_feature_flags(CLIMATE_SUPPORTS_TARGET_HUMIDITY);
} else {
this->clear_feature_flags(CLIMATE_SUPPORTS_TARGET_HUMIDITY);
}
}
ESPDEPRECATED("This method is deprecated, use get_feature_flags() instead", "2025.11.0")
bool get_supports_action() const { return this->has_feature_flags(CLIMATE_SUPPORTS_ACTION); }
ESPDEPRECATED("This method is deprecated, use add_feature_flags() instead", "2025.11.0")
void set_supports_action(bool supports_action) {
if (supports_action) {
this->add_feature_flags(CLIMATE_SUPPORTS_ACTION);
} else {
this->clear_feature_flags(CLIMATE_SUPPORTS_ACTION);
}
}
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_supports_action(bool supports_action) { this->supports_action_ = supports_action; }
bool get_supports_action() const { return this->supports_action_; }
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();
@@ -137,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_; }
@@ -219,24 +218,20 @@ class ClimateTraits {
}
}
bool supports_current_temperature_{false};
bool supports_current_humidity_{false};
bool supports_two_point_target_temperature_{false};
bool supports_target_humidity_{false};
std::set<climate::ClimateMode> supported_modes_ = {climate::CLIMATE_MODE_OFF};
bool supports_action_{false};
std::set<climate::ClimateFanMode> supported_fan_modes_;
std::set<climate::ClimateSwingMode> supported_swing_modes_;
std::set<climate::ClimatePreset> supported_presets_;
std::set<std::string> supported_custom_fan_modes_;
std::set<std::string> supported_custom_presets_;
uint32_t feature_flags_{0};
float visual_min_temperature_{10};
float visual_max_temperature_{30};
float visual_target_temperature_step_{0.1};
float visual_current_temperature_step_{0.1};
float visual_min_humidity_{30};
float visual_max_humidity_{99};
std::set<climate::ClimateMode> supported_modes_ = {climate::CLIMATE_MODE_OFF};
std::set<climate::ClimateFanMode> supported_fan_modes_;
std::set<climate::ClimateSwingMode> supported_swing_modes_;
std::set<climate::ClimatePreset> supported_presets_;
std::set<std::string> supported_custom_fan_modes_;
std::set<std::string> supported_custom_presets_;
};
} // namespace climate

View File

@@ -30,14 +30,12 @@ class DateTimeBase : public EntityBase {
#endif
};
#ifdef USE_TIME
class DateTimeStateTrigger : public Trigger<ESPTime> {
public:
explicit DateTimeStateTrigger(DateTimeBase *parent) {
parent->add_on_state_callback([this, parent]() { this->trigger(parent->state_as_esptime()); });
}
};
#endif
} // namespace datetime
} // namespace esphome

View File

@@ -11,8 +11,6 @@
#include <esp_chip_info.h>
#include <esp_partition.h>
#include <map>
#ifdef USE_ARDUINO
#include <Esp.h>
#endif
@@ -125,7 +123,12 @@ void DebugComponent::log_partition_info_() {
uint32_t DebugComponent::get_free_heap_() { return heap_caps_get_free_size(MALLOC_CAP_INTERNAL); }
static const std::map<int, const char *> CHIP_FEATURES = {
struct ChipFeature {
int bit;
const char *name;
};
static constexpr ChipFeature CHIP_FEATURES[] = {
{CHIP_FEATURE_BLE, "BLE"},
{CHIP_FEATURE_BT, "BT"},
{CHIP_FEATURE_EMB_FLASH, "EMB Flash"},
@@ -170,11 +173,13 @@ void DebugComponent::get_device_info_(std::string &device_info) {
esp_chip_info(&info);
const char *model = ESPHOME_VARIANT;
std::string features;
for (auto feature : CHIP_FEATURES) {
if (info.features & feature.first) {
features += feature.second;
// Check each known feature bit
for (const auto &feature : CHIP_FEATURES) {
if (info.features & feature.bit) {
features += feature.name;
features += ", ";
info.features &= ~feature.first;
info.features &= ~feature.bit;
}
}
if (info.features != 0)

View File

@@ -25,10 +25,37 @@ static void show_reset_reason(std::string &reset_reason, bool set, const char *r
reset_reason += reason;
}
inline uint32_t read_mem_u32(uintptr_t addr) {
static inline uint32_t read_mem_u32(uintptr_t addr) {
return *reinterpret_cast<volatile uint32_t *>(addr); // NOLINT(performance-no-int-to-ptr)
}
static inline uint8_t read_mem_u8(uintptr_t addr) {
return *reinterpret_cast<volatile uint8_t *>(addr); // NOLINT(performance-no-int-to-ptr)
}
// defines from https://github.com/adafruit/Adafruit_nRF52_Bootloader which prints those information
constexpr uint32_t SD_MAGIC_NUMBER = 0x51B1E5DB;
constexpr uintptr_t MBR_SIZE = 0x1000;
constexpr uintptr_t SOFTDEVICE_INFO_STRUCT_OFFSET = 0x2000;
constexpr uintptr_t SD_ID_OFFSET = SOFTDEVICE_INFO_STRUCT_OFFSET + 0x10;
constexpr uintptr_t SD_VERSION_OFFSET = SOFTDEVICE_INFO_STRUCT_OFFSET + 0x14;
static inline bool is_sd_present() {
return read_mem_u32(SOFTDEVICE_INFO_STRUCT_OFFSET + MBR_SIZE + 4) == SD_MAGIC_NUMBER;
}
static inline uint32_t sd_id_get() {
if (read_mem_u8(MBR_SIZE + SOFTDEVICE_INFO_STRUCT_OFFSET) > (SD_ID_OFFSET - SOFTDEVICE_INFO_STRUCT_OFFSET)) {
return read_mem_u32(MBR_SIZE + SD_ID_OFFSET);
}
return 0;
}
static inline uint32_t sd_version_get() {
if (read_mem_u8(MBR_SIZE + SOFTDEVICE_INFO_STRUCT_OFFSET) > (SD_VERSION_OFFSET - SOFTDEVICE_INFO_STRUCT_OFFSET)) {
return read_mem_u32(MBR_SIZE + SD_VERSION_OFFSET);
}
return 0;
}
std::string DebugComponent::get_reset_reason_() {
uint32_t cause;
auto ret = hwinfo_get_reset_cause(&cause);
@@ -271,6 +298,29 @@ void DebugComponent::get_device_info_(std::string &device_info) {
NRF_UICR->NRFFW[0]);
ESP_LOGD(TAG, "MBR param page addr 0x%08x, UICR param page addr 0x%08x", read_mem_u32(MBR_PARAM_PAGE_ADDR),
NRF_UICR->NRFFW[1]);
if (is_sd_present()) {
uint32_t const sd_id = sd_id_get();
uint32_t const sd_version = sd_version_get();
uint32_t ver[3];
ver[0] = sd_version / 1000000;
ver[1] = (sd_version - ver[0] * 1000000) / 1000;
ver[2] = (sd_version - ver[0] * 1000000 - ver[1] * 1000);
ESP_LOGD(TAG, "SoftDevice: S%u %u.%u.%u", sd_id, ver[0], ver[1], ver[2]);
#ifdef USE_SOFTDEVICE_ID
#ifdef USE_SOFTDEVICE_VERSION
if (USE_SOFTDEVICE_ID != sd_id || USE_SOFTDEVICE_VERSION != ver[0]) {
ESP_LOGE(TAG, "Built for SoftDevice S%u %u.x.y. It may crash due to mismatch of bootloader version.",
USE_SOFTDEVICE_ID, USE_SOFTDEVICE_VERSION);
}
#else
if (USE_SOFTDEVICE_ID != sd_id) {
ESP_LOGE(TAG, "Built for SoftDevice S%u. It may crash due to mismatch of bootloader version.", USE_SOFTDEVICE_ID);
}
#endif
#endif
}
#endif
}

View File

@@ -828,6 +828,9 @@ async def to_code(config):
# Disable dynamic log level control to save memory
add_idf_sdkconfig_option("CONFIG_LOG_DYNAMIC_LEVEL_CONTROL", False)
# Reduce PHY TX power in the event of a brownout
add_idf_sdkconfig_option("CONFIG_ESP_PHY_REDUCE_TX_POWER", True)
# Set default CPU frequency
add_idf_sdkconfig_option(
f"CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_{config[CONF_CPU_FREQUENCY][:-3]}", 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

@@ -108,8 +108,13 @@ class BTLoggers(Enum):
"""ESP32 WiFi provisioning over Bluetooth"""
# Set to track which loggers are needed by components
_required_loggers: set[BTLoggers] = set()
# Key for storing required loggers in CORE.data
ESP32_BLE_REQUIRED_LOGGERS_KEY = "esp32_ble_required_loggers"
def _get_required_loggers() -> set[BTLoggers]:
"""Get the set of required Bluetooth loggers from CORE.data."""
return CORE.data.setdefault(ESP32_BLE_REQUIRED_LOGGERS_KEY, set())
# Dataclass for handler registration counts
@@ -170,12 +175,13 @@ def register_bt_logger(*loggers: BTLoggers) -> None:
Args:
*loggers: One or more BTLoggers enum members
"""
required_loggers = _get_required_loggers()
for logger in loggers:
if not isinstance(logger, BTLoggers):
raise TypeError(
f"Logger must be a BTLoggers enum member, got {type(logger)}"
)
_required_loggers.add(logger)
required_loggers.add(logger)
CONF_BLE_ID = "ble_id"
@@ -488,8 +494,9 @@ async def to_code(config):
# Apply logger settings if log disabling is enabled
if config.get(CONF_DISABLE_BT_LOGS, False):
# Disable all Bluetooth loggers that are not required
required_loggers = _get_required_loggers()
for logger in BTLoggers:
if logger not in _required_loggers:
if logger not in required_loggers:
add_idf_sdkconfig_option(f"{logger.value}_NONE", True)
# Set BLE connection establishment timeout to match aioesphomeapi/bleak-retry-connector

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

@@ -60,11 +60,21 @@ class RegistrationCounts:
clients: int = 0
# Set to track which features are needed by components
_required_features: set[BLEFeatures] = set()
# CORE.data keys for state management
ESP32_BLE_TRACKER_REQUIRED_FEATURES_KEY = "esp32_ble_tracker_required_features"
ESP32_BLE_TRACKER_REGISTRATION_COUNTS_KEY = "esp32_ble_tracker_registration_counts"
# Track registration counts for StaticVector sizing
_registration_counts = RegistrationCounts()
def _get_required_features() -> set[BLEFeatures]:
"""Get the set of required BLE features from CORE.data."""
return CORE.data.setdefault(ESP32_BLE_TRACKER_REQUIRED_FEATURES_KEY, set())
def _get_registration_counts() -> RegistrationCounts:
"""Get the registration counts from CORE.data."""
return CORE.data.setdefault(
ESP32_BLE_TRACKER_REGISTRATION_COUNTS_KEY, RegistrationCounts()
)
def register_ble_features(features: set[BLEFeatures]) -> None:
@@ -73,7 +83,7 @@ def register_ble_features(features: set[BLEFeatures]) -> None:
Args:
features: Set of BLEFeatures enum members
"""
_required_features.update(features)
_get_required_features().update(features)
esp32_ble_tracker_ns = cg.esphome_ns.namespace("esp32_ble_tracker")
@@ -267,15 +277,17 @@ async def to_code(config):
):
register_ble_features({BLEFeatures.ESP_BT_DEVICE})
registration_counts = _get_registration_counts()
for conf in config.get(CONF_ON_BLE_ADVERTISE, []):
_registration_counts.listeners += 1
registration_counts.listeners += 1
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
if CONF_MAC_ADDRESS in conf:
addr_list = [it.as_hex for it in conf[CONF_MAC_ADDRESS]]
cg.add(trigger.set_addresses(addr_list))
await automation.build_automation(trigger, [(ESPBTDeviceConstRef, "x")], conf)
for conf in config.get(CONF_ON_BLE_SERVICE_DATA_ADVERTISE, []):
_registration_counts.listeners += 1
registration_counts.listeners += 1
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
if len(conf[CONF_SERVICE_UUID]) == len(bt_uuid16_format):
cg.add(trigger.set_service_uuid16(as_hex(conf[CONF_SERVICE_UUID])))
@@ -288,7 +300,7 @@ async def to_code(config):
cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex))
await automation.build_automation(trigger, [(adv_data_t_const_ref, "x")], conf)
for conf in config.get(CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE, []):
_registration_counts.listeners += 1
registration_counts.listeners += 1
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
if len(conf[CONF_MANUFACTURER_ID]) == len(bt_uuid16_format):
cg.add(trigger.set_manufacturer_uuid16(as_hex(conf[CONF_MANUFACTURER_ID])))
@@ -301,7 +313,7 @@ async def to_code(config):
cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex))
await automation.build_automation(trigger, [(adv_data_t_const_ref, "x")], conf)
for conf in config.get(CONF_ON_SCAN_END, []):
_registration_counts.listeners += 1
registration_counts.listeners += 1
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
@@ -331,19 +343,21 @@ async def to_code(config):
@coroutine_with_priority(CoroPriority.FINAL)
async def _add_ble_features():
# Add feature-specific defines based on what's needed
if BLEFeatures.ESP_BT_DEVICE in _required_features:
required_features = _get_required_features()
if BLEFeatures.ESP_BT_DEVICE in required_features:
cg.add_define("USE_ESP32_BLE_DEVICE")
cg.add_define("USE_ESP32_BLE_UUID")
# Add defines for StaticVector sizing based on registration counts
# Only define if count > 0 to avoid allocating unnecessary memory
if _registration_counts.listeners > 0:
registration_counts = _get_registration_counts()
if registration_counts.listeners > 0:
cg.add_define(
"ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT", _registration_counts.listeners
"ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT", registration_counts.listeners
)
if _registration_counts.clients > 0:
if registration_counts.clients > 0:
cg.add_define(
"ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT", _registration_counts.clients
"ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT", registration_counts.clients
)
@@ -395,7 +409,7 @@ async def register_ble_device(
var: cg.SafeExpType, config: ConfigType
) -> cg.SafeExpType:
register_ble_features({BLEFeatures.ESP_BT_DEVICE})
_registration_counts.listeners += 1
_get_registration_counts().listeners += 1
paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
cg.add(paren.register_listener(var))
return var
@@ -403,7 +417,7 @@ async def register_ble_device(
async def register_client(var: cg.SafeExpType, config: ConfigType) -> cg.SafeExpType:
register_ble_features({BLEFeatures.ESP_BT_DEVICE})
_registration_counts.clients += 1
_get_registration_counts().clients += 1
paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
cg.add(paren.register_client(var))
return var
@@ -417,7 +431,7 @@ async def register_raw_ble_device(
This does NOT register the ESP_BT_DEVICE feature, meaning ESPBTDevice
will not be compiled in if this is the only registration method used.
"""
_registration_counts.listeners += 1
_get_registration_counts().listeners += 1
paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
cg.add(paren.register_listener(var))
return var
@@ -431,7 +445,7 @@ async def register_raw_client(
This does NOT register the ESP_BT_DEVICE feature, meaning ESPBTDevice
will not be compiled in if this is the only registration method used.
"""
_registration_counts.clients += 1
_get_registration_counts().clients += 1
paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
cg.add(paren.register_client(var))
return var

View File

@@ -190,7 +190,7 @@ async def to_code(config):
cg.add_define("ESPHOME_VARIANT", "ESP8266")
cg.add_define(ThreadModel.SINGLE)
cg.add_platformio_option("extra_scripts", ["post:post_build.py"])
cg.add_platformio_option("extra_scripts", ["pre:iram_fix.py", "post:post_build.py"])
conf = config[CONF_FRAMEWORK]
cg.add_platformio_option("framework", "arduino")
@@ -230,6 +230,12 @@ 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
if CORE.testing_mode:
cg.add_build_flag("-DESPHOME_TESTING_MODE")
cg.add_platformio_option("board_build.flash_mode", config[CONF_BOARD_FLASH_MODE])
ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
@@ -265,3 +271,8 @@ def copy_files():
post_build_file,
CORE.relative_build_path("post_build.py"),
)
iram_fix_file = dir / "iram_fix.py.script"
copy_file_if_changed(
iram_fix_file,
CORE.relative_build_path("iram_fix.py"),
)

View File

@@ -0,0 +1,44 @@
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

@@ -19,6 +19,7 @@ from esphome.const import (
from esphome.core import CORE, coroutine_with_priority
from esphome.coroutine import CoroPriority
import esphome.final_validate as fv
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
@@ -136,11 +137,12 @@ FINAL_VALIDATE_SCHEMA = ota_esphome_final_validate
@coroutine_with_priority(CoroPriority.OTA_UPDATES)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
cg.add(var.set_port(config[CONF_PORT]))
if CONF_PASSWORD in config:
# Password could be set to an empty string and we can assume that means no password
if config.get(CONF_PASSWORD):
cg.add(var.set_auth_password(config[CONF_PASSWORD]))
cg.add_define("USE_OTA_PASSWORD")
# Only include hash algorithms when password is configured

View File

@@ -9,8 +9,8 @@ static const char *const TAG = "htu21d";
static const uint8_t HTU21D_ADDRESS = 0x40;
static const uint8_t HTU21D_REGISTER_RESET = 0xFE;
static const uint8_t HTU21D_REGISTER_TEMPERATURE = 0xE3;
static const uint8_t HTU21D_REGISTER_HUMIDITY = 0xE5;
static const uint8_t HTU21D_REGISTER_TEMPERATURE = 0xF3;
static const uint8_t HTU21D_REGISTER_HUMIDITY = 0xF5;
static const uint8_t HTU21D_WRITERHT_REG_CMD = 0xE6; /**< Write RH/T User Register 1 */
static const uint8_t HTU21D_REGISTER_STATUS = 0xE7;
static const uint8_t HTU21D_WRITEHEATER_REG_CMD = 0x51; /**< Write Heater Control Register */

View File

@@ -143,7 +143,18 @@ def validate_mclk_divisible_by_3(config):
return config
_use_legacy_driver = None
# Key for storing legacy driver setting in CORE.data
I2S_USE_LEGACY_DRIVER_KEY = "i2s_use_legacy_driver"
def _get_use_legacy_driver():
"""Get the legacy driver setting from CORE.data."""
return CORE.data.get(I2S_USE_LEGACY_DRIVER_KEY)
def _set_use_legacy_driver(value: bool) -> None:
"""Set the legacy driver setting in CORE.data."""
CORE.data[I2S_USE_LEGACY_DRIVER_KEY] = value
def i2s_audio_component_schema(
@@ -209,17 +220,15 @@ async def register_i2s_audio_component(var, config):
def validate_use_legacy(value):
global _use_legacy_driver # noqa: PLW0603
if CONF_USE_LEGACY in value:
if (_use_legacy_driver is not None) and (
_use_legacy_driver != value[CONF_USE_LEGACY]
):
existing_value = _get_use_legacy_driver()
if (existing_value is not None) and (existing_value != value[CONF_USE_LEGACY]):
raise cv.Invalid(
f"All i2s_audio components must set {CONF_USE_LEGACY} to the same value."
)
if (not value[CONF_USE_LEGACY]) and (CORE.using_arduino):
raise cv.Invalid("Arduino supports only the legacy i2s driver")
_use_legacy_driver = value[CONF_USE_LEGACY]
_set_use_legacy_driver(value[CONF_USE_LEGACY])
return value
@@ -249,7 +258,8 @@ def _final_validate(_):
def use_legacy():
return not (CORE.using_esp_idf and not _use_legacy_driver)
legacy_driver = _get_use_legacy_driver()
return not (CORE.using_esp_idf and not legacy_driver)
FINAL_VALIDATE_SCHEMA = _final_validate

View File

@@ -35,6 +35,7 @@ CONF_CHARGE = "charge"
CONF_CHARGE_COULOMBS = "charge_coulombs"
CONF_ENERGY_JOULES = "energy_joules"
CONF_TEMPERATURE_COEFFICIENT = "temperature_coefficient"
CONF_RESET_ON_BOOT = "reset_on_boot"
UNIT_AMPERE_HOURS = "Ah"
UNIT_COULOMB = "C"
UNIT_JOULE = "J"
@@ -113,6 +114,7 @@ INA2XX_SCHEMA = cv.Schema(
cv.Optional(CONF_TEMPERATURE_COEFFICIENT, default=0): cv.int_range(
min=0, max=16383
),
cv.Optional(CONF_RESET_ON_BOOT, default=True): cv.boolean,
cv.Optional(CONF_SHUNT_VOLTAGE): cv.maybe_simple_value(
sensor.sensor_schema(
unit_of_measurement=UNIT_MILLIVOLT,
@@ -206,6 +208,7 @@ async def setup_ina2xx(var, config):
cg.add(var.set_adc_range(config[CONF_ADC_RANGE]))
cg.add(var.set_adc_avg_samples(config[CONF_ADC_AVERAGING]))
cg.add(var.set_shunt_tempco(config[CONF_TEMPERATURE_COEFFICIENT]))
cg.add(var.set_reset_on_boot(config[CONF_RESET_ON_BOOT]))
adc_time_config = config[CONF_ADC_TIME]
if isinstance(adc_time_config, dict):

View File

@@ -257,7 +257,12 @@ bool INA2XX::reset_energy_counters() {
bool INA2XX::reset_config_() {
ESP_LOGV(TAG, "Reset");
ConfigurationRegister cfg{0};
cfg.RST = true;
if (!this->reset_on_boot_) {
ESP_LOGI(TAG, "Skipping on-boot device reset");
cfg.RST = false;
} else {
cfg.RST = true;
}
return this->write_unsigned_16_(RegisterMap::REG_CONFIG, cfg.raw_u16);
}

View File

@@ -127,6 +127,7 @@ class INA2XX : public PollingComponent {
void set_adc_time_die_temperature(AdcTime time) { this->adc_time_die_temperature_ = time; }
void set_adc_avg_samples(AdcAvgSamples samples) { this->adc_avg_samples_ = samples; }
void set_shunt_tempco(uint16_t coeff) { this->shunt_tempco_ppm_c_ = coeff; }
void set_reset_on_boot(bool reset) { this->reset_on_boot_ = reset; }
void set_shunt_voltage_sensor(sensor::Sensor *sensor) { this->shunt_voltage_sensor_ = sensor; }
void set_bus_voltage_sensor(sensor::Sensor *sensor) { this->bus_voltage_sensor_ = sensor; }
@@ -172,6 +173,7 @@ class INA2XX : public PollingComponent {
AdcTime adc_time_die_temperature_{AdcTime::ADC_TIME_4120US};
AdcAvgSamples adc_avg_samples_{AdcAvgSamples::ADC_AVG_SAMPLES_128};
uint16_t shunt_tempco_ppm_c_{0};
bool reset_on_boot_{true};
//
// Calculated coefficients

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

@@ -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

@@ -140,11 +140,8 @@ void MQTTClientComponent::send_device_info_() {
#endif
#ifdef USE_API_NOISE
if (api::global_api_server->get_noise_ctx()->has_psk()) {
root["api_encryption"] = "Noise_NNpsk0_25519_ChaChaPoly_SHA256";
} else {
root["api_encryption_supported"] = "Noise_NNpsk0_25519_ChaChaPoly_SHA256";
}
root[api::global_api_server->get_noise_ctx()->has_psk() ? "api_encryption" : "api_encryption_supported"] =
"Noise_NNpsk0_25519_ChaChaPoly_SHA256";
#endif
},
2, this->discovery_info_.retain);

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

@@ -85,24 +85,20 @@ bool MQTTComponent::send_discovery_() {
}
// Fields from EntityBase
if (this->get_entity()->has_own_name()) {
root[MQTT_NAME] = this->friendly_name();
} else {
root[MQTT_NAME] = "";
}
root[MQTT_NAME] = this->get_entity()->has_own_name() ? this->friendly_name() : "";
if (this->is_disabled_by_default())
root[MQTT_ENABLED_BY_DEFAULT] = false;
if (!this->get_icon().empty())
root[MQTT_ICON] = this->get_icon();
switch (this->get_entity()->get_entity_category()) {
const auto entity_category = this->get_entity()->get_entity_category();
switch (entity_category) {
case ENTITY_CATEGORY_NONE:
break;
case ENTITY_CATEGORY_CONFIG:
root[MQTT_ENTITY_CATEGORY] = "config";
break;
case ENTITY_CATEGORY_DIAGNOSTIC:
root[MQTT_ENTITY_CATEGORY] = "diagnostic";
root[MQTT_ENTITY_CATEGORY] = entity_category == ENTITY_CATEGORY_CONFIG ? "config" : "diagnostic";
break;
}
@@ -113,20 +109,14 @@ bool MQTTComponent::send_discovery_() {
if (this->command_retain_)
root[MQTT_COMMAND_RETAIN] = true;
if (this->availability_ == nullptr) {
if (!global_mqtt_client->get_availability().topic.empty()) {
root[MQTT_AVAILABILITY_TOPIC] = global_mqtt_client->get_availability().topic;
if (global_mqtt_client->get_availability().payload_available != "online")
root[MQTT_PAYLOAD_AVAILABLE] = global_mqtt_client->get_availability().payload_available;
if (global_mqtt_client->get_availability().payload_not_available != "offline")
root[MQTT_PAYLOAD_NOT_AVAILABLE] = global_mqtt_client->get_availability().payload_not_available;
}
} else if (!this->availability_->topic.empty()) {
root[MQTT_AVAILABILITY_TOPIC] = this->availability_->topic;
if (this->availability_->payload_available != "online")
root[MQTT_PAYLOAD_AVAILABLE] = this->availability_->payload_available;
if (this->availability_->payload_not_available != "offline")
root[MQTT_PAYLOAD_NOT_AVAILABLE] = this->availability_->payload_not_available;
const Availability &avail =
this->availability_ == nullptr ? global_mqtt_client->get_availability() : *this->availability_;
if (!avail.topic.empty()) {
root[MQTT_AVAILABILITY_TOPIC] = avail.topic;
if (avail.payload_available != "online")
root[MQTT_PAYLOAD_AVAILABLE] = avail.payload_available;
if (avail.payload_not_available != "offline")
root[MQTT_PAYLOAD_NOT_AVAILABLE] = avail.payload_not_available;
}
const MQTTDiscoveryInfo &discovery_info = global_mqtt_client->get_discovery_info();
@@ -145,10 +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_();
std::string node_friendly_name = App.get_friendly_name();
if (node_friendly_name.empty()) {
node_friendly_name = node_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>();
@@ -158,13 +146,9 @@ bool MQTTComponent::send_discovery_() {
#ifdef ESPHOME_PROJECT_NAME
device_info[MQTT_DEVICE_SW_VERSION] = ESPHOME_PROJECT_VERSION " (ESPHome " ESPHOME_VERSION ")";
const char *model = std::strchr(ESPHOME_PROJECT_NAME, '.');
if (model == nullptr) { // must never happen but check anyway
device_info[MQTT_DEVICE_MODEL] = ESPHOME_BOARD;
device_info[MQTT_DEVICE_MANUFACTURER] = ESPHOME_PROJECT_NAME;
} else {
device_info[MQTT_DEVICE_MODEL] = model + 1;
device_info[MQTT_DEVICE_MANUFACTURER] = std::string(ESPHOME_PROJECT_NAME, model - ESPHOME_PROJECT_NAME);
}
device_info[MQTT_DEVICE_MODEL] = model == nullptr ? ESPHOME_BOARD : model + 1;
device_info[MQTT_DEVICE_MANUFACTURER] =
model == nullptr ? ESPHOME_PROJECT_NAME : std::string(ESPHOME_PROJECT_NAME, model - ESPHOME_PROJECT_NAME);
#else
device_info[MQTT_DEVICE_SW_VERSION] = ESPHOME_VERSION " (" + App.get_compilation_time() + ")";
device_info[MQTT_DEVICE_MODEL] = ESPHOME_BOARD;

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

@@ -1,5 +1,6 @@
from __future__ import annotations
import logging
from pathlib import Path
from esphome import pins
@@ -48,6 +49,7 @@ from .gpio import nrf52_pin_to_code # noqa
CODEOWNERS = ["@tomaszduda23"]
AUTO_LOAD = ["zephyr"]
IS_TARGET_PLATFORM = True
_LOGGER = logging.getLogger(__name__)
def set_core_data(config: ConfigType) -> ConfigType:
@@ -127,6 +129,10 @@ def _validate_mcumgr(config):
def _final_validate(config):
if CONF_DFU in config:
_validate_mcumgr(config)
if config[KEY_BOOTLOADER] == BOOTLOADER_ADAFRUIT:
_LOGGER.warning(
"Selected generic Adafruit bootloader. The board might crash. Consider settings `bootloader:`"
)
FINAL_VALIDATE_SCHEMA = _final_validate
@@ -157,6 +163,13 @@ async def to_code(config: ConfigType) -> None:
if config[KEY_BOOTLOADER] == BOOTLOADER_MCUBOOT:
cg.add_define("USE_BOOTLOADER_MCUBOOT")
else:
if "_sd" in config[KEY_BOOTLOADER]:
bootloader = config[KEY_BOOTLOADER].split("_")
sd_id = bootloader[2][2:]
cg.add_define("USE_SOFTDEVICE_ID", int(sd_id))
if (len(bootloader)) > 3:
sd_version = bootloader[3][1:]
cg.add_define("USE_SOFTDEVICE_VERSION", int(sd_version))
# make sure that firmware.zip is created
# for Adafruit_nRF52_Bootloader
cg.add_platformio_option("board_upload.protocol", "nrfutil")

View File

@@ -11,10 +11,18 @@ from .const import (
BOARDS_ZEPHYR = {
"adafruit_itsybitsy_nrf52840": {
KEY_BOOTLOADER: [
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6,
BOOTLOADER_ADAFRUIT,
BOOTLOADER_ADAFRUIT_NRF52_SD132,
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7,
]
},
"xiao_ble": {
KEY_BOOTLOADER: [
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7,
BOOTLOADER_ADAFRUIT,
BOOTLOADER_ADAFRUIT_NRF52_SD132,
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6,
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7,
]
},
}

View File

@@ -66,6 +66,7 @@ from esphome.const import (
DEVICE_CLASS_SPEED,
DEVICE_CLASS_SULPHUR_DIOXIDE,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_TEMPERATURE_DELTA,
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
DEVICE_CLASS_VOLTAGE,
@@ -130,6 +131,7 @@ DEVICE_CLASSES = [
DEVICE_CLASS_SPEED,
DEVICE_CLASS_SULPHUR_DIOXIDE,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_TEMPERATURE_DELTA,
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
DEVICE_CLASS_VOLTAGE,

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

@@ -1056,6 +1056,52 @@ async def sony_action(var, config, args):
cg.add(var.set_nbits(template_))
# Symphony
SymphonyData, SymphonyBinarySensor, SymphonyTrigger, SymphonyAction, SymphonyDumper = (
declare_protocol("Symphony")
)
SYMPHONY_SCHEMA = cv.Schema(
{
cv.Required(CONF_DATA): cv.hex_uint32_t,
cv.Required(CONF_NBITS): cv.int_range(min=1, max=32),
cv.Optional(CONF_COMMAND_REPEATS, default=2): cv.uint8_t,
}
)
@register_binary_sensor("symphony", SymphonyBinarySensor, SYMPHONY_SCHEMA)
def symphony_binary_sensor(var, config):
cg.add(
var.set_data(
cg.StructInitializer(
SymphonyData,
("data", config[CONF_DATA]),
("nbits", config[CONF_NBITS]),
)
)
)
@register_trigger("symphony", SymphonyTrigger, SymphonyData)
def symphony_trigger(var, config):
pass
@register_dumper("symphony", SymphonyDumper)
def symphony_dumper(var, config):
pass
@register_action("symphony", SymphonyAction, SYMPHONY_SCHEMA)
async def symphony_action(var, config, args):
template_ = await cg.templatable(config[CONF_DATA], args, cg.uint32)
cg.add(var.set_data(template_))
template_ = await cg.templatable(config[CONF_NBITS], args, cg.uint32)
cg.add(var.set_nbits(template_))
template_ = await cg.templatable(config[CONF_COMMAND_REPEATS], args, cg.uint8)
cg.add(var.set_repeats(template_))
# Raw
def validate_raw_alternating(value):
assert isinstance(value, list)

View File

@@ -0,0 +1,120 @@
#include "symphony_protocol.h"
#include "esphome/core/log.h"
namespace esphome {
namespace remote_base {
static const char *const TAG = "remote.symphony";
// Reference implementation and timing details:
// IRremoteESP8266 ir_Symphony.cpp
// https://github.com/crankyoldgit/IRremoteESP8266/blob/master/src/ir_Symphony.cpp
// The implementation below mirrors the constant bit-time mapping and
// footer-gap handling used there.
// Symphony protocol timing specifications (tuned to handset captures)
static const uint32_t BIT_ZERO_HIGH_US = 460; // short
static const uint32_t BIT_ZERO_LOW_US = 1260; // long
static const uint32_t BIT_ONE_HIGH_US = 1260; // long
static const uint32_t BIT_ONE_LOW_US = 460; // short
static const uint32_t CARRIER_FREQUENCY = 38000;
// IRremoteESP8266 reference: kSymphonyFooterGap = 4 * (mark + space)
static const uint32_t FOOTER_GAP_US = 4 * (BIT_ZERO_HIGH_US + BIT_ZERO_LOW_US);
// Typical inter-frame gap (~34.8 ms observed)
static const uint32_t INTER_FRAME_GAP_US = 34760;
void SymphonyProtocol::encode(RemoteTransmitData *dst, const SymphonyData &data) {
dst->set_carrier_frequency(CARRIER_FREQUENCY);
ESP_LOGD(TAG, "Sending Symphony: data=0x%0*X nbits=%u repeats=%u", (data.nbits + 3) / 4, (uint32_t) data.data,
data.nbits, data.repeats);
// Each bit produces a mark+space (2 entries). We fold the inter-frame/footer gap
// into the last bit's space of each frame to avoid over-length gaps.
dst->reserve(data.nbits * 2u * data.repeats);
for (uint8_t repeats = 0; repeats < data.repeats; repeats++) {
// Data bits (MSB first)
for (uint32_t mask = 1UL << (data.nbits - 1); mask != 0; mask >>= 1) {
const bool is_last_bit = (mask == 1);
const bool is_last_frame = (repeats == (data.repeats - 1));
if (is_last_bit) {
// Emit last bit's mark; replace its space with the proper gap
if (data.data & mask) {
dst->mark(BIT_ONE_HIGH_US);
} else {
dst->mark(BIT_ZERO_HIGH_US);
}
dst->space(is_last_frame ? FOOTER_GAP_US : INTER_FRAME_GAP_US);
} else {
if (data.data & mask) {
dst->item(BIT_ONE_HIGH_US, BIT_ONE_LOW_US);
} else {
dst->item(BIT_ZERO_HIGH_US, BIT_ZERO_LOW_US);
}
}
}
}
}
optional<SymphonyData> SymphonyProtocol::decode(RemoteReceiveData src) {
auto is_valid_len = [](uint8_t nbits) -> bool { return nbits == 8 || nbits == 12 || nbits == 16; };
RemoteReceiveData s = src; // copy
SymphonyData out{0, 0, 1};
for (; out.nbits < 32; out.nbits++) {
if (s.expect_mark(BIT_ONE_HIGH_US)) {
if (!s.expect_space(BIT_ONE_LOW_US)) {
// Allow footer gap immediately after the last mark
if (s.peek_space_at_least(FOOTER_GAP_US)) {
uint8_t bits_with_this = out.nbits + 1;
if (is_valid_len(bits_with_this)) {
out.data = (out.data << 1UL) | 1UL;
out.nbits = bits_with_this;
return out;
}
}
return {};
}
// Successfully consumed a '1' bit (mark + space)
out.data = (out.data << 1UL) | 1UL;
continue;
} else if (s.expect_mark(BIT_ZERO_HIGH_US)) {
if (!s.expect_space(BIT_ZERO_LOW_US)) {
// Allow footer gap immediately after the last mark
if (s.peek_space_at_least(FOOTER_GAP_US)) {
uint8_t bits_with_this = out.nbits + 1;
if (is_valid_len(bits_with_this)) {
out.data = (out.data << 1UL) | 0UL;
out.nbits = bits_with_this;
return out;
}
}
return {};
}
// Successfully consumed a '0' bit (mark + space)
out.data = (out.data << 1UL) | 0UL;
continue;
} else {
// Completed a valid-length frame followed by a footer gap
if (is_valid_len(out.nbits) && s.peek_space_at_least(FOOTER_GAP_US)) {
return out;
}
return {};
}
}
if (is_valid_len(out.nbits) && s.peek_space_at_least(FOOTER_GAP_US)) {
return out;
}
return {};
}
void SymphonyProtocol::dump(const SymphonyData &data) {
const int32_t hex_width = (data.nbits + 3) / 4; // pad to nibble width
ESP_LOGI(TAG, "Received Symphony: data=0x%0*X, nbits=%d", hex_width, (uint32_t) data.data, data.nbits);
}
} // namespace remote_base
} // namespace esphome

View File

@@ -0,0 +1,44 @@
#pragma once
#include "esphome/core/component.h"
#include "remote_base.h"
#include <cinttypes>
namespace esphome {
namespace remote_base {
struct SymphonyData {
uint32_t data;
uint8_t nbits;
uint8_t repeats{1};
bool operator==(const SymphonyData &rhs) const { return data == rhs.data && nbits == rhs.nbits; }
};
class SymphonyProtocol : public RemoteProtocol<SymphonyData> {
public:
void encode(RemoteTransmitData *dst, const SymphonyData &data) override;
optional<SymphonyData> decode(RemoteReceiveData src) override;
void dump(const SymphonyData &data) override;
};
DECLARE_REMOTE_PROTOCOL(Symphony)
template<typename... Ts> class SymphonyAction : public RemoteTransmitterActionBase<Ts...> {
public:
TEMPLATABLE_VALUE(uint32_t, data)
TEMPLATABLE_VALUE(uint8_t, nbits)
TEMPLATABLE_VALUE(uint8_t, repeats)
void encode(RemoteTransmitData *dst, Ts... x) override {
SymphonyData data{};
data.data = this->data_.value(x...);
data.nbits = this->nbits_.value(x...);
data.repeats = this->repeats_.value(x...);
SymphonyProtocol().encode(dst, data);
}
};
} // namespace remote_base
} // namespace esphome

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

@@ -89,6 +89,7 @@ from esphome.const import (
DEVICE_CLASS_SPEED,
DEVICE_CLASS_SULPHUR_DIOXIDE,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_TEMPERATURE_DELTA,
DEVICE_CLASS_TIMESTAMP,
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
@@ -157,6 +158,7 @@ DEVICE_CLASSES = [
DEVICE_CLASS_SPEED,
DEVICE_CLASS_SULPHUR_DIOXIDE,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_TEMPERATURE_DELTA,
DEVICE_CLASS_TIMESTAMP,
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
@@ -249,6 +251,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
)
@@ -450,14 +455,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(
@@ -472,14 +484,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(
@@ -496,16 +512,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(

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
@@ -543,5 +472,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.
@@ -476,5 +501,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

@@ -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

View File

@@ -1,7 +1,7 @@
import logging
from esphome import core
from esphome.config_helpers import Extend, Remove, merge_config
from esphome.config_helpers import Extend, Remove, merge_config, merge_dicts_ordered
import esphome.config_validation as cv
from esphome.const import CONF_SUBSTITUTIONS, VALID_SUBSTITUTIONS_CHARACTERS
from esphome.yaml_util import ESPHomeDataBase, ESPLiteralValue, make_data_base
@@ -170,10 +170,10 @@ def do_substitution_pass(config, command_line_substitutions, ignore_missing=Fals
return
# Merge substitutions in config, overriding with substitutions coming from command line:
substitutions = {
**config.get(CONF_SUBSTITUTIONS, {}),
**(command_line_substitutions or {}),
}
# Use merge_dicts_ordered to preserve OrderedDict type for move_to_end()
substitutions = merge_dicts_ordered(
config.get(CONF_SUBSTITUTIONS, {}), command_line_substitutions or {}
)
with cv.prepend_path("substitutions"):
if not isinstance(substitutions, dict):
raise cv.Invalid(

View File

@@ -71,9 +71,14 @@ from esphome.const import (
CONF_VISUAL,
)
CONF_PRESET_CHANGE = "preset_change"
CONF_DEFAULT_PRESET = "default_preset"
CONF_HUMIDITY_CONTROL_DEHUMIDIFY_ACTION = "humidity_control_dehumidify_action"
CONF_HUMIDITY_CONTROL_HUMIDIFY_ACTION = "humidity_control_humidify_action"
CONF_HUMIDITY_CONTROL_OFF_ACTION = "humidity_control_off_action"
CONF_HUMIDITY_HYSTERESIS = "humidity_hysteresis"
CONF_ON_BOOT_RESTORE_FROM = "on_boot_restore_from"
CONF_PRESET_CHANGE = "preset_change"
CONF_TARGET_HUMIDITY_CHANGE_ACTION = "target_humidity_change_action"
CODEOWNERS = ["@kbx81"]
@@ -241,6 +246,14 @@ def validate_thermostat(config):
CONF_MAX_HEATING_RUN_TIME,
CONF_SUPPLEMENTAL_HEATING_ACTION,
],
CONF_HUMIDITY_CONTROL_DEHUMIDIFY_ACTION: [
CONF_HUMIDITY_CONTROL_OFF_ACTION,
CONF_HUMIDITY_SENSOR,
],
CONF_HUMIDITY_CONTROL_HUMIDIFY_ACTION: [
CONF_HUMIDITY_CONTROL_OFF_ACTION,
CONF_HUMIDITY_SENSOR,
],
}
for config_trigger, req_triggers in requirements.items():
for req_trigger in req_triggers:
@@ -338,7 +351,7 @@ def validate_thermostat(config):
# Warn about using the removed CONF_DEFAULT_MODE and advise users
if CONF_DEFAULT_MODE in config and config[CONF_DEFAULT_MODE] is not None:
raise cv.Invalid(
f"{CONF_DEFAULT_MODE} is no longer valid. Please switch to using presets and specify a {CONF_DEFAULT_PRESET}."
f"{CONF_DEFAULT_MODE} is no longer valid. Please switch to using presets and specify a {CONF_DEFAULT_PRESET}"
)
default_mode = config[CONF_DEFAULT_MODE]
@@ -588,9 +601,24 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_SWING_VERTICAL_ACTION): automation.validate_automation(
single=True
),
cv.Optional(
CONF_TARGET_HUMIDITY_CHANGE_ACTION
): automation.validate_automation(single=True),
cv.Optional(
CONF_TARGET_TEMPERATURE_CHANGE_ACTION
): automation.validate_automation(single=True),
cv.Exclusive(
CONF_HUMIDITY_CONTROL_DEHUMIDIFY_ACTION,
group_of_exclusion="humidity_control",
): automation.validate_automation(single=True),
cv.Exclusive(
CONF_HUMIDITY_CONTROL_HUMIDIFY_ACTION,
group_of_exclusion="humidity_control",
): automation.validate_automation(single=True),
cv.Optional(
CONF_HUMIDITY_CONTROL_OFF_ACTION
): automation.validate_automation(single=True),
cv.Optional(CONF_HUMIDITY_HYSTERESIS, default=1.0): cv.percentage,
cv.Optional(CONF_DEFAULT_MODE, default=None): cv.valid,
cv.Optional(CONF_DEFAULT_PRESET): cv.templatable(cv.string),
cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature,
@@ -882,12 +910,39 @@ async def to_code(config):
config[CONF_SWING_VERTICAL_ACTION],
)
cg.add(var.set_supports_swing_mode_vertical(True))
if CONF_TARGET_HUMIDITY_CHANGE_ACTION in config:
await automation.build_automation(
var.get_humidity_change_trigger(),
[],
config[CONF_TARGET_HUMIDITY_CHANGE_ACTION],
)
if CONF_TARGET_TEMPERATURE_CHANGE_ACTION in config:
await automation.build_automation(
var.get_temperature_change_trigger(),
[],
config[CONF_TARGET_TEMPERATURE_CHANGE_ACTION],
)
if CONF_HUMIDITY_CONTROL_DEHUMIDIFY_ACTION in config:
cg.add(var.set_supports_dehumidification(True))
await automation.build_automation(
var.get_humidity_control_dehumidify_action_trigger(),
[],
config[CONF_HUMIDITY_CONTROL_DEHUMIDIFY_ACTION],
)
if CONF_HUMIDITY_CONTROL_HUMIDIFY_ACTION in config:
cg.add(var.set_supports_humidification(True))
await automation.build_automation(
var.get_humidity_control_humidify_action_trigger(),
[],
config[CONF_HUMIDITY_CONTROL_HUMIDIFY_ACTION],
)
if CONF_HUMIDITY_CONTROL_OFF_ACTION in config:
await automation.build_automation(
var.get_humidity_control_off_action_trigger(),
[],
config[CONF_HUMIDITY_CONTROL_OFF_ACTION],
)
cg.add(var.set_humidity_hysteresis(config[CONF_HUMIDITY_HYSTERESIS]))
if CONF_PRESET in config:
for preset_config in config[CONF_PRESET]:

View File

@@ -32,6 +32,7 @@ void ThermostatClimate::setup() {
if (this->humidity_sensor_ != nullptr) {
this->humidity_sensor_->add_on_state_callback([this](float state) {
this->current_humidity = state;
this->switch_to_humidity_control_action_(this->compute_humidity_control_action_());
this->publish_state();
});
this->current_humidity = this->humidity_sensor_->state;
@@ -84,6 +85,8 @@ void ThermostatClimate::refresh() {
this->switch_to_supplemental_action_(this->compute_supplemental_action_());
this->switch_to_fan_mode_(this->fan_mode.value(), false);
this->switch_to_swing_mode_(this->swing_mode, false);
this->switch_to_humidity_control_action_(this->compute_humidity_control_action_());
this->check_humidity_change_trigger_();
this->check_temperature_change_trigger_();
this->publish_state();
}
@@ -129,6 +132,11 @@ bool ThermostatClimate::hysteresis_valid() {
return true;
}
bool ThermostatClimate::humidity_hysteresis_valid() {
return !std::isnan(this->humidity_hysteresis_) && this->humidity_hysteresis_ >= 0.0f &&
this->humidity_hysteresis_ < 100.0f;
}
bool ThermostatClimate::limit_setpoints_for_heat_cool() {
return this->mode == climate::CLIMATE_MODE_HEAT_COOL ||
(this->mode == climate::CLIMATE_MODE_AUTO && this->supports_heat_cool_);
@@ -189,6 +197,16 @@ void ThermostatClimate::validate_target_temperature_high() {
}
}
void ThermostatClimate::validate_target_humidity() {
if (std::isnan(this->target_humidity)) {
this->target_humidity =
(this->get_traits().get_visual_max_humidity() - this->get_traits().get_visual_min_humidity()) / 2.0f;
} else {
this->target_humidity = clamp<float>(this->target_humidity, this->get_traits().get_visual_min_humidity(),
this->get_traits().get_visual_max_humidity());
}
}
void ThermostatClimate::control(const climate::ClimateCall &call) {
bool target_temperature_high_changed = false;
@@ -235,15 +253,27 @@ void ThermostatClimate::control(const climate::ClimateCall &call) {
this->validate_target_temperature();
}
}
if (call.get_target_humidity().has_value()) {
this->target_humidity = call.get_target_humidity().value();
this->validate_target_humidity();
}
// make any changes happen
this->refresh();
}
climate::ClimateTraits ThermostatClimate::traits() {
auto traits = climate::ClimateTraits();
traits.set_supports_current_temperature(true);
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_ACTION | climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
if (this->supports_two_points_)
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE);
if (this->humidity_sensor_ != nullptr)
traits.set_supports_current_humidity(true);
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY);
if (this->supports_humidification_ || this->supports_dehumidification_)
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY);
if (this->supports_auto_)
traits.add_supported_mode(climate::CLIMATE_MODE_AUTO);
@@ -294,9 +324,6 @@ climate::ClimateTraits ThermostatClimate::traits() {
for (auto &it : this->custom_preset_config_) {
traits.add_supported_custom_preset(it.first);
}
traits.set_supports_two_point_target_temperature(this->supports_two_points_);
traits.set_supports_action(true);
return traits;
}
@@ -421,6 +448,28 @@ climate::ClimateAction ThermostatClimate::compute_supplemental_action_() {
return target_action;
}
HumidificationAction ThermostatClimate::compute_humidity_control_action_() {
auto target_action = THERMOSTAT_HUMIDITY_CONTROL_ACTION_OFF;
// if hysteresis value or current_humidity is not valid, we go to OFF
if (std::isnan(this->current_humidity) || !this->humidity_hysteresis_valid()) {
return THERMOSTAT_HUMIDITY_CONTROL_ACTION_OFF;
}
// ensure set point is valid before computing the action
this->validate_target_humidity();
// everything has been validated so we can now safely compute the action
if (this->dehumidification_required_() && this->humidification_required_()) {
// this is bad and should never happen, so just stop.
// target_action = THERMOSTAT_HUMIDITY_CONTROL_ACTION_OFF;
} else if (this->supports_dehumidification_ && this->dehumidification_required_()) {
target_action = THERMOSTAT_HUMIDITY_CONTROL_ACTION_DEHUMIDIFY;
} else if (this->supports_humidification_ && this->humidification_required_()) {
target_action = THERMOSTAT_HUMIDITY_CONTROL_ACTION_HUMIDIFY;
}
return target_action;
}
void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool publish_state) {
// setup_complete_ helps us ensure an action is called immediately after boot
if ((action == this->action) && this->setup_complete_) {
@@ -594,6 +643,44 @@ void ThermostatClimate::trigger_supplemental_action_() {
}
}
void ThermostatClimate::switch_to_humidity_control_action_(HumidificationAction action) {
// setup_complete_ helps us ensure an action is called immediately after boot
if ((action == this->humidification_action_) && this->setup_complete_) {
// already in target mode
return;
}
Trigger<> *trig = this->humidity_control_off_action_trigger_;
switch (action) {
case THERMOSTAT_HUMIDITY_CONTROL_ACTION_OFF:
// trig = this->humidity_control_off_action_trigger_;
ESP_LOGVV(TAG, "Switching to HUMIDIFICATION_OFF action");
break;
case THERMOSTAT_HUMIDITY_CONTROL_ACTION_DEHUMIDIFY:
trig = this->humidity_control_dehumidify_action_trigger_;
ESP_LOGVV(TAG, "Switching to DEHUMIDIFY action");
break;
case THERMOSTAT_HUMIDITY_CONTROL_ACTION_HUMIDIFY:
trig = this->humidity_control_humidify_action_trigger_;
ESP_LOGVV(TAG, "Switching to HUMIDIFY action");
break;
case THERMOSTAT_HUMIDITY_CONTROL_ACTION_NONE:
default:
action = THERMOSTAT_HUMIDITY_CONTROL_ACTION_OFF;
// trig = this->humidity_control_off_action_trigger_;
}
if (this->prev_humidity_control_trigger_ != nullptr) {
this->prev_humidity_control_trigger_->stop_action();
this->prev_humidity_control_trigger_ = nullptr;
}
this->humidification_action_ = action;
this->prev_humidity_control_trigger_ = trig;
if (trig != nullptr) {
trig->trigger();
}
}
void ThermostatClimate::switch_to_fan_mode_(climate::ClimateFanMode fan_mode, bool publish_state) {
// setup_complete_ helps us ensure an action is called immediately after boot
if ((fan_mode == this->prev_fan_mode_) && this->setup_complete_) {
@@ -885,6 +972,20 @@ void ThermostatClimate::idle_on_timer_callback_() {
this->switch_to_supplemental_action_(this->compute_supplemental_action_());
}
void ThermostatClimate::check_humidity_change_trigger_() {
if ((this->prev_target_humidity_ == this->target_humidity) && this->setup_complete_) {
return; // nothing changed, no reason to trigger
} else {
// save the new temperature so we can check it again later; the trigger will fire below
this->prev_target_humidity_ = this->target_humidity;
}
// trigger the action
Trigger<> *trig = this->humidity_change_trigger_;
if (trig != nullptr) {
trig->trigger();
}
}
void ThermostatClimate::check_temperature_change_trigger_() {
if (this->supports_two_points_) {
// setup_complete_ helps us ensure an action is called immediately after boot
@@ -994,6 +1095,32 @@ bool ThermostatClimate::supplemental_heating_required_() {
(this->supplemental_action_ == climate::CLIMATE_ACTION_HEATING));
}
bool ThermostatClimate::dehumidification_required_() {
if (this->current_humidity > this->target_humidity + this->humidity_hysteresis_) {
// if the current humidity exceeds the target + hysteresis, dehumidification is required
return true;
} else if (this->current_humidity < this->target_humidity - this->humidity_hysteresis_) {
// if the current humidity is less than the target - hysteresis, dehumidification should stop
return false;
}
// if we get here, the current humidity is between target + hysteresis and target - hysteresis,
// so the action should not change
return this->humidification_action_ == THERMOSTAT_HUMIDITY_CONTROL_ACTION_DEHUMIDIFY;
}
bool ThermostatClimate::humidification_required_() {
if (this->current_humidity < this->target_humidity - this->humidity_hysteresis_) {
// if the current humidity is below the target - hysteresis, humidification is required
return true;
} else if (this->current_humidity > this->target_humidity + this->humidity_hysteresis_) {
// if the current humidity is above the target + hysteresis, humidification should stop
return false;
}
// if we get here, the current humidity is between target - hysteresis and target + hysteresis,
// so the action should not change
return this->humidification_action_ == THERMOSTAT_HUMIDITY_CONTROL_ACTION_HUMIDIFY;
}
void ThermostatClimate::dump_preset_config_(const char *preset_name, const ThermostatClimateTargetTempConfig &config) {
if (this->supports_heat_) {
ESP_LOGCONFIG(TAG, " Default Target Temperature Low: %.1f°C",
@@ -1150,8 +1277,12 @@ ThermostatClimate::ThermostatClimate()
swing_mode_off_trigger_(new Trigger<>()),
swing_mode_horizontal_trigger_(new Trigger<>()),
swing_mode_vertical_trigger_(new Trigger<>()),
humidity_change_trigger_(new Trigger<>()),
temperature_change_trigger_(new Trigger<>()),
preset_change_trigger_(new Trigger<>()) {}
preset_change_trigger_(new Trigger<>()),
humidity_control_dehumidify_action_trigger_(new Trigger<>()),
humidity_control_humidify_action_trigger_(new Trigger<>()),
humidity_control_off_action_trigger_(new Trigger<>()) {}
void ThermostatClimate::set_default_preset(const std::string &custom_preset) {
this->default_custom_preset_ = custom_preset;
@@ -1215,6 +1346,9 @@ void ThermostatClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sen
void ThermostatClimate::set_humidity_sensor(sensor::Sensor *humidity_sensor) {
this->humidity_sensor_ = humidity_sensor;
}
void ThermostatClimate::set_humidity_hysteresis(float humidity_hysteresis) {
this->humidity_hysteresis_ = std::clamp<float>(humidity_hysteresis, 0.0f, 100.0f);
}
void ThermostatClimate::set_use_startup_delay(bool use_startup_delay) { this->use_startup_delay_ = use_startup_delay; }
void ThermostatClimate::set_supports_heat_cool(bool supports_heat_cool) {
this->supports_heat_cool_ = supports_heat_cool;
@@ -1282,6 +1416,18 @@ void ThermostatClimate::set_supports_swing_mode_vertical(bool supports_swing_mod
void ThermostatClimate::set_supports_two_points(bool supports_two_points) {
this->supports_two_points_ = supports_two_points;
}
void ThermostatClimate::set_supports_dehumidification(bool supports_dehumidification) {
this->supports_dehumidification_ = supports_dehumidification;
if (supports_dehumidification) {
this->supports_humidification_ = false;
}
}
void ThermostatClimate::set_supports_humidification(bool supports_humidification) {
this->supports_humidification_ = supports_humidification;
if (supports_humidification) {
this->supports_dehumidification_ = false;
}
}
Trigger<> *ThermostatClimate::get_cool_action_trigger() const { return this->cool_action_trigger_; }
Trigger<> *ThermostatClimate::get_supplemental_cool_action_trigger() const {
@@ -1315,8 +1461,18 @@ Trigger<> *ThermostatClimate::get_swing_mode_both_trigger() const { return this-
Trigger<> *ThermostatClimate::get_swing_mode_off_trigger() const { return this->swing_mode_off_trigger_; }
Trigger<> *ThermostatClimate::get_swing_mode_horizontal_trigger() const { return this->swing_mode_horizontal_trigger_; }
Trigger<> *ThermostatClimate::get_swing_mode_vertical_trigger() const { return this->swing_mode_vertical_trigger_; }
Trigger<> *ThermostatClimate::get_humidity_change_trigger() const { return this->humidity_change_trigger_; }
Trigger<> *ThermostatClimate::get_temperature_change_trigger() const { return this->temperature_change_trigger_; }
Trigger<> *ThermostatClimate::get_preset_change_trigger() const { return this->preset_change_trigger_; }
Trigger<> *ThermostatClimate::get_humidity_control_dehumidify_action_trigger() const {
return this->humidity_control_dehumidify_action_trigger_;
}
Trigger<> *ThermostatClimate::get_humidity_control_humidify_action_trigger() const {
return this->humidity_control_humidify_action_trigger_;
}
Trigger<> *ThermostatClimate::get_humidity_control_off_action_trigger() const {
return this->humidity_control_off_action_trigger_;
}
void ThermostatClimate::dump_config() {
LOG_CLIMATE("", "Thermostat", this);
@@ -1420,7 +1576,12 @@ void ThermostatClimate::dump_config() {
" OFF: %s\n"
" HORIZONTAL: %s\n"
" VERTICAL: %s\n"
" Supports TWO SET POINTS: %s",
" Supports TWO SET POINTS: %s\n"
" Supported Humidity Parameters:\n"
" CURRENT: %s\n"
" TARGET: %s\n"
" DEHUMIDIFICATION: %s\n"
" HUMIDIFICATION: %s",
YESNO(this->supports_fan_mode_on_), YESNO(this->supports_fan_mode_off_),
YESNO(this->supports_fan_mode_auto_), YESNO(this->supports_fan_mode_low_),
YESNO(this->supports_fan_mode_medium_), YESNO(this->supports_fan_mode_high_),
@@ -1428,7 +1589,10 @@ void ThermostatClimate::dump_config() {
YESNO(this->supports_fan_mode_diffuse_), YESNO(this->supports_fan_mode_quiet_),
YESNO(this->supports_swing_mode_both_), YESNO(this->supports_swing_mode_off_),
YESNO(this->supports_swing_mode_horizontal_), YESNO(this->supports_swing_mode_vertical_),
YESNO(this->supports_two_points_));
YESNO(this->supports_two_points_),
YESNO(this->get_traits().has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)),
YESNO(this->supports_dehumidification_ || this->supports_humidification_),
YESNO(this->supports_dehumidification_), YESNO(this->supports_humidification_));
if (!this->preset_config_.empty()) {
ESP_LOGCONFIG(TAG, " Supported PRESETS:");

View File

@@ -13,6 +13,13 @@
namespace esphome {
namespace thermostat {
enum HumidificationAction : uint8_t {
THERMOSTAT_HUMIDITY_CONTROL_ACTION_OFF = 0,
THERMOSTAT_HUMIDITY_CONTROL_ACTION_DEHUMIDIFY = 1,
THERMOSTAT_HUMIDITY_CONTROL_ACTION_HUMIDIFY = 2,
THERMOSTAT_HUMIDITY_CONTROL_ACTION_NONE,
};
enum ThermostatClimateTimerIndex : uint8_t {
THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME = 0,
THERMOSTAT_TIMER_COOLING_OFF = 1,
@@ -90,6 +97,7 @@ class ThermostatClimate : public climate::Climate, public Component {
void set_idle_minimum_time_in_sec(uint32_t time);
void set_sensor(sensor::Sensor *sensor);
void set_humidity_sensor(sensor::Sensor *humidity_sensor);
void set_humidity_hysteresis(float humidity_hysteresis);
void set_use_startup_delay(bool use_startup_delay);
void set_supports_auto(bool supports_auto);
void set_supports_heat_cool(bool supports_heat_cool);
@@ -115,6 +123,8 @@ class ThermostatClimate : public climate::Climate, public Component {
void set_supports_swing_mode_horizontal(bool supports_swing_mode_horizontal);
void set_supports_swing_mode_off(bool supports_swing_mode_off);
void set_supports_swing_mode_vertical(bool supports_swing_mode_vertical);
void set_supports_dehumidification(bool supports_dehumidification);
void set_supports_humidification(bool supports_humidification);
void set_supports_two_points(bool supports_two_points);
void set_preset_config(climate::ClimatePreset preset, const ThermostatClimateTargetTempConfig &config);
@@ -148,8 +158,12 @@ class ThermostatClimate : public climate::Climate, public Component {
Trigger<> *get_swing_mode_horizontal_trigger() const;
Trigger<> *get_swing_mode_off_trigger() const;
Trigger<> *get_swing_mode_vertical_trigger() const;
Trigger<> *get_humidity_change_trigger() const;
Trigger<> *get_temperature_change_trigger() const;
Trigger<> *get_preset_change_trigger() const;
Trigger<> *get_humidity_control_dehumidify_action_trigger() const;
Trigger<> *get_humidity_control_humidify_action_trigger() const;
Trigger<> *get_humidity_control_off_action_trigger() const;
/// Get current hysteresis values
float cool_deadband();
float cool_overrun();
@@ -166,11 +180,13 @@ class ThermostatClimate : public climate::Climate, public Component {
climate::ClimateFanMode locked_fan_mode();
/// Set point and hysteresis validation
bool hysteresis_valid(); // returns true if valid
bool humidity_hysteresis_valid(); // returns true if valid
bool limit_setpoints_for_heat_cool(); // returns true if set points should be further limited within visual range
void validate_target_temperature();
void validate_target_temperatures(bool pin_target_temperature_high);
void validate_target_temperature_low();
void validate_target_temperature_high();
void validate_target_humidity();
protected:
/// Override control to change settings of the climate device.
@@ -192,11 +208,13 @@ class ThermostatClimate : public climate::Climate, public Component {
/// Re-compute the required action of this climate controller.
climate::ClimateAction compute_action_(bool ignore_timers = false);
climate::ClimateAction compute_supplemental_action_();
HumidificationAction compute_humidity_control_action_();
/// Switch the climate device to the given climate action.
void switch_to_action_(climate::ClimateAction action, bool publish_state = true);
void switch_to_supplemental_action_(climate::ClimateAction action);
void trigger_supplemental_action_();
void switch_to_humidity_control_action_(HumidificationAction action);
/// Switch the climate device to the given climate fan mode.
void switch_to_fan_mode_(climate::ClimateFanMode fan_mode, bool publish_state = true);
@@ -207,6 +225,9 @@ class ThermostatClimate : public climate::Climate, public Component {
/// Switch the climate device to the given climate swing mode.
void switch_to_swing_mode_(climate::ClimateSwingMode swing_mode, bool publish_state = true);
/// Check if the humidity change trigger should be called.
void check_humidity_change_trigger_();
/// Check if the temperature change trigger should be called.
void check_temperature_change_trigger_();
@@ -243,6 +264,8 @@ class ThermostatClimate : public climate::Climate, public Component {
bool heating_required_();
bool supplemental_cooling_required_();
bool supplemental_heating_required_();
bool dehumidification_required_();
bool humidification_required_();
void dump_preset_config_(const char *preset_name, const ThermostatClimateTargetTempConfig &config);
@@ -259,6 +282,9 @@ class ThermostatClimate : public climate::Climate, public Component {
/// The current supplemental action
climate::ClimateAction supplemental_action_{climate::CLIMATE_ACTION_OFF};
/// The current humidification action
HumidificationAction humidification_action_{THERMOSTAT_HUMIDITY_CONTROL_ACTION_NONE};
/// Default standard preset to use on start up
climate::ClimatePreset default_preset_{};
@@ -321,6 +347,12 @@ class ThermostatClimate : public climate::Climate, public Component {
/// A false value means that the controller has no such support.
bool supports_two_points_{false};
/// Whether the controller supports dehumidification and/or humidification
///
/// A false value means that the controller has no such support.
bool supports_dehumidification_{false};
bool supports_humidification_{false};
/// Flags indicating if maximum allowable run time was exceeded
bool cooling_max_runtime_exceeded_{false};
bool heating_max_runtime_exceeded_{false};
@@ -331,9 +363,10 @@ class ThermostatClimate : public climate::Climate, public Component {
/// setup_complete_ blocks modifying/resetting the temps immediately after boot
bool setup_complete_{false};
/// Store previously-known temperatures
/// Store previously-known humidity and temperatures
///
/// These are used to determine when the temperature change trigger/action needs to be called
/// These are used to determine when a temperature/humidity has changed
float prev_target_humidity_{NAN};
float prev_target_temperature_{NAN};
float prev_target_temperature_low_{NAN};
float prev_target_temperature_high_{NAN};
@@ -347,6 +380,9 @@ class ThermostatClimate : public climate::Climate, public Component {
float heating_deadband_{0};
float heating_overrun_{0};
/// Hysteresis values used for computing humidification action
float humidity_hysteresis_{0};
/// Maximum allowable temperature deltas before engaging supplemental cooling/heating actions
float supplemental_cool_delta_{0};
float supplemental_heat_delta_{0};
@@ -448,12 +484,24 @@ class ThermostatClimate : public climate::Climate, public Component {
/// The trigger to call when the controller should switch the swing mode to "vertical".
Trigger<> *swing_mode_vertical_trigger_{nullptr};
/// The trigger to call when the target humidity changes.
Trigger<> *humidity_change_trigger_{nullptr};
/// The trigger to call when the target temperature(s) change(es).
Trigger<> *temperature_change_trigger_{nullptr};
/// The trigger to call when the preset mode changes
Trigger<> *preset_change_trigger_{nullptr};
/// The trigger to call when dehumidification is required
Trigger<> *humidity_control_dehumidify_action_trigger_{nullptr};
/// The trigger to call when humidification is required
Trigger<> *humidity_control_humidify_action_trigger_{nullptr};
/// The trigger to call when (de)humidification should stop
Trigger<> *humidity_control_off_action_trigger_{nullptr};
/// A reference to the trigger that was previously active.
///
/// This is so that the previous trigger can be stopped before enabling a new one
@@ -462,6 +510,7 @@ class ThermostatClimate : public climate::Climate, public Component {
Trigger<> *prev_fan_mode_trigger_{nullptr};
Trigger<> *prev_mode_trigger_{nullptr};
Trigger<> *prev_swing_mode_trigger_{nullptr};
Trigger<> *prev_humidity_control_trigger_{nullptr};
/// Default custom preset to use on start up
std::string default_custom_preset_{};

View File

@@ -27,6 +27,14 @@ class RealTimeClock : public PollingComponent {
this->apply_timezone_();
}
/// Set the time zone from raw buffer, only if it differs from the current one.
void set_timezone(const char *tz, size_t len) {
if (this->timezone_.length() != len || memcmp(this->timezone_.c_str(), tz, len) != 0) {
this->timezone_.assign(tz, len);
this->apply_timezone_();
}
}
/// Get the time zone currently in use.
std::string get_timezone() { return this->timezone_; }
#endif

View File

@@ -14,6 +14,7 @@ MODELS = {
"GENERIC": Model.MODEL_GENERIC,
"RAC-PT1411HWRU-C": Model.MODEL_RAC_PT1411HWRU_C,
"RAC-PT1411HWRU-F": Model.MODEL_RAC_PT1411HWRU_F,
"RAS-2819T": Model.MODEL_RAS_2819T,
}
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(ToshibaClimate).extend(

View File

@@ -1,4 +1,5 @@
#include "toshiba.h"
#include "esphome/components/remote_base/toshiba_ac_protocol.h"
#include <vector>
@@ -97,6 +98,282 @@ const std::vector<uint8_t> RAC_PT1411HWRU_TEMPERATURE_F{0x10, 0x30, 0x00, 0x20,
0x22, 0x06, 0x26, 0x07, 0x05, 0x25, 0x04, 0x24, 0x0C,
0x2C, 0x0D, 0x2D, 0x09, 0x08, 0x28, 0x0A, 0x2A, 0x0B};
// RAS-2819T protocol constants
const uint16_t RAS_2819T_HEADER1 = 0xC23D;
const uint8_t RAS_2819T_HEADER2 = 0xD5;
const uint8_t RAS_2819T_MESSAGE_LENGTH = 6;
// RAS-2819T fan speed codes for rc_code_1 (bytes 2-3)
const uint16_t RAS_2819T_FAN_AUTO = 0xBF40;
const uint16_t RAS_2819T_FAN_QUIET = 0xFF00;
const uint16_t RAS_2819T_FAN_LOW = 0x9F60;
const uint16_t RAS_2819T_FAN_MEDIUM = 0x5FA0;
const uint16_t RAS_2819T_FAN_HIGH = 0x3FC0;
// RAS-2819T fan speed codes for rc_code_2 (byte 1)
const uint8_t RAS_2819T_FAN2_AUTO = 0x66;
const uint8_t RAS_2819T_FAN2_QUIET = 0x01;
const uint8_t RAS_2819T_FAN2_LOW = 0x28;
const uint8_t RAS_2819T_FAN2_MEDIUM = 0x3C;
const uint8_t RAS_2819T_FAN2_HIGH = 0x50;
// RAS-2819T second packet suffix bytes for rc_code_2 (bytes 3-5)
// These are fixed patterns, not actual checksums
struct Ras2819tPacketSuffix {
uint8_t byte3;
uint8_t byte4;
uint8_t byte5;
};
const Ras2819tPacketSuffix RAS_2819T_SUFFIX_AUTO{0x00, 0x02, 0x3D};
const Ras2819tPacketSuffix RAS_2819T_SUFFIX_QUIET{0x00, 0x02, 0xD8};
const Ras2819tPacketSuffix RAS_2819T_SUFFIX_LOW{0x00, 0x02, 0xFF};
const Ras2819tPacketSuffix RAS_2819T_SUFFIX_MEDIUM{0x00, 0x02, 0x13};
const Ras2819tPacketSuffix RAS_2819T_SUFFIX_HIGH{0x00, 0x02, 0x27};
// RAS-2819T swing toggle command
const uint64_t RAS_2819T_SWING_TOGGLE = 0xC23D6B94E01F;
// RAS-2819T single-packet commands
const uint64_t RAS_2819T_POWER_OFF_COMMAND = 0xC23D7B84E01F;
// RAS-2819T known valid command patterns for validation
const std::array<uint64_t, 2> RAS_2819T_VALID_SINGLE_COMMANDS = {
RAS_2819T_POWER_OFF_COMMAND, // Power off
RAS_2819T_SWING_TOGGLE, // Swing toggle
};
const uint16_t RAS_2819T_VALID_HEADER1 = 0xC23D;
const uint8_t RAS_2819T_VALID_HEADER2 = 0xD5;
const uint8_t RAS_2819T_DRY_BYTE2 = 0x1F;
const uint8_t RAS_2819T_DRY_BYTE3 = 0xE0;
const uint8_t RAS_2819T_DRY_TEMP_OFFSET = 0x24;
const uint8_t RAS_2819T_AUTO_BYTE2 = 0x1F;
const uint8_t RAS_2819T_AUTO_BYTE3 = 0xE0;
const uint8_t RAS_2819T_AUTO_TEMP_OFFSET = 0x08;
const uint8_t RAS_2819T_FAN_ONLY_TEMP = 0xE4;
const uint8_t RAS_2819T_FAN_ONLY_TEMP_INV = 0x1B;
const uint8_t RAS_2819T_HEAT_TEMP_OFFSET = 0x0C;
// RAS-2819T second packet fixed values
const uint8_t RAS_2819T_AUTO_DRY_FAN_BYTE = 0x65;
const uint8_t RAS_2819T_AUTO_DRY_SUFFIX = 0x3A;
const uint8_t RAS_2819T_HEAT_SUFFIX = 0x3B;
// RAS-2819T temperature codes for 18-30°C
static const uint8_t RAS_2819T_TEMP_CODES[] = {
0x10, // 18°C
0x30, // 19°C
0x20, // 20°C
0x60, // 21°C
0x70, // 22°C
0x50, // 23°C
0x40, // 24°C
0xC0, // 25°C
0xD0, // 26°C
0x90, // 27°C
0x80, // 28°C
0xA0, // 29°C
0xB0 // 30°C
};
// Helper functions for RAS-2819T protocol
//
// ===== RAS-2819T PROTOCOL DOCUMENTATION =====
//
// The RAS-2819T uses a two-packet IR protocol with some exceptions for simple commands.
//
// PACKET STRUCTURE:
// All packets are 6 bytes (48 bits) transmitted with standard Toshiba timing.
//
// TWO-PACKET COMMANDS (Mode/Temperature/Fan changes):
//
// First Packet (rc_code_1): [C2 3D] [FAN_HI FAN_LO] [TEMP] [~TEMP]
// Byte 0-1: Header (always 0xC23D)
// Byte 2-3: Fan speed encoding (varies by mode, see fan tables below)
// Byte 4: Temperature + mode encoding
// Byte 5: Bitwise complement of temperature byte
//
// Second Packet (rc_code_2): [D5] [FAN2] [00] [SUF1] [SUF2] [SUF3]
// Byte 0: Header (always 0xD5)
// Byte 1: Fan speed secondary encoding
// Byte 2: Always 0x00
// Byte 3-5: Fixed suffix pattern (depends on fan speed and mode)
//
// TEMPERATURE ENCODING:
// Base temp codes: 18°C=0x10, 19°C=0x30, 20°C=0x20, 21°C=0x60, 22°C=0x70,
// 23°C=0x50, 24°C=0x40, 25°C=0xC0, 26°C=0xD0, 27°C=0x90,
// 28°C=0x80, 29°C=0xA0, 30°C=0xB0
// Mode offsets added to base temp:
// COOL: No offset
// HEAT: +0x0C (e.g., 24°C heat = 0x40 | 0x0C = 0x4C)
// AUTO: +0x08 (e.g., 24°C auto = 0x40 | 0x08 = 0x48)
// DRY: +0x24 (e.g., 24°C dry = 0x40 | 0x24 = 0x64)
//
// FAN SPEED ENCODING (First packet bytes 2-3):
// AUTO: 0xBF40, QUIET: 0xFF00, LOW: 0x9F60, MEDIUM: 0x5FA0, HIGH: 0x3FC0
// Special cases: AUTO/DRY modes use 0x1FE0 instead
//
// SINGLE-PACKET COMMANDS:
// Power Off: 0xC23D7B84E01F (6 bytes, no second packet)
// Swing Toggle: 0xC23D6B94E01F (6 bytes, no second packet)
//
// MODE DETECTION (from first packet):
// - Check bytes 2-3: if 0x7B84 → OFF mode
// - Check bytes 2-3: if 0x1FE0 → AUTO/DRY/low-temp-COOL (distinguish by temp code)
// - Otherwise: COOL/HEAT/FAN_ONLY (distinguish by temp code and byte 5)
/**
* Get fan speed encoding for RAS-2819T first packet (rc_code_1, bytes 2-3)
*/
static uint16_t get_ras_2819t_fan_code(climate::ClimateFanMode fan_mode) {
switch (fan_mode) {
case climate::CLIMATE_FAN_QUIET:
return RAS_2819T_FAN_QUIET;
case climate::CLIMATE_FAN_LOW:
return RAS_2819T_FAN_LOW;
case climate::CLIMATE_FAN_MEDIUM:
return RAS_2819T_FAN_MEDIUM;
case climate::CLIMATE_FAN_HIGH:
return RAS_2819T_FAN_HIGH;
case climate::CLIMATE_FAN_AUTO:
default:
return RAS_2819T_FAN_AUTO;
}
}
/**
* Get fan speed encoding for RAS-2819T rc_code_2 packet (second packet)
*/
struct Ras2819tSecondPacketCodes {
uint8_t fan_byte;
Ras2819tPacketSuffix suffix;
};
static Ras2819tSecondPacketCodes get_ras_2819t_second_packet_codes(climate::ClimateFanMode fan_mode) {
switch (fan_mode) {
case climate::CLIMATE_FAN_QUIET:
return {RAS_2819T_FAN2_QUIET, RAS_2819T_SUFFIX_QUIET};
case climate::CLIMATE_FAN_LOW:
return {RAS_2819T_FAN2_LOW, RAS_2819T_SUFFIX_LOW};
case climate::CLIMATE_FAN_MEDIUM:
return {RAS_2819T_FAN2_MEDIUM, RAS_2819T_SUFFIX_MEDIUM};
case climate::CLIMATE_FAN_HIGH:
return {RAS_2819T_FAN2_HIGH, RAS_2819T_SUFFIX_HIGH};
case climate::CLIMATE_FAN_AUTO:
default:
return {RAS_2819T_FAN2_AUTO, RAS_2819T_SUFFIX_AUTO};
}
}
/**
* Get temperature code for RAS-2819T protocol
*/
static uint8_t get_ras_2819t_temp_code(float temperature) {
int temp_index = static_cast<int>(temperature) - 18;
if (temp_index < 0 || temp_index >= static_cast<int>(sizeof(RAS_2819T_TEMP_CODES))) {
ESP_LOGW(TAG, "Temperature %.1f°C out of range [18-30°C], defaulting to 24°C", temperature);
return 0x40; // Default to 24°C
}
return RAS_2819T_TEMP_CODES[temp_index];
}
/**
* Decode temperature from RAS-2819T temp code
*/
static float decode_ras_2819t_temperature(uint8_t temp_code) {
uint8_t base_temp_code = temp_code & 0xF0;
// Find the code in the temperature array
for (size_t temp_index = 0; temp_index < sizeof(RAS_2819T_TEMP_CODES); temp_index++) {
if (RAS_2819T_TEMP_CODES[temp_index] == base_temp_code) {
return static_cast<float>(temp_index + 18); // 18°C is the minimum
}
}
ESP_LOGW(TAG, "Unknown temp code: 0x%02X, defaulting to 24°C", base_temp_code);
return 24.0f; // Default to 24°C
}
/**
* Decode fan speed from RAS-2819T IR codes
*/
static climate::ClimateFanMode decode_ras_2819t_fan_mode(uint16_t fan_code) {
switch (fan_code) {
case RAS_2819T_FAN_QUIET:
return climate::CLIMATE_FAN_QUIET;
case RAS_2819T_FAN_LOW:
return climate::CLIMATE_FAN_LOW;
case RAS_2819T_FAN_MEDIUM:
return climate::CLIMATE_FAN_MEDIUM;
case RAS_2819T_FAN_HIGH:
return climate::CLIMATE_FAN_HIGH;
case RAS_2819T_FAN_AUTO:
default:
return climate::CLIMATE_FAN_AUTO;
}
}
/**
* Validate RAS-2819T IR command structure and content
*/
static bool is_valid_ras_2819t_command(uint64_t rc_code_1, uint64_t rc_code_2 = 0) {
// Check header of first packet
uint16_t header1 = (rc_code_1 >> 32) & 0xFFFF;
if (header1 != RAS_2819T_VALID_HEADER1) {
return false;
}
// Single packet commands
if (rc_code_2 == 0) {
for (uint64_t valid_cmd : RAS_2819T_VALID_SINGLE_COMMANDS) {
if (rc_code_1 == valid_cmd) {
return true;
}
}
// Additional validation for unknown single packets
return false;
}
// Two-packet commands - validate second packet header
uint8_t header2 = (rc_code_2 >> 40) & 0xFF;
if (header2 != RAS_2819T_VALID_HEADER2) {
return false;
}
// Validate temperature complement in first packet (byte 4 should be ~byte 5)
uint8_t temp_byte = (rc_code_1 >> 8) & 0xFF;
uint8_t temp_complement = rc_code_1 & 0xFF;
if (temp_byte != static_cast<uint8_t>(~temp_complement)) {
return false;
}
// Validate fan speed combinations make sense
uint16_t fan_code = (rc_code_1 >> 16) & 0xFFFF;
uint8_t fan2_byte = (rc_code_2 >> 32) & 0xFF;
// Check if fan codes are from known valid patterns
bool valid_fan_combo = false;
if (fan_code == RAS_2819T_FAN_AUTO && fan2_byte == RAS_2819T_FAN2_AUTO)
valid_fan_combo = true;
if (fan_code == RAS_2819T_FAN_QUIET && fan2_byte == RAS_2819T_FAN2_QUIET)
valid_fan_combo = true;
if (fan_code == RAS_2819T_FAN_LOW && fan2_byte == RAS_2819T_FAN2_LOW)
valid_fan_combo = true;
if (fan_code == RAS_2819T_FAN_MEDIUM && fan2_byte == RAS_2819T_FAN2_MEDIUM)
valid_fan_combo = true;
if (fan_code == RAS_2819T_FAN_HIGH && fan2_byte == RAS_2819T_FAN2_HIGH)
valid_fan_combo = true;
if (fan_code == 0x1FE0 && fan2_byte == RAS_2819T_AUTO_DRY_FAN_BYTE)
valid_fan_combo = true; // AUTO/DRY
return valid_fan_combo;
}
void ToshibaClimate::setup() {
if (this->sensor_) {
this->sensor_->add_on_state_callback([this](float state) {
@@ -126,16 +403,43 @@ void ToshibaClimate::setup() {
this->minimum_temperature_ = this->temperature_min_();
this->maximum_temperature_ = this->temperature_max_();
this->swing_modes_ = this->toshiba_swing_modes_();
// Ensure swing mode is always initialized to a valid value
if (this->swing_modes_.empty() || this->swing_modes_.find(this->swing_mode) == this->swing_modes_.end()) {
// No swing support for this model or current swing mode not supported, reset to OFF
this->swing_mode = climate::CLIMATE_SWING_OFF;
}
// Ensure mode is valid - ESPHome should only use standard climate modes
if (this->mode != climate::CLIMATE_MODE_OFF && this->mode != climate::CLIMATE_MODE_HEAT &&
this->mode != climate::CLIMATE_MODE_COOL && this->mode != climate::CLIMATE_MODE_HEAT_COOL &&
this->mode != climate::CLIMATE_MODE_DRY && this->mode != climate::CLIMATE_MODE_FAN_ONLY) {
ESP_LOGW(TAG, "Invalid mode detected during setup, resetting to OFF");
this->mode = climate::CLIMATE_MODE_OFF;
}
// Ensure fan mode is valid
if (!this->fan_mode.has_value()) {
ESP_LOGW(TAG, "Fan mode not set during setup, defaulting to AUTO");
this->fan_mode = climate::CLIMATE_FAN_AUTO;
}
// Never send nan to HA
if (std::isnan(this->target_temperature))
this->target_temperature = 24;
// Log final state for debugging HA errors
ESP_LOGV(TAG, "Setup complete - Mode: %d, Fan: %s, Swing: %d, Temp: %.1f", static_cast<int>(this->mode),
this->fan_mode.has_value() ? std::to_string(static_cast<int>(this->fan_mode.value())).c_str() : "NONE",
static_cast<int>(this->swing_mode), this->target_temperature);
}
void ToshibaClimate::transmit_state() {
if (this->model_ == MODEL_RAC_PT1411HWRU_C || this->model_ == MODEL_RAC_PT1411HWRU_F) {
transmit_rac_pt1411hwru_();
this->transmit_rac_pt1411hwru_();
} else if (this->model_ == MODEL_RAS_2819T) {
this->transmit_ras_2819t_();
} else {
transmit_generic_();
this->transmit_generic_();
}
}
@@ -230,7 +534,7 @@ void ToshibaClimate::transmit_generic_() {
auto transmit = this->transmitter_->transmit();
auto *data = transmit.get_data();
encode_(data, message, message_length, 1);
this->encode_(data, message, message_length, 1);
transmit.perform();
}
@@ -348,15 +652,12 @@ void ToshibaClimate::transmit_rac_pt1411hwru_() {
message[11] += message[index];
}
}
ESP_LOGV(TAG, "*** Generated codes: 0x%.2X%.2X%.2X%.2X%.2X%.2X 0x%.2X%.2X%.2X%.2X%.2X%.2X", message[0], message[1],
message[2], message[3], message[4], message[5], message[6], message[7], message[8], message[9], message[10],
message[11]);
// load first block of IR code and repeat it once
encode_(data, &message[0], RAC_PT1411HWRU_MESSAGE_LENGTH, 1);
this->encode_(data, &message[0], RAC_PT1411HWRU_MESSAGE_LENGTH, 1);
// load second block of IR code, if present
if (message[6] != 0) {
encode_(data, &message[6], RAC_PT1411HWRU_MESSAGE_LENGTH, 0);
this->encode_(data, &message[6], RAC_PT1411HWRU_MESSAGE_LENGTH, 0);
}
transmit.perform();
@@ -366,19 +667,19 @@ void ToshibaClimate::transmit_rac_pt1411hwru_() {
data->space(TOSHIBA_PACKET_SPACE);
switch (this->swing_mode) {
case climate::CLIMATE_SWING_VERTICAL:
encode_(data, &RAC_PT1411HWRU_SWING_VERTICAL[0], RAC_PT1411HWRU_MESSAGE_LENGTH, 1);
this->encode_(data, &RAC_PT1411HWRU_SWING_VERTICAL[0], RAC_PT1411HWRU_MESSAGE_LENGTH, 1);
break;
case climate::CLIMATE_SWING_OFF:
default:
encode_(data, &RAC_PT1411HWRU_SWING_OFF[0], RAC_PT1411HWRU_MESSAGE_LENGTH, 1);
this->encode_(data, &RAC_PT1411HWRU_SWING_OFF[0], RAC_PT1411HWRU_MESSAGE_LENGTH, 1);
}
data->space(TOSHIBA_PACKET_SPACE);
transmit.perform();
if (this->sensor_) {
transmit_rac_pt1411hwru_temp_(true, false);
this->transmit_rac_pt1411hwru_temp_(true, false);
}
}
@@ -430,15 +731,217 @@ void ToshibaClimate::transmit_rac_pt1411hwru_temp_(const bool cs_state, const bo
// Byte 5: Footer lower/bitwise complement of byte 4
message[5] = ~message[4];
ESP_LOGV(TAG, "*** Generated code: 0x%.2X%.2X%.2X%.2X%.2X%.2X", message[0], message[1], message[2], message[3],
message[4], message[5]);
// load IR code and repeat it once
encode_(data, message, RAC_PT1411HWRU_MESSAGE_LENGTH, 1);
this->encode_(data, message, RAC_PT1411HWRU_MESSAGE_LENGTH, 1);
transmit.perform();
}
}
void ToshibaClimate::transmit_ras_2819t_() {
// Handle swing mode transmission for RAS-2819T
// Note: RAS-2819T uses a toggle command, so we need to track state changes
// Check if ONLY swing mode changed (and no other climate parameters)
bool swing_changed = (this->swing_mode != this->last_swing_mode_);
bool mode_changed = (this->mode != this->last_mode_);
bool fan_changed = (this->fan_mode != this->last_fan_mode_);
bool temp_changed = (abs(this->target_temperature - this->last_target_temperature_) > 0.1f);
bool only_swing_changed = swing_changed && !mode_changed && !fan_changed && !temp_changed;
if (only_swing_changed) {
// Send ONLY swing toggle command (like the physical remote does)
auto swing_transmit = this->transmitter_->transmit();
auto *swing_data = swing_transmit.get_data();
// Convert toggle command to bytes for transmission
uint8_t swing_message[RAS_2819T_MESSAGE_LENGTH];
swing_message[0] = (RAS_2819T_SWING_TOGGLE >> 40) & 0xFF;
swing_message[1] = (RAS_2819T_SWING_TOGGLE >> 32) & 0xFF;
swing_message[2] = (RAS_2819T_SWING_TOGGLE >> 24) & 0xFF;
swing_message[3] = (RAS_2819T_SWING_TOGGLE >> 16) & 0xFF;
swing_message[4] = (RAS_2819T_SWING_TOGGLE >> 8) & 0xFF;
swing_message[5] = RAS_2819T_SWING_TOGGLE & 0xFF;
// Use single packet transmission WITH repeat (like regular commands)
this->encode_(swing_data, swing_message, RAS_2819T_MESSAGE_LENGTH, 1);
swing_transmit.perform();
// Update all state tracking
this->last_swing_mode_ = this->swing_mode;
this->last_mode_ = this->mode;
this->last_fan_mode_ = this->fan_mode;
this->last_target_temperature_ = this->target_temperature;
// Immediately publish the state change to Home Assistant
this->publish_state();
return; // Exit early - don't send climate command
}
// If we get here, send the regular climate command (temperature/mode/fan)
uint8_t message1[RAS_2819T_MESSAGE_LENGTH] = {0};
uint8_t message2[RAS_2819T_MESSAGE_LENGTH] = {0};
float temperature =
clamp<float>(this->target_temperature, TOSHIBA_RAS_2819T_TEMP_C_MIN, TOSHIBA_RAS_2819T_TEMP_C_MAX);
// Build first packet (RAS_2819T_HEADER1 + 4 bytes)
message1[0] = (RAS_2819T_HEADER1 >> 8) & 0xFF;
message1[1] = RAS_2819T_HEADER1 & 0xFF;
// Handle OFF mode
if (this->mode == climate::CLIMATE_MODE_OFF) {
// Extract bytes from power off command constant
message1[2] = (RAS_2819T_POWER_OFF_COMMAND >> 24) & 0xFF;
message1[3] = (RAS_2819T_POWER_OFF_COMMAND >> 16) & 0xFF;
message1[4] = (RAS_2819T_POWER_OFF_COMMAND >> 8) & 0xFF;
message1[5] = RAS_2819T_POWER_OFF_COMMAND & 0xFF;
// No second packet for OFF
} else {
// Get temperature and fan encoding
uint8_t temp_code = get_ras_2819t_temp_code(temperature);
// Get fan speed encoding for rc_code_1
climate::ClimateFanMode effective_fan_mode = this->fan_mode.value();
// Dry mode only supports AUTO fan speed
if (this->mode == climate::CLIMATE_MODE_DRY) {
effective_fan_mode = climate::CLIMATE_FAN_AUTO;
if (this->fan_mode.value() != climate::CLIMATE_FAN_AUTO) {
ESP_LOGW(TAG, "Dry mode only supports AUTO fan speed, forcing AUTO");
}
}
uint16_t fan_code = get_ras_2819t_fan_code(effective_fan_mode);
// Mode and temperature encoding
switch (this->mode) {
case climate::CLIMATE_MODE_COOL:
// All cooling temperatures support fan speed control
message1[2] = (fan_code >> 8) & 0xFF;
message1[3] = fan_code & 0xFF;
message1[4] = temp_code;
message1[5] = ~temp_code;
break;
case climate::CLIMATE_MODE_HEAT:
// Heating supports fan speed control
message1[2] = (fan_code >> 8) & 0xFF;
message1[3] = fan_code & 0xFF;
// Heat mode adds offset to temperature code
message1[4] = temp_code | RAS_2819T_HEAT_TEMP_OFFSET;
message1[5] = ~(temp_code | RAS_2819T_HEAT_TEMP_OFFSET);
break;
case climate::CLIMATE_MODE_HEAT_COOL:
// Auto mode uses fixed encoding
message1[2] = RAS_2819T_AUTO_BYTE2;
message1[3] = RAS_2819T_AUTO_BYTE3;
message1[4] = temp_code | RAS_2819T_AUTO_TEMP_OFFSET;
message1[5] = ~(temp_code | RAS_2819T_AUTO_TEMP_OFFSET);
break;
case climate::CLIMATE_MODE_DRY:
// Dry mode uses fixed encoding and forces AUTO fan
message1[2] = RAS_2819T_DRY_BYTE2;
message1[3] = RAS_2819T_DRY_BYTE3;
message1[4] = temp_code | RAS_2819T_DRY_TEMP_OFFSET;
message1[5] = ~message1[4];
break;
case climate::CLIMATE_MODE_FAN_ONLY:
// Fan only mode supports fan speed control
message1[2] = (fan_code >> 8) & 0xFF;
message1[3] = fan_code & 0xFF;
message1[4] = RAS_2819T_FAN_ONLY_TEMP;
message1[5] = RAS_2819T_FAN_ONLY_TEMP_INV;
break;
default:
// Default case supports fan speed control
message1[2] = (fan_code >> 8) & 0xFF;
message1[3] = fan_code & 0xFF;
message1[4] = temp_code;
message1[5] = ~temp_code;
break;
}
// Build second packet (RAS_2819T_HEADER2 + 4 bytes)
message2[0] = RAS_2819T_HEADER2;
// Get fan speed encoding for rc_code_2
Ras2819tSecondPacketCodes second_packet_codes = get_ras_2819t_second_packet_codes(effective_fan_mode);
// Determine header byte 2 and fan encoding based on mode
switch (this->mode) {
case climate::CLIMATE_MODE_COOL:
message2[1] = second_packet_codes.fan_byte;
message2[2] = 0x00;
message2[3] = second_packet_codes.suffix.byte3;
message2[4] = second_packet_codes.suffix.byte4;
message2[5] = second_packet_codes.suffix.byte5;
break;
case climate::CLIMATE_MODE_HEAT:
message2[1] = second_packet_codes.fan_byte;
message2[2] = 0x00;
message2[3] = second_packet_codes.suffix.byte3;
message2[4] = 0x00;
message2[5] = RAS_2819T_HEAT_SUFFIX;
break;
case climate::CLIMATE_MODE_HEAT_COOL:
case climate::CLIMATE_MODE_DRY:
// Auto/Dry modes use fixed values regardless of fan setting
message2[1] = RAS_2819T_AUTO_DRY_FAN_BYTE;
message2[2] = 0x00;
message2[3] = 0x00;
message2[4] = 0x00;
message2[5] = RAS_2819T_AUTO_DRY_SUFFIX;
break;
case climate::CLIMATE_MODE_FAN_ONLY:
message2[1] = second_packet_codes.fan_byte;
message2[2] = 0x00;
message2[3] = second_packet_codes.suffix.byte3;
message2[4] = 0x00;
message2[5] = RAS_2819T_HEAT_SUFFIX;
break;
default:
message2[1] = second_packet_codes.fan_byte;
message2[2] = 0x00;
message2[3] = second_packet_codes.suffix.byte3;
message2[4] = second_packet_codes.suffix.byte4;
message2[5] = second_packet_codes.suffix.byte5;
break;
}
}
// Log final messages being transmitted
// Transmit using proper Toshiba protocol timing
auto transmit = this->transmitter_->transmit();
auto *data = transmit.get_data();
// Use existing Toshiba encode function for proper timing
this->encode_(data, message1, RAS_2819T_MESSAGE_LENGTH, 1);
if (this->mode != climate::CLIMATE_MODE_OFF) {
// Send second packet with gap
this->encode_(data, message2, RAS_2819T_MESSAGE_LENGTH, 0);
}
transmit.perform();
// Update all state tracking after successful transmission
this->last_swing_mode_ = this->swing_mode;
this->last_mode_ = this->mode;
this->last_fan_mode_ = this->fan_mode;
this->last_target_temperature_ = this->target_temperature;
}
uint8_t ToshibaClimate::is_valid_rac_pt1411hwru_header_(const uint8_t *message) {
const std::vector<uint8_t> header{RAC_PT1411HWRU_MESSAGE_HEADER0, RAC_PT1411HWRU_CS_HEADER,
RAC_PT1411HWRU_SWING_HEADER};
@@ -464,11 +967,11 @@ bool ToshibaClimate::compare_rac_pt1411hwru_packets_(const uint8_t *message1, co
bool ToshibaClimate::is_valid_rac_pt1411hwru_message_(const uint8_t *message) {
uint8_t checksum = 0;
switch (is_valid_rac_pt1411hwru_header_(message)) {
switch (this->is_valid_rac_pt1411hwru_header_(message)) {
case RAC_PT1411HWRU_MESSAGE_HEADER0:
case RAC_PT1411HWRU_CS_HEADER:
case RAC_PT1411HWRU_SWING_HEADER:
if (is_valid_rac_pt1411hwru_header_(message) && (message[2] == static_cast<uint8_t>(~message[3])) &&
if (this->is_valid_rac_pt1411hwru_header_(message) && (message[2] == static_cast<uint8_t>(~message[3])) &&
(message[4] == static_cast<uint8_t>(~message[5]))) {
return true;
}
@@ -490,7 +993,103 @@ bool ToshibaClimate::is_valid_rac_pt1411hwru_message_(const uint8_t *message) {
return false;
}
bool ToshibaClimate::process_ras_2819t_command_(const remote_base::ToshibaAcData &toshiba_data) {
// Check for power-off command (single packet)
if (toshiba_data.rc_code_2 == 0 && toshiba_data.rc_code_1 == RAS_2819T_POWER_OFF_COMMAND) {
this->mode = climate::CLIMATE_MODE_OFF;
ESP_LOGI(TAG, "Mode: OFF");
this->publish_state();
return true;
}
// Check for swing toggle command (single packet)
if (toshiba_data.rc_code_2 == 0 && toshiba_data.rc_code_1 == RAS_2819T_SWING_TOGGLE) {
// Toggle swing mode
if (this->swing_mode == climate::CLIMATE_SWING_VERTICAL) {
this->swing_mode = climate::CLIMATE_SWING_OFF;
ESP_LOGI(TAG, "Swing: OFF");
} else {
this->swing_mode = climate::CLIMATE_SWING_VERTICAL;
ESP_LOGI(TAG, "Swing: VERTICAL");
}
this->publish_state();
return true;
}
// Handle regular two-packet commands (mode/temperature/fan changes)
if (toshiba_data.rc_code_2 != 0) {
// Convert to byte array for easier processing
uint8_t message1[6], message2[6];
for (uint8_t i = 0; i < 6; i++) {
message1[i] = (toshiba_data.rc_code_1 >> (40 - i * 8)) & 0xFF;
message2[i] = (toshiba_data.rc_code_2 >> (40 - i * 8)) & 0xFF;
}
// Decode the protocol using message1 (rc_code_1)
uint8_t temp_code = message1[4];
// Decode mode - check bytes 2-3 pattern and temperature code
if ((message1[2] == 0x7B) && (message1[3] == 0x84)) {
// OFF mode has specific pattern
this->mode = climate::CLIMATE_MODE_OFF;
ESP_LOGI(TAG, "Mode: OFF");
} else if ((message1[2] == 0x1F) && (message1[3] == 0xE0)) {
// 0x1FE0 pattern is used for AUTO, DRY, and low-temp COOL
if ((temp_code & 0x0F) == 0x08) {
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
ESP_LOGI(TAG, "Mode: AUTO");
} else if ((temp_code & 0x0F) == 0x04) {
this->mode = climate::CLIMATE_MODE_DRY;
ESP_LOGI(TAG, "Mode: DRY");
} else {
this->mode = climate::CLIMATE_MODE_COOL;
ESP_LOGI(TAG, "Mode: COOL (low temp)");
}
} else {
// Variable fan speed patterns - decode by temperature code
if ((temp_code & 0x0F) == 0x0C) {
this->mode = climate::CLIMATE_MODE_HEAT;
ESP_LOGI(TAG, "Mode: HEAT");
} else if (message1[5] == 0x1B) {
this->mode = climate::CLIMATE_MODE_FAN_ONLY;
ESP_LOGI(TAG, "Mode: FAN_ONLY");
} else {
this->mode = climate::CLIMATE_MODE_COOL;
ESP_LOGI(TAG, "Mode: COOL");
}
}
// Decode fan speed from rc_code_1
uint16_t fan_code = (message1[2] << 8) | message1[3];
this->fan_mode = decode_ras_2819t_fan_mode(fan_code);
// Decode temperature
if (this->mode != climate::CLIMATE_MODE_OFF && this->mode != climate::CLIMATE_MODE_FAN_ONLY) {
this->target_temperature = decode_ras_2819t_temperature(temp_code);
}
this->publish_state();
return true;
} else {
ESP_LOGD(TAG, "Unknown single-packet RAS-2819T command: 0x%" PRIX64, toshiba_data.rc_code_1);
return false;
}
}
bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) {
// Try modern ToshibaAcProtocol decoder first (handles RAS-2819T and potentially others)
remote_base::ToshibaAcProtocol toshiba_protocol;
auto decode_result = toshiba_protocol.decode(data);
if (decode_result.has_value()) {
auto toshiba_data = decode_result.value();
// Validate and process RAS-2819T commands
if (is_valid_ras_2819t_command(toshiba_data.rc_code_1, toshiba_data.rc_code_2)) {
return this->process_ras_2819t_command_(toshiba_data);
}
}
// Fall back to generic processing for older protocols
uint8_t message[18] = {0};
uint8_t message_length = TOSHIBA_HEADER_LENGTH, temperature_code = 0;
@@ -499,11 +1098,11 @@ bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) {
return false;
}
// Read incoming bits into buffer
if (!decode_(&data, message, message_length)) {
if (!this->decode_(&data, message, message_length)) {
return false;
}
// Determine incoming message protocol version and/or length
if (is_valid_rac_pt1411hwru_header_(message)) {
if (this->is_valid_rac_pt1411hwru_header_(message)) {
// We already received four bytes
message_length = RAC_PT1411HWRU_MESSAGE_LENGTH - 4;
} else if ((message[0] ^ message[1] ^ message[2]) != message[3]) {
@@ -514,11 +1113,11 @@ bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) {
message_length = message[2] + 2;
}
// Decode the remaining bytes
if (!decode_(&data, &message[4], message_length)) {
if (!this->decode_(&data, &message[4], message_length)) {
return false;
}
// If this is a RAC-PT1411HWRU message, we expect the first packet a second time and also possibly a third packet
if (is_valid_rac_pt1411hwru_header_(message)) {
if (this->is_valid_rac_pt1411hwru_header_(message)) {
// There is always a space between packets
if (!data.expect_item(TOSHIBA_BIT_MARK, TOSHIBA_GAP_SPACE)) {
return false;
@@ -527,7 +1126,7 @@ bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) {
if (!data.expect_item(TOSHIBA_HEADER_MARK, TOSHIBA_HEADER_SPACE)) {
return false;
}
if (!decode_(&data, &message[6], RAC_PT1411HWRU_MESSAGE_LENGTH)) {
if (!this->decode_(&data, &message[6], RAC_PT1411HWRU_MESSAGE_LENGTH)) {
return false;
}
// If this is a RAC-PT1411HWRU message, there may also be a third packet.
@@ -535,25 +1134,25 @@ bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) {
if (data.expect_item(TOSHIBA_BIT_MARK, TOSHIBA_GAP_SPACE)) {
// Validate header 3
data.expect_item(TOSHIBA_HEADER_MARK, TOSHIBA_HEADER_SPACE);
if (decode_(&data, &message[12], RAC_PT1411HWRU_MESSAGE_LENGTH)) {
if (!is_valid_rac_pt1411hwru_message_(&message[12])) {
if (this->decode_(&data, &message[12], RAC_PT1411HWRU_MESSAGE_LENGTH)) {
if (!this->is_valid_rac_pt1411hwru_message_(&message[12])) {
// If a third packet was received but the checksum is not valid, fail
return false;
}
}
}
if (!compare_rac_pt1411hwru_packets_(&message[0], &message[6])) {
if (!this->compare_rac_pt1411hwru_packets_(&message[0], &message[6])) {
// If the first two packets don't match each other, fail
return false;
}
if (!is_valid_rac_pt1411hwru_message_(&message[0])) {
if (!this->is_valid_rac_pt1411hwru_message_(&message[0])) {
// If the first packet isn't valid, fail
return false;
}
}
// Header has been verified, now determine protocol version and set the climate component properties
switch (is_valid_rac_pt1411hwru_header_(message)) {
switch (this->is_valid_rac_pt1411hwru_header_(message)) {
// Power, temperature, mode, fan speed
case RAC_PT1411HWRU_MESSAGE_HEADER0:
// Get the mode
@@ -608,7 +1207,7 @@ bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) {
break;
}
// Get the target temperature
if (is_valid_rac_pt1411hwru_message_(&message[12])) {
if (this->is_valid_rac_pt1411hwru_message_(&message[12])) {
temperature_code =
(message[4] >> 4) | (message[14] & RAC_PT1411HWRU_FLAG_FRAC) | (message[15] & RAC_PT1411HWRU_FLAG_NEG);
if (message[15] & RAC_PT1411HWRU_FLAG_FAH) {

View File

@@ -1,6 +1,7 @@
#pragma once
#include "esphome/components/climate_ir/climate_ir.h"
#include "esphome/components/remote_base/toshiba_ac_protocol.h"
namespace esphome {
namespace toshiba {
@@ -10,6 +11,7 @@ enum Model {
MODEL_GENERIC = 0, // Temperature range is from 17 to 30
MODEL_RAC_PT1411HWRU_C = 1, // Temperature range is from 16 to 30
MODEL_RAC_PT1411HWRU_F = 2, // Temperature range is from 16 to 30
MODEL_RAS_2819T = 3, // RAS-2819T protocol variant, temperature range 18 to 30
};
// Supported temperature ranges
@@ -19,6 +21,8 @@ const float TOSHIBA_RAC_PT1411HWRU_TEMP_C_MIN = 16.0;
const float TOSHIBA_RAC_PT1411HWRU_TEMP_C_MAX = 30.0;
const float TOSHIBA_RAC_PT1411HWRU_TEMP_F_MIN = 60.0;
const float TOSHIBA_RAC_PT1411HWRU_TEMP_F_MAX = 86.0;
const float TOSHIBA_RAS_2819T_TEMP_C_MIN = 18.0;
const float TOSHIBA_RAS_2819T_TEMP_C_MAX = 30.0;
class ToshibaClimate : public climate_ir::ClimateIR {
public:
@@ -35,6 +39,9 @@ class ToshibaClimate : public climate_ir::ClimateIR {
void transmit_generic_();
void transmit_rac_pt1411hwru_();
void transmit_rac_pt1411hwru_temp_(bool cs_state = true, bool cs_send_update = true);
void transmit_ras_2819t_();
// Process RAS-2819T IR command data
bool process_ras_2819t_command_(const remote_base::ToshibaAcData &toshiba_data);
// Returns the header if valid, else returns zero
uint8_t is_valid_rac_pt1411hwru_header_(const uint8_t *message);
// Returns true if message is a valid RAC-PT1411HWRU IR message, regardless if first or second packet
@@ -43,11 +50,26 @@ class ToshibaClimate : public climate_ir::ClimateIR {
bool compare_rac_pt1411hwru_packets_(const uint8_t *message1, const uint8_t *message2);
bool on_receive(remote_base::RemoteReceiveData data) override;
private:
// RAS-2819T state tracking for swing mode optimization
climate::ClimateSwingMode last_swing_mode_{climate::CLIMATE_SWING_OFF};
climate::ClimateMode last_mode_{climate::CLIMATE_MODE_OFF};
optional<climate::ClimateFanMode> last_fan_mode_{};
float last_target_temperature_{24.0f};
float temperature_min_() {
return (this->model_ == MODEL_GENERIC) ? TOSHIBA_GENERIC_TEMP_C_MIN : TOSHIBA_RAC_PT1411HWRU_TEMP_C_MIN;
if (this->model_ == MODEL_RAC_PT1411HWRU_C || this->model_ == MODEL_RAC_PT1411HWRU_F)
return TOSHIBA_RAC_PT1411HWRU_TEMP_C_MIN;
if (this->model_ == MODEL_RAS_2819T)
return TOSHIBA_RAS_2819T_TEMP_C_MIN;
return TOSHIBA_GENERIC_TEMP_C_MIN; // Default to GENERIC for unknown models
}
float temperature_max_() {
return (this->model_ == MODEL_GENERIC) ? TOSHIBA_GENERIC_TEMP_C_MAX : TOSHIBA_RAC_PT1411HWRU_TEMP_C_MAX;
if (this->model_ == MODEL_RAC_PT1411HWRU_C || this->model_ == MODEL_RAC_PT1411HWRU_F)
return TOSHIBA_RAC_PT1411HWRU_TEMP_C_MAX;
if (this->model_ == MODEL_RAS_2819T)
return TOSHIBA_RAS_2819T_TEMP_C_MAX;
return TOSHIBA_GENERIC_TEMP_C_MAX; // Default to GENERIC for unknown models
}
std::set<climate::ClimateSwingMode> toshiba_swing_modes_() {
return (this->model_ == MODEL_GENERIC)

View File

@@ -1325,7 +1325,7 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf
root["max_temp"] = value_accuracy_to_string(traits.get_visual_max_temperature(), target_accuracy);
root["min_temp"] = value_accuracy_to_string(traits.get_visual_min_temperature(), target_accuracy);
root["step"] = traits.get_visual_target_temperature_step();
if (traits.get_supports_action()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) {
root["action"] = PSTR_LOCAL(climate_action_to_string(obj->action));
root["state"] = root["action"];
has_state = true;
@@ -1345,14 +1345,15 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf
if (traits.get_supports_swing_modes()) {
root["swing_mode"] = PSTR_LOCAL(climate_swing_mode_to_string(obj->swing_mode));
}
if (traits.get_supports_current_temperature()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) {
if (!std::isnan(obj->current_temperature)) {
root["current_temperature"] = value_accuracy_to_string(obj->current_temperature, current_accuracy);
} else {
root["current_temperature"] = "NA";
}
}
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)) {
root["target_temperature_low"] = value_accuracy_to_string(obj->target_temperature_low, target_accuracy);
root["target_temperature_high"] = value_accuracy_to_string(obj->target_temperature_high, target_accuracy);
if (!has_state) {

View File

@@ -402,12 +402,13 @@ async def to_code(config):
add_idf_sdkconfig_option("CONFIG_LWIP_DHCPS", False)
# Disable Enterprise WiFi support if no EAP is configured
if CORE.is_esp32 and not has_eap:
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT", False)
if CORE.is_esp32:
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT", has_eap)
cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
cg.add(var.set_power_save_mode(config[CONF_POWER_SAVE_MODE]))
cg.add(var.set_fast_connect(config[CONF_FAST_CONNECT]))
if config[CONF_FAST_CONNECT]:
cg.add_define("USE_WIFI_FAST_CONNECT")
cg.add(var.set_passive_scan(config[CONF_PASSIVE_SCAN]))
if CONF_OUTPUT_POWER in config:
cg.add(var.set_output_power(config[CONF_OUTPUT_POWER]))

View File

@@ -84,9 +84,9 @@ void WiFiComponent::start() {
uint32_t hash = this->has_sta() ? fnv1_hash(App.get_compilation_time()) : 88491487UL;
this->pref_ = global_preferences->make_preference<wifi::SavedWifiSettings>(hash, true);
if (this->fast_connect_) {
this->fast_connect_pref_ = global_preferences->make_preference<wifi::SavedWifiFastConnectSettings>(hash + 1, false);
}
#ifdef USE_WIFI_FAST_CONNECT
this->fast_connect_pref_ = global_preferences->make_preference<wifi::SavedWifiFastConnectSettings>(hash + 1, false);
#endif
SavedWifiSettings save{};
if (this->pref_.load(&save)) {
@@ -108,16 +108,16 @@ void WiFiComponent::start() {
ESP_LOGV(TAG, "Setting Power Save Option failed");
}
if (this->fast_connect_) {
this->trying_loaded_ap_ = this->load_fast_connect_settings_();
if (!this->trying_loaded_ap_) {
this->ap_index_ = 0;
this->selected_ap_ = this->sta_[this->ap_index_];
}
this->start_connecting(this->selected_ap_, false);
} else {
this->start_scanning();
#ifdef USE_WIFI_FAST_CONNECT
this->trying_loaded_ap_ = this->load_fast_connect_settings_();
if (!this->trying_loaded_ap_) {
this->ap_index_ = 0;
this->selected_ap_ = this->sta_[this->ap_index_];
}
this->start_connecting(this->selected_ap_, false);
#else
this->start_scanning();
#endif
#ifdef USE_WIFI_AP
} else if (this->has_ap()) {
this->setup_ap_config_();
@@ -168,13 +168,20 @@ void WiFiComponent::loop() {
case WIFI_COMPONENT_STATE_COOLDOWN: {
this->status_set_warning(LOG_STR("waiting to reconnect"));
if (millis() - this->action_started_ > 5000) {
if (this->fast_connect_ || this->retry_hidden_) {
#ifdef USE_WIFI_FAST_CONNECT
// NOTE: This check may not make sense here as it could interfere with AP cycling
if (!this->selected_ap_.get_bssid().has_value())
this->selected_ap_ = this->sta_[0];
this->start_connecting(this->selected_ap_, false);
#else
if (this->retry_hidden_) {
if (!this->selected_ap_.get_bssid().has_value())
this->selected_ap_ = this->sta_[0];
this->start_connecting(this->selected_ap_, false);
} else {
this->start_scanning();
}
#endif
}
break;
}
@@ -244,7 +251,6 @@ WiFiComponent::WiFiComponent() { global_wifi_component = this; }
bool WiFiComponent::has_ap() const { return this->has_ap_; }
bool WiFiComponent::has_sta() const { return !this->sta_.empty(); }
void WiFiComponent::set_fast_connect(bool fast_connect) { this->fast_connect_ = fast_connect; }
#ifdef USE_WIFI_11KV_SUPPORT
void WiFiComponent::set_btm(bool btm) { this->btm_ = btm; }
void WiFiComponent::set_rrm(bool rrm) { this->rrm_ = rrm; }
@@ -607,10 +613,12 @@ void WiFiComponent::check_scanning_finished() {
for (auto &ap : this->sta_) {
if (res.matches(ap)) {
res.set_matches(true);
if (!this->has_sta_priority(res.get_bssid())) {
this->set_sta_priority(res.get_bssid(), ap.get_priority());
// Cache priority lookup - do single search instead of 2 separate searches
const bssid_t &bssid = res.get_bssid();
if (!this->has_sta_priority(bssid)) {
this->set_sta_priority(bssid, ap.get_priority());
}
res.set_priority(this->get_sta_priority(res.get_bssid()));
res.set_priority(this->get_sta_priority(bssid));
break;
}
}
@@ -629,8 +637,9 @@ void WiFiComponent::check_scanning_finished() {
return;
}
WiFiAP connect_params;
WiFiScanResult scan_res = this->scan_result_[0];
// Build connection params directly into selected_ap_ to avoid extra copy
const WiFiScanResult &scan_res = this->scan_result_[0];
WiFiAP &selected = this->selected_ap_;
for (auto &config : this->sta_) {
// search for matching STA config, at least one will match (from checks before)
if (!scan_res.matches(config)) {
@@ -639,37 +648,38 @@ void WiFiComponent::check_scanning_finished() {
if (config.get_hidden()) {
// selected network is hidden, we use the data from the config
connect_params.set_hidden(true);
connect_params.set_ssid(config.get_ssid());
// don't set BSSID and channel, there might be multiple hidden networks
selected.set_hidden(true);
selected.set_ssid(config.get_ssid());
// Clear channel and BSSID for hidden networks - there might be multiple hidden networks
// but we can't know which one is the correct one. Rely on probe-req with just SSID.
selected.set_channel(0);
selected.set_bssid(optional<bssid_t>{});
} else {
// selected network is visible, we use the data from the scan
// limit the connect params to only connect to exactly this network
// (network selection is done during scan phase).
connect_params.set_hidden(false);
connect_params.set_ssid(scan_res.get_ssid());
connect_params.set_channel(scan_res.get_channel());
connect_params.set_bssid(scan_res.get_bssid());
selected.set_hidden(false);
selected.set_ssid(scan_res.get_ssid());
selected.set_channel(scan_res.get_channel());
selected.set_bssid(scan_res.get_bssid());
}
// copy manual IP (if set)
connect_params.set_manual_ip(config.get_manual_ip());
selected.set_manual_ip(config.get_manual_ip());
#ifdef USE_WIFI_WPA2_EAP
// copy EAP parameters (if set)
connect_params.set_eap(config.get_eap());
selected.set_eap(config.get_eap());
#endif
// copy password (if set)
connect_params.set_password(config.get_password());
selected.set_password(config.get_password());
break;
}
yield();
this->selected_ap_ = connect_params;
this->start_connecting(connect_params, false);
this->start_connecting(this->selected_ap_, false);
}
void WiFiComponent::dump_config() {
@@ -719,9 +729,9 @@ void WiFiComponent::check_connecting_finished() {
this->scan_result_.shrink_to_fit();
}
if (this->fast_connect_) {
this->save_fast_connect_settings_();
}
#ifdef USE_WIFI_FAST_CONNECT
this->save_fast_connect_settings_();
#endif
return;
}
@@ -769,31 +779,31 @@ void WiFiComponent::retry_connect() {
delay(10);
if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_() &&
(this->num_retried_ > 3 || this->error_from_callback_)) {
if (this->fast_connect_) {
if (this->trying_loaded_ap_) {
this->trying_loaded_ap_ = false;
this->ap_index_ = 0; // Retry from the first configured AP
} else if (this->ap_index_ >= this->sta_.size() - 1) {
ESP_LOGW(TAG, "No more APs to try");
this->ap_index_ = 0;
this->restart_adapter();
} else {
// Try next AP
this->ap_index_++;
}
this->num_retried_ = 0;
this->selected_ap_ = this->sta_[this->ap_index_];
#ifdef USE_WIFI_FAST_CONNECT
if (this->trying_loaded_ap_) {
this->trying_loaded_ap_ = false;
this->ap_index_ = 0; // Retry from the first configured AP
} else if (this->ap_index_ >= this->sta_.size() - 1) {
ESP_LOGW(TAG, "No more APs to try");
this->ap_index_ = 0;
this->restart_adapter();
} else {
if (this->num_retried_ > 5) {
// If retry failed for more than 5 times, let's restart STA
this->restart_adapter();
} else {
// Try hidden networks after 3 failed retries
ESP_LOGD(TAG, "Retrying with hidden networks");
this->retry_hidden_ = true;
this->num_retried_++;
}
// Try next AP
this->ap_index_++;
}
this->num_retried_ = 0;
this->selected_ap_ = this->sta_[this->ap_index_];
#else
if (this->num_retried_ > 5) {
// If retry failed for more than 5 times, let's restart STA
this->restart_adapter();
} else {
// Try hidden networks after 3 failed retries
ESP_LOGD(TAG, "Retrying with hidden networks");
this->retry_hidden_ = true;
this->num_retried_++;
}
#endif
} else {
this->num_retried_++;
}
@@ -839,6 +849,7 @@ bool WiFiComponent::is_esp32_improv_active_() {
#endif
}
#ifdef USE_WIFI_FAST_CONNECT
bool WiFiComponent::load_fast_connect_settings_() {
SavedWifiFastConnectSettings fast_connect_save{};
@@ -873,6 +884,7 @@ void WiFiComponent::save_fast_connect_settings_() {
ESP_LOGD(TAG, "Saved fast_connect settings");
}
}
#endif
void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = ssid; }
void WiFiAP::set_bssid(bssid_t bssid) { this->bssid_ = bssid; }
@@ -902,7 +914,7 @@ WiFiScanResult::WiFiScanResult(const bssid_t &bssid, std::string ssid, uint8_t c
rssi_(rssi),
with_auth_(with_auth),
is_hidden_(is_hidden) {}
bool WiFiScanResult::matches(const WiFiAP &config) {
bool WiFiScanResult::matches(const WiFiAP &config) const {
if (config.get_hidden()) {
// User configured a hidden network, only match actually hidden networks
// don't match SSID

View File

@@ -170,7 +170,7 @@ class WiFiScanResult {
public:
WiFiScanResult(const bssid_t &bssid, std::string ssid, uint8_t channel, int8_t rssi, bool with_auth, bool is_hidden);
bool matches(const WiFiAP &config);
bool matches(const WiFiAP &config) const;
bool get_matches() const;
void set_matches(bool matches);
@@ -240,7 +240,6 @@ class WiFiComponent : public Component {
void start_scanning();
void check_scanning_finished();
void start_connecting(const WiFiAP &ap, bool two);
void set_fast_connect(bool fast_connect);
void set_ap_timeout(uint32_t ap_timeout) { ap_timeout_ = ap_timeout; }
void check_connecting_finished();
@@ -364,8 +363,10 @@ class WiFiComponent : public Component {
bool is_captive_portal_active_();
bool is_esp32_improv_active_();
#ifdef USE_WIFI_FAST_CONNECT
bool load_fast_connect_settings_();
void save_fast_connect_settings_();
#endif
#ifdef USE_ESP8266
static void wifi_event_callback(System_Event_t *event);
@@ -399,7 +400,9 @@ class WiFiComponent : public Component {
WiFiAP ap_;
optional<float> output_power_;
ESPPreferenceObject pref_;
#ifdef USE_WIFI_FAST_CONNECT
ESPPreferenceObject fast_connect_pref_;
#endif
// Group all 32-bit integers together
uint32_t action_started_;
@@ -411,14 +414,17 @@ class WiFiComponent : public Component {
WiFiComponentState state_{WIFI_COMPONENT_STATE_OFF};
WiFiPowerSaveMode power_save_{WIFI_POWER_SAVE_NONE};
uint8_t num_retried_{0};
#ifdef USE_WIFI_FAST_CONNECT
uint8_t ap_index_{0};
#endif
#if USE_NETWORK_IPV6
uint8_t num_ipv6_addresses_{0};
#endif /* USE_NETWORK_IPV6 */
// Group all boolean values together
bool fast_connect_{false};
#ifdef USE_WIFI_FAST_CONNECT
bool trying_loaded_ap_{false};
#endif
bool retry_hidden_{false};
bool has_ap_{false};
bool handled_connected_state_{false};

View File

@@ -706,10 +706,10 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) {
this->scan_result_.init(count);
for (bss_info *it = head; it != nullptr; it = STAILQ_NEXT(it, next)) {
WiFiScanResult res({it->bssid[0], it->bssid[1], it->bssid[2], it->bssid[3], it->bssid[4], it->bssid[5]},
std::string(reinterpret_cast<char *>(it->ssid), it->ssid_len), it->channel, it->rssi,
it->authmode != AUTH_OPEN, it->is_hidden != 0);
this->scan_result_.push_back(res);
this->scan_result_.emplace_back(
bssid_t{it->bssid[0], it->bssid[1], it->bssid[2], it->bssid[3], it->bssid[4], it->bssid[5]},
std::string(reinterpret_cast<char *>(it->ssid), it->ssid_len), it->channel, it->rssi, it->authmode != AUTH_OPEN,
it->is_hidden != 0);
}
this->scan_done_ = true;
}

View File

@@ -776,13 +776,12 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
}
uint16_t number = it.number;
std::vector<wifi_ap_record_t> records(number);
err = esp_wifi_scan_get_ap_records(&number, records.data());
auto records = std::make_unique<wifi_ap_record_t[]>(number);
err = esp_wifi_scan_get_ap_records(&number, records.get());
if (err != ESP_OK) {
ESP_LOGW(TAG, "esp_wifi_scan_get_ap_records failed: %s", esp_err_to_name(err));
return;
}
records.resize(number);
scan_result_.init(number);
for (int i = 0; i < number; i++) {
@@ -790,8 +789,8 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
bssid_t bssid;
std::copy(record.bssid, record.bssid + 6, bssid.begin());
std::string ssid(reinterpret_cast<const char *>(record.ssid));
WiFiScanResult result(bssid, ssid, record.primary, record.rssi, record.authmode != WIFI_AUTH_OPEN, ssid.empty());
scan_result_.push_back(result);
scan_result_.emplace_back(bssid, ssid, record.primary, record.rssi, record.authmode != WIFI_AUTH_OPEN,
ssid.empty());
}
} else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_START) {

View File

@@ -419,9 +419,9 @@ void WiFiComponent::wifi_scan_done_callback_() {
uint8_t *bssid = WiFi.BSSID(i);
int32_t channel = WiFi.channel(i);
WiFiScanResult scan({bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]}, std::string(ssid.c_str()),
channel, rssi, authmode != WIFI_AUTH_OPEN, ssid.length() == 0);
this->scan_result_.push_back(scan);
this->scan_result_.emplace_back(bssid_t{bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]},
std::string(ssid.c_str()), channel, rssi, authmode != WIFI_AUTH_OPEN,
ssid.length() == 0);
}
WiFi.scanDelete();
this->scan_done_ = true;

View File

@@ -3,6 +3,7 @@ from esphome.components import i2c, sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
CONF_OVERSAMPLING,
CONF_PRESSURE,
CONF_TEMPERATURE,
DEVICE_CLASS_PRESSURE,
@@ -18,6 +19,17 @@ CODEOWNERS = ["@gcormier"]
CONF_K_VALUE = "k_value"
xgzp68xx_ns = cg.esphome_ns.namespace("xgzp68xx")
XGZP68XXOversampling = xgzp68xx_ns.enum("XGZP68XXOversampling")
OVERSAMPLING_OPTIONS = {
"256X": XGZP68XXOversampling.XGZP68XX_OVERSAMPLING_256X,
"512X": XGZP68XXOversampling.XGZP68XX_OVERSAMPLING_512X,
"1024X": XGZP68XXOversampling.XGZP68XX_OVERSAMPLING_1024X,
"2048X": XGZP68XXOversampling.XGZP68XX_OVERSAMPLING_2048X,
"4096X": XGZP68XXOversampling.XGZP68XX_OVERSAMPLING_4096X,
"8192X": XGZP68XXOversampling.XGZP68XX_OVERSAMPLING_8192X,
"16384X": XGZP68XXOversampling.XGZP68XX_OVERSAMPLING_16384X,
"32768X": XGZP68XXOversampling.XGZP68XX_OVERSAMPLING_32768X,
}
XGZP68XXComponent = xgzp68xx_ns.class_(
"XGZP68XXComponent", cg.PollingComponent, i2c.I2CDevice
)
@@ -31,6 +43,12 @@ CONFIG_SCHEMA = (
accuracy_decimals=1,
device_class=DEVICE_CLASS_PRESSURE,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
cv.Optional(CONF_OVERSAMPLING, default="4096X"): cv.enum(
OVERSAMPLING_OPTIONS, upper=True
),
}
),
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
@@ -58,5 +76,6 @@ async def to_code(config):
if pressure_config := config.get(CONF_PRESSURE):
sens = await sensor.new_sensor(pressure_config)
cg.add(var.set_pressure_sensor(sens))
cg.add(var.set_pressure_oversampling(pressure_config[CONF_OVERSAMPLING]))
cg.add(var.set_k_value(config[CONF_K_VALUE]))

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