mirror of
https://github.com/esphome/esphome.git
synced 2025-11-01 15:41:52 +00:00
Compare commits
2 Commits
fix_clang_
...
jesserockz
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6bf78e2e82 | ||
|
|
817ee70db0 |
@@ -1,5 +1,4 @@
|
||||
[run]
|
||||
omit =
|
||||
esphome/components/*
|
||||
esphome/analyze_memory/*
|
||||
tests/integration/*
|
||||
|
||||
111
.github/workflows/ci-memory-impact-comment.yml
vendored
111
.github/workflows/ci-memory-impact-comment.yml
vendored
@@ -1,111 +0,0 @@
|
||||
---
|
||||
name: Memory Impact Comment (Forks)
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["CI"]
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
memory-impact-comment:
|
||||
name: Post memory impact comment (fork PRs only)
|
||||
runs-on: ubuntu-24.04
|
||||
# Only run for PRs from forks that had successful CI runs
|
||||
if: >
|
||||
github.event.workflow_run.event == 'pull_request' &&
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.head_repository.full_name != github.repository
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- name: Get PR details
|
||||
id: pr
|
||||
run: |
|
||||
# Get PR details by searching for PR with matching head SHA
|
||||
# The workflow_run.pull_requests field is often empty for forks
|
||||
# Use paginate to handle repos with many open PRs
|
||||
head_sha="${{ github.event.workflow_run.head_sha }}"
|
||||
pr_data=$(gh api --paginate "/repos/${{ github.repository }}/pulls" \
|
||||
--jq ".[] | select(.head.sha == \"$head_sha\") | {number: .number, base_ref: .base.ref}" \
|
||||
| head -n 1)
|
||||
|
||||
if [ -z "$pr_data" ]; then
|
||||
echo "No PR found for SHA $head_sha, skipping"
|
||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
pr_number=$(echo "$pr_data" | jq -r '.number')
|
||||
base_ref=$(echo "$pr_data" | jq -r '.base_ref')
|
||||
|
||||
echo "pr_number=$pr_number" >> "$GITHUB_OUTPUT"
|
||||
echo "base_ref=$base_ref" >> "$GITHUB_OUTPUT"
|
||||
echo "Found PR #$pr_number targeting base branch: $base_ref"
|
||||
|
||||
- name: Check out code from base repository
|
||||
if: steps.pr.outputs.skip != 'true'
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
# Always check out from the base repository (esphome/esphome), never from forks
|
||||
# Use the PR's target branch to ensure we run trusted code from the main repo
|
||||
repository: ${{ github.repository }}
|
||||
ref: ${{ steps.pr.outputs.base_ref }}
|
||||
|
||||
- name: Restore Python
|
||||
if: steps.pr.outputs.skip != 'true'
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
python-version: "3.11"
|
||||
cache-key: ${{ hashFiles('.cache-key') }}
|
||||
|
||||
- name: Download memory analysis artifacts
|
||||
if: steps.pr.outputs.skip != 'true'
|
||||
run: |
|
||||
run_id="${{ github.event.workflow_run.id }}"
|
||||
echo "Downloading artifacts from workflow run $run_id"
|
||||
|
||||
mkdir -p memory-analysis
|
||||
|
||||
# Download target analysis artifact
|
||||
if gh run download --name "memory-analysis-target" --dir memory-analysis --repo "${{ github.repository }}" "$run_id"; then
|
||||
echo "Downloaded memory-analysis-target artifact."
|
||||
else
|
||||
echo "No memory-analysis-target artifact found."
|
||||
fi
|
||||
|
||||
# Download PR analysis artifact
|
||||
if gh run download --name "memory-analysis-pr" --dir memory-analysis --repo "${{ github.repository }}" "$run_id"; then
|
||||
echo "Downloaded memory-analysis-pr artifact."
|
||||
else
|
||||
echo "No memory-analysis-pr artifact found."
|
||||
fi
|
||||
|
||||
- name: Check if artifacts exist
|
||||
id: check
|
||||
if: steps.pr.outputs.skip != 'true'
|
||||
run: |
|
||||
if [ -f ./memory-analysis/memory-analysis-target.json ] && [ -f ./memory-analysis/memory-analysis-pr.json ]; then
|
||||
echo "found=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "found=false" >> "$GITHUB_OUTPUT"
|
||||
echo "Memory analysis artifacts not found, skipping comment"
|
||||
fi
|
||||
|
||||
- name: Post or update PR comment
|
||||
if: steps.pr.outputs.skip != 'true' && steps.check.outputs.found == 'true'
|
||||
env:
|
||||
PR_NUMBER: ${{ steps.pr.outputs.pr_number }}
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
# Pass PR number and JSON file paths directly to Python script
|
||||
# Let Python parse the JSON to avoid shell injection risks
|
||||
# The script will validate and sanitize all inputs
|
||||
python script/ci_memory_impact_comment.py \
|
||||
--pr-number "$PR_NUMBER" \
|
||||
--target-json ./memory-analysis/memory-analysis-target.json \
|
||||
--pr-json ./memory-analysis/memory-analysis-pr.json
|
||||
456
.github/workflows/ci.yml
vendored
456
.github/workflows/ci.yml
vendored
@@ -170,14 +170,11 @@ jobs:
|
||||
outputs:
|
||||
integration-tests: ${{ steps.determine.outputs.integration-tests }}
|
||||
clang-tidy: ${{ steps.determine.outputs.clang-tidy }}
|
||||
clang-tidy-mode: ${{ steps.determine.outputs.clang-tidy-mode }}
|
||||
python-linters: ${{ steps.determine.outputs.python-linters }}
|
||||
changed-components: ${{ steps.determine.outputs.changed-components }}
|
||||
changed-components-with-tests: ${{ steps.determine.outputs.changed-components-with-tests }}
|
||||
directly-changed-components-with-tests: ${{ steps.determine.outputs.directly-changed-components-with-tests }}
|
||||
component-test-count: ${{ steps.determine.outputs.component-test-count }}
|
||||
changed-cpp-file-count: ${{ steps.determine.outputs.changed-cpp-file-count }}
|
||||
memory_impact: ${{ steps.determine.outputs.memory-impact }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
@@ -202,14 +199,11 @@ jobs:
|
||||
# Extract individual fields
|
||||
echo "integration-tests=$(echo "$output" | jq -r '.integration_tests')" >> $GITHUB_OUTPUT
|
||||
echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT
|
||||
echo "clang-tidy-mode=$(echo "$output" | jq -r '.clang_tidy_mode')" >> $GITHUB_OUTPUT
|
||||
echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT
|
||||
echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT
|
||||
echo "changed-components-with-tests=$(echo "$output" | jq -c '.changed_components_with_tests')" >> $GITHUB_OUTPUT
|
||||
echo "directly-changed-components-with-tests=$(echo "$output" | jq -c '.directly_changed_components_with_tests')" >> $GITHUB_OUTPUT
|
||||
echo "component-test-count=$(echo "$output" | jq -r '.component_test_count')" >> $GITHUB_OUTPUT
|
||||
echo "changed-cpp-file-count=$(echo "$output" | jq -r '.changed_cpp_file_count')" >> $GITHUB_OUTPUT
|
||||
echo "memory-impact=$(echo "$output" | jq -c '.memory_impact')" >> $GITHUB_OUTPUT
|
||||
|
||||
integration-tests:
|
||||
name: Run integration tests
|
||||
@@ -247,7 +241,7 @@ jobs:
|
||||
. venv/bin/activate
|
||||
pytest -vv --no-cov --tb=native -n auto tests/integration/
|
||||
|
||||
clang-tidy-single:
|
||||
clang-tidy:
|
||||
name: ${{ matrix.name }}
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
@@ -265,6 +259,22 @@ jobs:
|
||||
name: Run script/clang-tidy for ESP8266
|
||||
options: --environment esp8266-arduino-tidy --grep USE_ESP8266
|
||||
pio_cache_key: tidyesp8266
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ESP32 Arduino 1/4
|
||||
options: --environment esp32-arduino-tidy --split-num 4 --split-at 1
|
||||
pio_cache_key: tidyesp32
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ESP32 Arduino 2/4
|
||||
options: --environment esp32-arduino-tidy --split-num 4 --split-at 2
|
||||
pio_cache_key: tidyesp32
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ESP32 Arduino 3/4
|
||||
options: --environment esp32-arduino-tidy --split-num 4 --split-at 3
|
||||
pio_cache_key: tidyesp32
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ESP32 Arduino 4/4
|
||||
options: --environment esp32-arduino-tidy --split-num 4 --split-at 4
|
||||
pio_cache_key: tidyesp32
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ESP32 IDF
|
||||
options: --environment esp32-idf-tidy --grep USE_ESP_IDF
|
||||
@@ -345,166 +355,6 @@ jobs:
|
||||
# yamllint disable-line rule:line-length
|
||||
if: always()
|
||||
|
||||
clang-tidy-nosplit:
|
||||
name: Run script/clang-tidy for ESP32 Arduino
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- common
|
||||
- determine-jobs
|
||||
if: needs.determine-jobs.outputs.clang-tidy-mode == 'nosplit'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
# Need history for HEAD~1 to work for checking changed files
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref != 'refs/heads/dev'
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Register problem matchers
|
||||
run: |
|
||||
echo "::add-matcher::.github/workflows/matchers/gcc.json"
|
||||
echo "::add-matcher::.github/workflows/matchers/clang-tidy.json"
|
||||
|
||||
- name: Check if full clang-tidy scan needed
|
||||
id: check_full_scan
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
if python script/clang_tidy_hash.py --check; then
|
||||
echo "full_scan=true" >> $GITHUB_OUTPUT
|
||||
echo "reason=hash_changed" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "full_scan=false" >> $GITHUB_OUTPUT
|
||||
echo "reason=normal" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Run clang-tidy
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
if [ "${{ steps.check_full_scan.outputs.full_scan }}" = "true" ]; then
|
||||
echo "Running FULL clang-tidy scan (hash changed)"
|
||||
script/clang-tidy --all-headers --fix --environment esp32-arduino-tidy
|
||||
else
|
||||
echo "Running clang-tidy on changed files only"
|
||||
script/clang-tidy --all-headers --fix --changed --environment esp32-arduino-tidy
|
||||
fi
|
||||
env:
|
||||
# Also cache libdeps, store them in a ~/.platformio subfolder
|
||||
PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps
|
||||
|
||||
- name: Suggested changes
|
||||
run: script/ci-suggest-changes
|
||||
if: always()
|
||||
|
||||
clang-tidy-split:
|
||||
name: ${{ matrix.name }}
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- common
|
||||
- determine-jobs
|
||||
if: needs.determine-jobs.outputs.clang-tidy-mode == 'split'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 1
|
||||
matrix:
|
||||
include:
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ESP32 Arduino 1/4
|
||||
options: --environment esp32-arduino-tidy --split-num 4 --split-at 1
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ESP32 Arduino 2/4
|
||||
options: --environment esp32-arduino-tidy --split-num 4 --split-at 2
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ESP32 Arduino 3/4
|
||||
options: --environment esp32-arduino-tidy --split-num 4 --split-at 3
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ESP32 Arduino 4/4
|
||||
options: --environment esp32-arduino-tidy --split-num 4 --split-at 4
|
||||
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
# Need history for HEAD~1 to work for checking changed files
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref != 'refs/heads/dev'
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Register problem matchers
|
||||
run: |
|
||||
echo "::add-matcher::.github/workflows/matchers/gcc.json"
|
||||
echo "::add-matcher::.github/workflows/matchers/clang-tidy.json"
|
||||
|
||||
- name: Check if full clang-tidy scan needed
|
||||
id: check_full_scan
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
if python script/clang_tidy_hash.py --check; then
|
||||
echo "full_scan=true" >> $GITHUB_OUTPUT
|
||||
echo "reason=hash_changed" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "full_scan=false" >> $GITHUB_OUTPUT
|
||||
echo "reason=normal" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Run clang-tidy
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
if [ "${{ steps.check_full_scan.outputs.full_scan }}" = "true" ]; then
|
||||
echo "Running FULL clang-tidy scan (hash changed)"
|
||||
script/clang-tidy --all-headers --fix ${{ matrix.options }}
|
||||
else
|
||||
echo "Running clang-tidy on changed files only"
|
||||
script/clang-tidy --all-headers --fix --changed ${{ matrix.options }}
|
||||
fi
|
||||
env:
|
||||
# Also cache libdeps, store them in a ~/.platformio subfolder
|
||||
PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps
|
||||
|
||||
- name: Suggested changes
|
||||
run: script/ci-suggest-changes
|
||||
if: always()
|
||||
|
||||
test-build-components-splitter:
|
||||
name: Split components for intelligent grouping (40 weighted per batch)
|
||||
runs-on: ubuntu-24.04
|
||||
@@ -671,271 +521,6 @@ 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@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
with:
|
||||
name: memory-analysis-target
|
||||
path: ./memory-analysis
|
||||
continue-on-error: true
|
||||
- name: Download PR analysis JSON
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.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
|
||||
@@ -945,16 +530,11 @@ jobs:
|
||||
- pylint
|
||||
- pytest
|
||||
- integration-tests
|
||||
- clang-tidy-single
|
||||
- clang-tidy-nosplit
|
||||
- clang-tidy-split
|
||||
- clang-tidy
|
||||
- determine-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
|
||||
|
||||
@@ -161,7 +161,6 @@ esphome/components/esp32_rmt_led_strip/* @jesserockz
|
||||
esphome/components/esp8266/* @esphome/core
|
||||
esphome/components/esp_ldo/* @clydebarrow
|
||||
esphome/components/espnow/* @jesserockz
|
||||
esphome/components/espnow/packet_transport/* @EasilyBoredEngineer
|
||||
esphome/components/ethernet_info/* @gtjadsonsantos
|
||||
esphome/components/event/* @nohat
|
||||
esphome/components/exposure_notifications/* @OttoWinter
|
||||
|
||||
@@ -62,40 +62,6 @@ from esphome.util import (
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Special non-component keys that appear in configs
|
||||
_NON_COMPONENT_KEYS = frozenset(
|
||||
{
|
||||
CONF_ESPHOME,
|
||||
"substitutions",
|
||||
"packages",
|
||||
"globals",
|
||||
"external_components",
|
||||
"<<",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def detect_external_components(config: ConfigType) -> set[str]:
|
||||
"""Detect external/custom components in the configuration.
|
||||
|
||||
External components are those that appear in the config but are not
|
||||
part of ESPHome's built-in components and are not special config keys.
|
||||
|
||||
Args:
|
||||
config: The ESPHome configuration dictionary
|
||||
|
||||
Returns:
|
||||
A set of external component names
|
||||
"""
|
||||
from esphome.analyze_memory.helpers import get_esphome_components
|
||||
|
||||
builtin_components = get_esphome_components()
|
||||
return {
|
||||
key
|
||||
for key in config
|
||||
if key not in builtin_components and key not in _NON_COMPONENT_KEYS
|
||||
}
|
||||
|
||||
|
||||
class ArgsProtocol(Protocol):
|
||||
device: list[str] | None
|
||||
@@ -219,9 +185,7 @@ def choose_upload_log_host(
|
||||
else:
|
||||
resolved.append(device)
|
||||
if not resolved:
|
||||
raise EsphomeError(
|
||||
f"All specified devices {defaults} could not be resolved. Is the device connected to the network?"
|
||||
)
|
||||
_LOGGER.error("All specified devices: %s could not be resolved.", defaults)
|
||||
return resolved
|
||||
|
||||
# No devices specified, show interactive chooser
|
||||
@@ -502,9 +466,7 @@ def write_cpp_file() -> int:
|
||||
def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
|
||||
from esphome import platformio_api
|
||||
|
||||
# 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)
|
||||
_LOGGER.info("Compiling app...")
|
||||
rc = platformio_api.run_compile(config, CORE.verbose)
|
||||
if rc != 0:
|
||||
return rc
|
||||
@@ -926,54 +888,6 @@ def command_idedata(args: ArgsProtocol, config: ConfigType) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
|
||||
"""Analyze memory usage by component.
|
||||
|
||||
This command compiles the configuration and performs memory analysis.
|
||||
Compilation is fast if sources haven't changed (just relinking).
|
||||
"""
|
||||
from esphome import platformio_api
|
||||
from esphome.analyze_memory.cli import MemoryAnalyzerCLI
|
||||
|
||||
# Always compile to ensure fresh data (fast if no changes - just relinks)
|
||||
exit_code = write_cpp(config)
|
||||
if exit_code != 0:
|
||||
return exit_code
|
||||
exit_code = compile_program(args, config)
|
||||
if exit_code != 0:
|
||||
return exit_code
|
||||
_LOGGER.info("Successfully compiled program.")
|
||||
|
||||
# Get idedata for analysis
|
||||
idedata = platformio_api.get_idedata(config)
|
||||
if idedata is None:
|
||||
_LOGGER.error("Failed to get IDE data for memory analysis")
|
||||
return 1
|
||||
|
||||
firmware_elf = Path(idedata.firmware_elf_path)
|
||||
|
||||
# Extract external components from config
|
||||
external_components = detect_external_components(config)
|
||||
_LOGGER.debug("Detected external components: %s", external_components)
|
||||
|
||||
# Perform memory analysis
|
||||
_LOGGER.info("Analyzing memory usage...")
|
||||
analyzer = MemoryAnalyzerCLI(
|
||||
str(firmware_elf),
|
||||
idedata.objdump_path,
|
||||
idedata.readelf_path,
|
||||
external_components,
|
||||
)
|
||||
analyzer.analyze()
|
||||
|
||||
# Generate and display report
|
||||
report = analyzer.generate_report()
|
||||
print()
|
||||
print(report)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def command_rename(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
new_name = args.name
|
||||
for c in new_name:
|
||||
@@ -1089,7 +1003,6 @@ POST_CONFIG_ACTIONS = {
|
||||
"idedata": command_idedata,
|
||||
"rename": command_rename,
|
||||
"discover": command_discover,
|
||||
"analyze-memory": command_analyze_memory,
|
||||
}
|
||||
|
||||
SIMPLE_CONFIG_ACTIONS = [
|
||||
@@ -1375,14 +1288,6 @@ def parse_args(argv):
|
||||
)
|
||||
parser_rename.add_argument("name", help="The new name for the device.", type=str)
|
||||
|
||||
parser_analyze_memory = subparsers.add_parser(
|
||||
"analyze-memory",
|
||||
help="Analyze memory usage by component.",
|
||||
)
|
||||
parser_analyze_memory.add_argument(
|
||||
"configuration", help="Your YAML configuration file(s).", nargs="+"
|
||||
)
|
||||
|
||||
# Keep backward compatibility with the old command line format of
|
||||
# esphome <config> <command>.
|
||||
#
|
||||
|
||||
@@ -1,502 +0,0 @@
|
||||
"""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()
|
||||
@@ -1,6 +0,0 @@
|
||||
"""Main entry point for running the memory analyzer as a module."""
|
||||
|
||||
from .cli import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,408 +0,0 @@
|
||||
"""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()
|
||||
@@ -1,903 +0,0 @@
|
||||
"""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::"],
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
"""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
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "absolute_humidity.h"
|
||||
// test
|
||||
|
||||
namespace esphome {
|
||||
namespace absolute_humidity {
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ enum SaturationVaporPressureEquation {
|
||||
};
|
||||
|
||||
/// This class implements calculation of absolute humidity from temperature and relative humidity.
|
||||
// Test change for clang-tidy split logic
|
||||
class AbsoluteHumidityComponent : public sensor::Sensor, public Component {
|
||||
public:
|
||||
AbsoluteHumidityComponent() = default;
|
||||
|
||||
@@ -28,7 +28,7 @@ class Anova : public climate::Climate, public esphome::ble_client::BLEClientNode
|
||||
void dump_config() override;
|
||||
climate::ClimateTraits traits() override {
|
||||
auto traits = climate::ClimateTraits();
|
||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
|
||||
traits.set_supports_current_temperature(true);
|
||||
traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::ClimateMode::CLIMATE_MODE_HEAT});
|
||||
traits.set_visual_min_temperature(25.0);
|
||||
traits.set_visual_max_temperature(100.0);
|
||||
|
||||
@@ -155,17 +155,6 @@ def _validate_api_config(config: ConfigType) -> ConfigType:
|
||||
return config
|
||||
|
||||
|
||||
def _consume_api_sockets(config: ConfigType) -> ConfigType:
|
||||
"""Register socket needs for API component."""
|
||||
from esphome.components import socket
|
||||
|
||||
# API needs 1 listening socket + typically 3 concurrent client connections
|
||||
# (not max_connections, which is the upper limit rarely reached)
|
||||
sockets_needed = 1 + 3
|
||||
socket.consume_sockets(sockets_needed, "api")(config)
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
@@ -233,7 +222,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
cv.rename_key(CONF_SERVICES, CONF_ACTIONS),
|
||||
_validate_api_config,
|
||||
_consume_api_sockets,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -506,7 +506,7 @@ message ListEntitiesLightResponse {
|
||||
string name = 3;
|
||||
reserved 4; // Deprecated: was string unique_id
|
||||
|
||||
repeated ColorMode supported_color_modes = 12 [(container_pointer_no_template) = "light::ColorModeMask"];
|
||||
repeated ColorMode supported_color_modes = 12 [(container_pointer) = "std::set<light::ColorMode>"];
|
||||
// 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];
|
||||
|
||||
@@ -453,6 +453,7 @@ 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();
|
||||
@@ -476,8 +477,7 @@ 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();
|
||||
// Pass pointer to ColorModeMask so the iterator can encode actual ColorMode enum values
|
||||
msg.supported_color_modes = &traits.get_supported_color_modes();
|
||||
msg.supported_color_modes = &traits.get_supported_color_modes_for_api_();
|
||||
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();
|
||||
@@ -1082,8 +1082,13 @@ 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) {
|
||||
homeassistant::global_homeassistant_time->set_timezone(reinterpret_cast<const char *>(value.timezone),
|
||||
value.timezone_len);
|
||||
const std::string ¤t_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));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -70,14 +70,4 @@ 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;
|
||||
}
|
||||
|
||||
@@ -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 light::ColorModeMask *supported_color_modes{};
|
||||
const std::set<light::ColorMode> *supported_color_modes{};
|
||||
float min_mireds{0.0f};
|
||||
float max_mireds{0.0f};
|
||||
std::vector<std::string> effects{};
|
||||
|
||||
@@ -6,9 +6,6 @@ namespace bang_bang {
|
||||
|
||||
static const char *const TAG = "bang_bang.climate";
|
||||
|
||||
BangBangClimate::BangBangClimate()
|
||||
: idle_trigger_(new Trigger<>()), cool_trigger_(new Trigger<>()), heat_trigger_(new Trigger<>()) {}
|
||||
|
||||
void BangBangClimate::setup() {
|
||||
this->sensor_->add_on_state_callback([this](float state) {
|
||||
this->current_temperature = state;
|
||||
@@ -34,63 +31,53 @@ void BangBangClimate::setup() {
|
||||
restore->to_call(this).perform();
|
||||
} else {
|
||||
// restore from defaults, change_away handles those for us
|
||||
if (this->supports_cool_ && this->supports_heat_) {
|
||||
if (supports_cool_ && supports_heat_) {
|
||||
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
|
||||
} else if (this->supports_cool_) {
|
||||
} else if (supports_cool_) {
|
||||
this->mode = climate::CLIMATE_MODE_COOL;
|
||||
} else if (this->supports_heat_) {
|
||||
} else if (supports_heat_) {
|
||||
this->mode = climate::CLIMATE_MODE_HEAT;
|
||||
}
|
||||
this->change_away_(false);
|
||||
}
|
||||
}
|
||||
|
||||
void BangBangClimate::control(const climate::ClimateCall &call) {
|
||||
if (call.get_mode().has_value()) {
|
||||
if (call.get_mode().has_value())
|
||||
this->mode = *call.get_mode();
|
||||
}
|
||||
if (call.get_target_temperature_low().has_value()) {
|
||||
if (call.get_target_temperature_low().has_value())
|
||||
this->target_temperature_low = *call.get_target_temperature_low();
|
||||
}
|
||||
if (call.get_target_temperature_high().has_value()) {
|
||||
if (call.get_target_temperature_high().has_value())
|
||||
this->target_temperature_high = *call.get_target_temperature_high();
|
||||
}
|
||||
if (call.get_preset().has_value()) {
|
||||
if (call.get_preset().has_value())
|
||||
this->change_away_(*call.get_preset() == climate::CLIMATE_PRESET_AWAY);
|
||||
}
|
||||
|
||||
this->compute_state_();
|
||||
this->publish_state();
|
||||
}
|
||||
|
||||
climate::ClimateTraits BangBangClimate::traits() {
|
||||
auto traits = climate::ClimateTraits();
|
||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE |
|
||||
climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE | climate::CLIMATE_SUPPORTS_ACTION);
|
||||
if (this->humidity_sensor_ != nullptr) {
|
||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY);
|
||||
}
|
||||
traits.set_supports_current_temperature(true);
|
||||
if (this->humidity_sensor_ != nullptr)
|
||||
traits.set_supports_current_humidity(true);
|
||||
traits.set_supported_modes({
|
||||
climate::CLIMATE_MODE_OFF,
|
||||
});
|
||||
if (this->supports_cool_) {
|
||||
if (supports_cool_)
|
||||
traits.add_supported_mode(climate::CLIMATE_MODE_COOL);
|
||||
}
|
||||
if (this->supports_heat_) {
|
||||
if (supports_heat_)
|
||||
traits.add_supported_mode(climate::CLIMATE_MODE_HEAT);
|
||||
}
|
||||
if (this->supports_cool_ && this->supports_heat_) {
|
||||
if (supports_cool_ && supports_heat_)
|
||||
traits.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL);
|
||||
}
|
||||
if (this->supports_away_) {
|
||||
traits.set_supports_two_point_target_temperature(true);
|
||||
if (supports_away_) {
|
||||
traits.set_supported_presets({
|
||||
climate::CLIMATE_PRESET_HOME,
|
||||
climate::CLIMATE_PRESET_AWAY,
|
||||
});
|
||||
}
|
||||
traits.set_supports_action(true);
|
||||
return traits;
|
||||
}
|
||||
|
||||
void BangBangClimate::compute_state_() {
|
||||
if (this->mode == climate::CLIMATE_MODE_OFF) {
|
||||
this->switch_to_action_(climate::CLIMATE_ACTION_OFF);
|
||||
@@ -135,7 +122,6 @@ void BangBangClimate::compute_state_() {
|
||||
|
||||
this->switch_to_action_(target_action);
|
||||
}
|
||||
|
||||
void BangBangClimate::switch_to_action_(climate::ClimateAction action) {
|
||||
if (action == this->action) {
|
||||
// already in target mode
|
||||
@@ -180,7 +166,6 @@ void BangBangClimate::switch_to_action_(climate::ClimateAction action) {
|
||||
this->prev_trigger_ = trig;
|
||||
this->publish_state();
|
||||
}
|
||||
|
||||
void BangBangClimate::change_away_(bool away) {
|
||||
if (!away) {
|
||||
this->target_temperature_low = this->normal_config_.default_temperature_low;
|
||||
@@ -191,26 +176,22 @@ void BangBangClimate::change_away_(bool away) {
|
||||
}
|
||||
this->preset = away ? climate::CLIMATE_PRESET_AWAY : climate::CLIMATE_PRESET_HOME;
|
||||
}
|
||||
|
||||
void BangBangClimate::set_normal_config(const BangBangClimateTargetTempConfig &normal_config) {
|
||||
this->normal_config_ = normal_config;
|
||||
}
|
||||
|
||||
void BangBangClimate::set_away_config(const BangBangClimateTargetTempConfig &away_config) {
|
||||
this->supports_away_ = true;
|
||||
this->away_config_ = away_config;
|
||||
}
|
||||
|
||||
BangBangClimate::BangBangClimate()
|
||||
: idle_trigger_(new Trigger<>()), cool_trigger_(new Trigger<>()), heat_trigger_(new Trigger<>()) {}
|
||||
void BangBangClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; }
|
||||
void BangBangClimate::set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
|
||||
|
||||
Trigger<> *BangBangClimate::get_idle_trigger() const { return this->idle_trigger_; }
|
||||
Trigger<> *BangBangClimate::get_cool_trigger() const { return this->cool_trigger_; }
|
||||
Trigger<> *BangBangClimate::get_heat_trigger() const { return this->heat_trigger_; }
|
||||
|
||||
void BangBangClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
|
||||
Trigger<> *BangBangClimate::get_heat_trigger() const { return this->heat_trigger_; }
|
||||
void BangBangClimate::set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; }
|
||||
|
||||
void BangBangClimate::dump_config() {
|
||||
LOG_CLIMATE("", "Bang Bang Climate", this);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
|
||||
@@ -25,15 +25,14 @@ class BangBangClimate : public climate::Climate, public Component {
|
||||
|
||||
void set_sensor(sensor::Sensor *sensor);
|
||||
void set_humidity_sensor(sensor::Sensor *humidity_sensor);
|
||||
Trigger<> *get_idle_trigger() const;
|
||||
Trigger<> *get_cool_trigger() const;
|
||||
void set_supports_cool(bool supports_cool);
|
||||
Trigger<> *get_heat_trigger() const;
|
||||
void set_supports_heat(bool supports_heat);
|
||||
void set_normal_config(const BangBangClimateTargetTempConfig &normal_config);
|
||||
void set_away_config(const BangBangClimateTargetTempConfig &away_config);
|
||||
|
||||
Trigger<> *get_idle_trigger() const;
|
||||
Trigger<> *get_cool_trigger() const;
|
||||
Trigger<> *get_heat_trigger() const;
|
||||
|
||||
protected:
|
||||
/// Override control to change settings of the climate device.
|
||||
void control(const climate::ClimateCall &call) override;
|
||||
@@ -57,10 +56,16 @@ class BangBangClimate : public climate::Climate, public Component {
|
||||
*
|
||||
* In idle mode, the controller is assumed to have both heating and cooling disabled.
|
||||
*/
|
||||
Trigger<> *idle_trigger_{nullptr};
|
||||
Trigger<> *idle_trigger_;
|
||||
/** The trigger to call when the controller should switch to cooling mode.
|
||||
*/
|
||||
Trigger<> *cool_trigger_{nullptr};
|
||||
Trigger<> *cool_trigger_;
|
||||
/** Whether the controller supports cooling.
|
||||
*
|
||||
* A false value for this attribute means that the controller has no cooling action
|
||||
* (for example a thermostat, where only heating and not-heating is possible).
|
||||
*/
|
||||
bool supports_cool_{false};
|
||||
/** The trigger to call when the controller should switch to heating mode.
|
||||
*
|
||||
* A null value for this attribute means that the controller has no heating action
|
||||
@@ -68,23 +73,15 @@ class BangBangClimate : public climate::Climate, public Component {
|
||||
* (blinds open) is possible.
|
||||
*/
|
||||
Trigger<> *heat_trigger_{nullptr};
|
||||
bool supports_heat_{false};
|
||||
/** A reference to the trigger that was previously active.
|
||||
*
|
||||
* This is so that the previous trigger can be stopped before enabling a new one.
|
||||
*/
|
||||
Trigger<> *prev_trigger_{nullptr};
|
||||
|
||||
/** Whether the controller supports cooling/heating
|
||||
*
|
||||
* A false value for this attribute means that the controller has no respective action
|
||||
* (for example a thermostat, where only heating and not-heating is possible).
|
||||
*/
|
||||
bool supports_cool_{false};
|
||||
bool supports_heat_{false};
|
||||
|
||||
bool supports_away_{false};
|
||||
|
||||
BangBangClimateTargetTempConfig normal_config_{};
|
||||
bool supports_away_{false};
|
||||
BangBangClimateTargetTempConfig away_config_{};
|
||||
};
|
||||
|
||||
|
||||
@@ -33,7 +33,8 @@ class BedJetClimate : public climate::Climate, public BedJetClient, public Polli
|
||||
|
||||
climate::ClimateTraits traits() override {
|
||||
auto traits = climate::ClimateTraits();
|
||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_ACTION | climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
|
||||
traits.set_supports_action(true);
|
||||
traits.set_supports_current_temperature(true);
|
||||
traits.set_supported_modes({
|
||||
climate::CLIMATE_MODE_OFF,
|
||||
climate::CLIMATE_MODE_HEAT,
|
||||
|
||||
@@ -155,12 +155,16 @@ 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];
|
||||
uint64_t conn_addr = connection->get_address();
|
||||
|
||||
if (conn_addr == address)
|
||||
if (connection->get_address() == address)
|
||||
return connection;
|
||||
}
|
||||
|
||||
if (reserve && conn_addr == 0) {
|
||||
if (!reserve)
|
||||
return nullptr;
|
||||
|
||||
for (uint8_t i = 0; i < this->connection_count_; i++) {
|
||||
auto *connection = this->connections_[i];
|
||||
if (connection->get_address() == 0) {
|
||||
connection->send_service_ = INIT_SENDING_SERVICES;
|
||||
connection->set_address(address);
|
||||
// All connections must start at INIT
|
||||
@@ -171,6 +175,7 @@ BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool rese
|
||||
return connection;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,42 +6,6 @@ namespace climate {
|
||||
|
||||
static const char *const TAG = "climate";
|
||||
|
||||
// Memory-efficient lookup tables
|
||||
struct StringToUint8 {
|
||||
const char *str;
|
||||
const uint8_t value;
|
||||
};
|
||||
|
||||
constexpr StringToUint8 CLIMATE_MODES_BY_STR[] = {
|
||||
{"OFF", CLIMATE_MODE_OFF},
|
||||
{"AUTO", CLIMATE_MODE_AUTO},
|
||||
{"COOL", CLIMATE_MODE_COOL},
|
||||
{"HEAT", CLIMATE_MODE_HEAT},
|
||||
{"FAN_ONLY", CLIMATE_MODE_FAN_ONLY},
|
||||
{"DRY", CLIMATE_MODE_DRY},
|
||||
{"HEAT_COOL", CLIMATE_MODE_HEAT_COOL},
|
||||
};
|
||||
|
||||
constexpr StringToUint8 CLIMATE_FAN_MODES_BY_STR[] = {
|
||||
{"ON", CLIMATE_FAN_ON}, {"OFF", CLIMATE_FAN_OFF}, {"AUTO", CLIMATE_FAN_AUTO},
|
||||
{"LOW", CLIMATE_FAN_LOW}, {"MEDIUM", CLIMATE_FAN_MEDIUM}, {"HIGH", CLIMATE_FAN_HIGH},
|
||||
{"MIDDLE", CLIMATE_FAN_MIDDLE}, {"FOCUS", CLIMATE_FAN_FOCUS}, {"DIFFUSE", CLIMATE_FAN_DIFFUSE},
|
||||
{"QUIET", CLIMATE_FAN_QUIET},
|
||||
};
|
||||
|
||||
constexpr StringToUint8 CLIMATE_PRESETS_BY_STR[] = {
|
||||
{"ECO", CLIMATE_PRESET_ECO}, {"AWAY", CLIMATE_PRESET_AWAY}, {"BOOST", CLIMATE_PRESET_BOOST},
|
||||
{"COMFORT", CLIMATE_PRESET_COMFORT}, {"HOME", CLIMATE_PRESET_HOME}, {"SLEEP", CLIMATE_PRESET_SLEEP},
|
||||
{"ACTIVITY", CLIMATE_PRESET_ACTIVITY}, {"NONE", CLIMATE_PRESET_NONE},
|
||||
};
|
||||
|
||||
constexpr StringToUint8 CLIMATE_SWING_MODES_BY_STR[] = {
|
||||
{"OFF", CLIMATE_SWING_OFF},
|
||||
{"BOTH", CLIMATE_SWING_BOTH},
|
||||
{"VERTICAL", CLIMATE_SWING_VERTICAL},
|
||||
{"HORIZONTAL", CLIMATE_SWING_HORIZONTAL},
|
||||
};
|
||||
|
||||
void ClimateCall::perform() {
|
||||
this->parent_->control_callback_.call(*this);
|
||||
ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str());
|
||||
@@ -86,46 +50,47 @@ void ClimateCall::perform() {
|
||||
}
|
||||
this->parent_->control(*this);
|
||||
}
|
||||
|
||||
void ClimateCall::validate_() {
|
||||
auto traits = this->parent_->get_traits();
|
||||
if (this->mode_.has_value()) {
|
||||
auto mode = *this->mode_;
|
||||
if (!traits.supports_mode(mode)) {
|
||||
ESP_LOGW(TAG, " Mode %s not supported", LOG_STR_ARG(climate_mode_to_string(mode)));
|
||||
ESP_LOGW(TAG, " Mode %s is not supported by this device!", LOG_STR_ARG(climate_mode_to_string(mode)));
|
||||
this->mode_.reset();
|
||||
}
|
||||
}
|
||||
if (this->custom_fan_mode_.has_value()) {
|
||||
auto custom_fan_mode = *this->custom_fan_mode_;
|
||||
if (!traits.supports_custom_fan_mode(custom_fan_mode)) {
|
||||
ESP_LOGW(TAG, " Fan Mode %s not supported", custom_fan_mode.c_str());
|
||||
ESP_LOGW(TAG, " Fan Mode %s is not supported by this device!", custom_fan_mode.c_str());
|
||||
this->custom_fan_mode_.reset();
|
||||
}
|
||||
} else if (this->fan_mode_.has_value()) {
|
||||
auto fan_mode = *this->fan_mode_;
|
||||
if (!traits.supports_fan_mode(fan_mode)) {
|
||||
ESP_LOGW(TAG, " Fan Mode %s not supported", LOG_STR_ARG(climate_fan_mode_to_string(fan_mode)));
|
||||
ESP_LOGW(TAG, " Fan Mode %s is not supported by this device!",
|
||||
LOG_STR_ARG(climate_fan_mode_to_string(fan_mode)));
|
||||
this->fan_mode_.reset();
|
||||
}
|
||||
}
|
||||
if (this->custom_preset_.has_value()) {
|
||||
auto custom_preset = *this->custom_preset_;
|
||||
if (!traits.supports_custom_preset(custom_preset)) {
|
||||
ESP_LOGW(TAG, " Preset %s not supported", custom_preset.c_str());
|
||||
ESP_LOGW(TAG, " Preset %s is not supported by this device!", custom_preset.c_str());
|
||||
this->custom_preset_.reset();
|
||||
}
|
||||
} else if (this->preset_.has_value()) {
|
||||
auto preset = *this->preset_;
|
||||
if (!traits.supports_preset(preset)) {
|
||||
ESP_LOGW(TAG, " Preset %s not supported", LOG_STR_ARG(climate_preset_to_string(preset)));
|
||||
ESP_LOGW(TAG, " Preset %s is not supported by this device!", LOG_STR_ARG(climate_preset_to_string(preset)));
|
||||
this->preset_.reset();
|
||||
}
|
||||
}
|
||||
if (this->swing_mode_.has_value()) {
|
||||
auto swing_mode = *this->swing_mode_;
|
||||
if (!traits.supports_swing_mode(swing_mode)) {
|
||||
ESP_LOGW(TAG, " Swing Mode %s not supported", LOG_STR_ARG(climate_swing_mode_to_string(swing_mode)));
|
||||
ESP_LOGW(TAG, " Swing Mode %s is not supported by this device!",
|
||||
LOG_STR_ARG(climate_swing_mode_to_string(swing_mode)));
|
||||
this->swing_mode_.reset();
|
||||
}
|
||||
}
|
||||
@@ -134,127 +99,159 @@ void ClimateCall::validate_() {
|
||||
if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
|
||||
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
|
||||
ESP_LOGW(TAG, " Cannot set target temperature for climate device "
|
||||
"with two-point target temperature");
|
||||
"with two-point target temperature!");
|
||||
this->target_temperature_.reset();
|
||||
} else if (std::isnan(target)) {
|
||||
ESP_LOGW(TAG, " Target temperature must not be NAN");
|
||||
ESP_LOGW(TAG, " Target temperature must not be NAN!");
|
||||
this->target_temperature_.reset();
|
||||
}
|
||||
}
|
||||
if (this->target_temperature_low_.has_value() || this->target_temperature_high_.has_value()) {
|
||||
if (!traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
|
||||
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
|
||||
ESP_LOGW(TAG, " Cannot set low/high target temperature");
|
||||
ESP_LOGW(TAG, " Cannot set low/high target temperature for this device!");
|
||||
this->target_temperature_low_.reset();
|
||||
this->target_temperature_high_.reset();
|
||||
}
|
||||
}
|
||||
if (this->target_temperature_low_.has_value() && std::isnan(*this->target_temperature_low_)) {
|
||||
ESP_LOGW(TAG, " Target temperature low must not be NAN");
|
||||
ESP_LOGW(TAG, " Target temperature low must not be NAN!");
|
||||
this->target_temperature_low_.reset();
|
||||
}
|
||||
if (this->target_temperature_high_.has_value() && std::isnan(*this->target_temperature_high_)) {
|
||||
ESP_LOGW(TAG, " Target temperature high must not be NAN");
|
||||
ESP_LOGW(TAG, " Target temperature low must not be NAN!");
|
||||
this->target_temperature_high_.reset();
|
||||
}
|
||||
if (this->target_temperature_low_.has_value() && this->target_temperature_high_.has_value()) {
|
||||
float low = *this->target_temperature_low_;
|
||||
float high = *this->target_temperature_high_;
|
||||
if (low > high) {
|
||||
ESP_LOGW(TAG, " Target temperature low %.2f must be less than target temperature high %.2f", low, high);
|
||||
ESP_LOGW(TAG, " Target temperature low %.2f must be smaller than target temperature high %.2f!", low, high);
|
||||
this->target_temperature_low_.reset();
|
||||
this->target_temperature_high_.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ClimateCall &ClimateCall::set_mode(ClimateMode mode) {
|
||||
this->mode_ = mode;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ClimateCall &ClimateCall::set_mode(const std::string &mode) {
|
||||
for (const auto &mode_entry : CLIMATE_MODES_BY_STR) {
|
||||
if (str_equals_case_insensitive(mode, mode_entry.str)) {
|
||||
this->set_mode(static_cast<ClimateMode>(mode_entry.value));
|
||||
return *this;
|
||||
}
|
||||
if (str_equals_case_insensitive(mode, "OFF")) {
|
||||
this->set_mode(CLIMATE_MODE_OFF);
|
||||
} else if (str_equals_case_insensitive(mode, "AUTO")) {
|
||||
this->set_mode(CLIMATE_MODE_AUTO);
|
||||
} else if (str_equals_case_insensitive(mode, "COOL")) {
|
||||
this->set_mode(CLIMATE_MODE_COOL);
|
||||
} else if (str_equals_case_insensitive(mode, "HEAT")) {
|
||||
this->set_mode(CLIMATE_MODE_HEAT);
|
||||
} else if (str_equals_case_insensitive(mode, "FAN_ONLY")) {
|
||||
this->set_mode(CLIMATE_MODE_FAN_ONLY);
|
||||
} else if (str_equals_case_insensitive(mode, "DRY")) {
|
||||
this->set_mode(CLIMATE_MODE_DRY);
|
||||
} else if (str_equals_case_insensitive(mode, "HEAT_COOL")) {
|
||||
this->set_mode(CLIMATE_MODE_HEAT_COOL);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "'%s' - Unrecognized mode %s", this->parent_->get_name().c_str(), mode.c_str());
|
||||
}
|
||||
ESP_LOGW(TAG, "'%s' - Unrecognized mode %s", this->parent_->get_name().c_str(), mode.c_str());
|
||||
return *this;
|
||||
}
|
||||
|
||||
ClimateCall &ClimateCall::set_fan_mode(ClimateFanMode fan_mode) {
|
||||
this->fan_mode_ = fan_mode;
|
||||
this->custom_fan_mode_.reset();
|
||||
return *this;
|
||||
}
|
||||
|
||||
ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) {
|
||||
for (const auto &mode_entry : CLIMATE_FAN_MODES_BY_STR) {
|
||||
if (str_equals_case_insensitive(fan_mode, mode_entry.str)) {
|
||||
this->set_fan_mode(static_cast<ClimateFanMode>(mode_entry.value));
|
||||
return *this;
|
||||
}
|
||||
}
|
||||
if (this->parent_->get_traits().supports_custom_fan_mode(fan_mode)) {
|
||||
this->custom_fan_mode_ = fan_mode;
|
||||
this->fan_mode_.reset();
|
||||
if (str_equals_case_insensitive(fan_mode, "ON")) {
|
||||
this->set_fan_mode(CLIMATE_FAN_ON);
|
||||
} else if (str_equals_case_insensitive(fan_mode, "OFF")) {
|
||||
this->set_fan_mode(CLIMATE_FAN_OFF);
|
||||
} else if (str_equals_case_insensitive(fan_mode, "AUTO")) {
|
||||
this->set_fan_mode(CLIMATE_FAN_AUTO);
|
||||
} else if (str_equals_case_insensitive(fan_mode, "LOW")) {
|
||||
this->set_fan_mode(CLIMATE_FAN_LOW);
|
||||
} else if (str_equals_case_insensitive(fan_mode, "MEDIUM")) {
|
||||
this->set_fan_mode(CLIMATE_FAN_MEDIUM);
|
||||
} else if (str_equals_case_insensitive(fan_mode, "HIGH")) {
|
||||
this->set_fan_mode(CLIMATE_FAN_HIGH);
|
||||
} else if (str_equals_case_insensitive(fan_mode, "MIDDLE")) {
|
||||
this->set_fan_mode(CLIMATE_FAN_MIDDLE);
|
||||
} else if (str_equals_case_insensitive(fan_mode, "FOCUS")) {
|
||||
this->set_fan_mode(CLIMATE_FAN_FOCUS);
|
||||
} else if (str_equals_case_insensitive(fan_mode, "DIFFUSE")) {
|
||||
this->set_fan_mode(CLIMATE_FAN_DIFFUSE);
|
||||
} else if (str_equals_case_insensitive(fan_mode, "QUIET")) {
|
||||
this->set_fan_mode(CLIMATE_FAN_QUIET);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), fan_mode.c_str());
|
||||
if (this->parent_->get_traits().supports_custom_fan_mode(fan_mode)) {
|
||||
this->custom_fan_mode_ = fan_mode;
|
||||
this->fan_mode_.reset();
|
||||
} else {
|
||||
ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), fan_mode.c_str());
|
||||
}
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
ClimateCall &ClimateCall::set_fan_mode(optional<std::string> fan_mode) {
|
||||
if (fan_mode.has_value()) {
|
||||
this->set_fan_mode(fan_mode.value());
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
ClimateCall &ClimateCall::set_preset(ClimatePreset preset) {
|
||||
this->preset_ = preset;
|
||||
this->custom_preset_.reset();
|
||||
return *this;
|
||||
}
|
||||
|
||||
ClimateCall &ClimateCall::set_preset(const std::string &preset) {
|
||||
for (const auto &preset_entry : CLIMATE_PRESETS_BY_STR) {
|
||||
if (str_equals_case_insensitive(preset, preset_entry.str)) {
|
||||
this->set_preset(static_cast<ClimatePreset>(preset_entry.value));
|
||||
return *this;
|
||||
}
|
||||
}
|
||||
if (this->parent_->get_traits().supports_custom_preset(preset)) {
|
||||
this->custom_preset_ = preset;
|
||||
this->preset_.reset();
|
||||
if (str_equals_case_insensitive(preset, "ECO")) {
|
||||
this->set_preset(CLIMATE_PRESET_ECO);
|
||||
} else if (str_equals_case_insensitive(preset, "AWAY")) {
|
||||
this->set_preset(CLIMATE_PRESET_AWAY);
|
||||
} else if (str_equals_case_insensitive(preset, "BOOST")) {
|
||||
this->set_preset(CLIMATE_PRESET_BOOST);
|
||||
} else if (str_equals_case_insensitive(preset, "COMFORT")) {
|
||||
this->set_preset(CLIMATE_PRESET_COMFORT);
|
||||
} else if (str_equals_case_insensitive(preset, "HOME")) {
|
||||
this->set_preset(CLIMATE_PRESET_HOME);
|
||||
} else if (str_equals_case_insensitive(preset, "SLEEP")) {
|
||||
this->set_preset(CLIMATE_PRESET_SLEEP);
|
||||
} else if (str_equals_case_insensitive(preset, "ACTIVITY")) {
|
||||
this->set_preset(CLIMATE_PRESET_ACTIVITY);
|
||||
} else if (str_equals_case_insensitive(preset, "NONE")) {
|
||||
this->set_preset(CLIMATE_PRESET_NONE);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), preset.c_str());
|
||||
if (this->parent_->get_traits().supports_custom_preset(preset)) {
|
||||
this->custom_preset_ = preset;
|
||||
this->preset_.reset();
|
||||
} else {
|
||||
ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), preset.c_str());
|
||||
}
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
ClimateCall &ClimateCall::set_preset(optional<std::string> preset) {
|
||||
if (preset.has_value()) {
|
||||
this->set_preset(preset.value());
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
ClimateCall &ClimateCall::set_swing_mode(ClimateSwingMode swing_mode) {
|
||||
this->swing_mode_ = swing_mode;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ClimateCall &ClimateCall::set_swing_mode(const std::string &swing_mode) {
|
||||
for (const auto &mode_entry : CLIMATE_SWING_MODES_BY_STR) {
|
||||
if (str_equals_case_insensitive(swing_mode, mode_entry.str)) {
|
||||
this->set_swing_mode(static_cast<ClimateSwingMode>(mode_entry.value));
|
||||
return *this;
|
||||
}
|
||||
if (str_equals_case_insensitive(swing_mode, "OFF")) {
|
||||
this->set_swing_mode(CLIMATE_SWING_OFF);
|
||||
} else if (str_equals_case_insensitive(swing_mode, "BOTH")) {
|
||||
this->set_swing_mode(CLIMATE_SWING_BOTH);
|
||||
} else if (str_equals_case_insensitive(swing_mode, "VERTICAL")) {
|
||||
this->set_swing_mode(CLIMATE_SWING_VERTICAL);
|
||||
} else if (str_equals_case_insensitive(swing_mode, "HORIZONTAL")) {
|
||||
this->set_swing_mode(CLIMATE_SWING_HORIZONTAL);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "'%s' - Unrecognized swing mode %s", this->parent_->get_name().c_str(), swing_mode.c_str());
|
||||
}
|
||||
ESP_LOGW(TAG, "'%s' - Unrecognized swing mode %s", this->parent_->get_name().c_str(), swing_mode.c_str());
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -262,71 +259,59 @@ ClimateCall &ClimateCall::set_target_temperature(float target_temperature) {
|
||||
this->target_temperature_ = target_temperature;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ClimateCall &ClimateCall::set_target_temperature_low(float target_temperature_low) {
|
||||
this->target_temperature_low_ = target_temperature_low;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ClimateCall &ClimateCall::set_target_temperature_high(float target_temperature_high) {
|
||||
this->target_temperature_high_ = target_temperature_high;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ClimateCall &ClimateCall::set_target_humidity(float target_humidity) {
|
||||
this->target_humidity_ = target_humidity;
|
||||
return *this;
|
||||
}
|
||||
|
||||
const optional<ClimateMode> &ClimateCall::get_mode() const { return this->mode_; }
|
||||
const optional<float> &ClimateCall::get_target_temperature() const { return this->target_temperature_; }
|
||||
const optional<float> &ClimateCall::get_target_temperature_low() const { return this->target_temperature_low_; }
|
||||
const optional<float> &ClimateCall::get_target_temperature_high() const { return this->target_temperature_high_; }
|
||||
const optional<float> &ClimateCall::get_target_humidity() const { return this->target_humidity_; }
|
||||
|
||||
const optional<ClimateMode> &ClimateCall::get_mode() const { return this->mode_; }
|
||||
const optional<ClimateFanMode> &ClimateCall::get_fan_mode() const { return this->fan_mode_; }
|
||||
const optional<ClimateSwingMode> &ClimateCall::get_swing_mode() const { return this->swing_mode_; }
|
||||
const optional<ClimatePreset> &ClimateCall::get_preset() const { return this->preset_; }
|
||||
const optional<std::string> &ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; }
|
||||
const optional<ClimatePreset> &ClimateCall::get_preset() const { return this->preset_; }
|
||||
const optional<std::string> &ClimateCall::get_custom_preset() const { return this->custom_preset_; }
|
||||
|
||||
const optional<ClimateSwingMode> &ClimateCall::get_swing_mode() const { return this->swing_mode_; }
|
||||
ClimateCall &ClimateCall::set_target_temperature_high(optional<float> target_temperature_high) {
|
||||
this->target_temperature_high_ = target_temperature_high;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ClimateCall &ClimateCall::set_target_temperature_low(optional<float> target_temperature_low) {
|
||||
this->target_temperature_low_ = target_temperature_low;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ClimateCall &ClimateCall::set_target_temperature(optional<float> target_temperature) {
|
||||
this->target_temperature_ = target_temperature;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ClimateCall &ClimateCall::set_target_humidity(optional<float> target_humidity) {
|
||||
this->target_humidity_ = target_humidity;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ClimateCall &ClimateCall::set_mode(optional<ClimateMode> mode) {
|
||||
this->mode_ = mode;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ClimateCall &ClimateCall::set_fan_mode(optional<ClimateFanMode> fan_mode) {
|
||||
this->fan_mode_ = fan_mode;
|
||||
this->custom_fan_mode_.reset();
|
||||
return *this;
|
||||
}
|
||||
|
||||
ClimateCall &ClimateCall::set_preset(optional<ClimatePreset> preset) {
|
||||
this->preset_ = preset;
|
||||
this->custom_preset_.reset();
|
||||
return *this;
|
||||
}
|
||||
|
||||
ClimateCall &ClimateCall::set_swing_mode(optional<ClimateSwingMode> swing_mode) {
|
||||
this->swing_mode_ = swing_mode;
|
||||
return *this;
|
||||
@@ -351,7 +336,6 @@ optional<ClimateDeviceRestoreState> Climate::restore_state_() {
|
||||
return {};
|
||||
return recovered;
|
||||
}
|
||||
|
||||
void Climate::save_state_() {
|
||||
#if (defined(USE_ESP_IDF) || (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0))) && \
|
||||
!defined(CLANG_TIDY)
|
||||
@@ -414,7 +398,6 @@ void Climate::save_state_() {
|
||||
|
||||
this->rtc_.save(&state);
|
||||
}
|
||||
|
||||
void Climate::publish_state() {
|
||||
ESP_LOGD(TAG, "'%s' - Sending state:", this->name_.c_str());
|
||||
auto traits = this->get_traits();
|
||||
@@ -486,20 +469,16 @@ ClimateTraits Climate::get_traits() {
|
||||
void Climate::set_visual_min_temperature_override(float visual_min_temperature_override) {
|
||||
this->visual_min_temperature_override_ = visual_min_temperature_override;
|
||||
}
|
||||
|
||||
void Climate::set_visual_max_temperature_override(float visual_max_temperature_override) {
|
||||
this->visual_max_temperature_override_ = visual_max_temperature_override;
|
||||
}
|
||||
|
||||
void Climate::set_visual_temperature_step_override(float target, float current) {
|
||||
this->visual_target_temperature_step_override_ = target;
|
||||
this->visual_current_temperature_step_override_ = current;
|
||||
}
|
||||
|
||||
void Climate::set_visual_min_humidity_override(float visual_min_humidity_override) {
|
||||
this->visual_min_humidity_override_ = visual_min_humidity_override;
|
||||
}
|
||||
|
||||
void Climate::set_visual_max_humidity_override(float visual_max_humidity_override) {
|
||||
this->visual_max_humidity_override_ = visual_max_humidity_override;
|
||||
}
|
||||
@@ -531,7 +510,6 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) {
|
||||
}
|
||||
return call;
|
||||
}
|
||||
|
||||
void ClimateDeviceRestoreState::apply(Climate *climate) {
|
||||
auto traits = climate->get_traits();
|
||||
climate->mode = this->mode;
|
||||
@@ -601,68 +579,68 @@ void Climate::dump_traits_(const char *tag) {
|
||||
auto traits = this->get_traits();
|
||||
ESP_LOGCONFIG(tag, "ClimateTraits:");
|
||||
ESP_LOGCONFIG(tag,
|
||||
" Visual settings:\n"
|
||||
" - Min temperature: %.1f\n"
|
||||
" - Max temperature: %.1f\n"
|
||||
" - Temperature step:\n"
|
||||
" Target: %.1f",
|
||||
" [x] Visual settings:\n"
|
||||
" - Min temperature: %.1f\n"
|
||||
" - Max temperature: %.1f\n"
|
||||
" - Temperature step:\n"
|
||||
" Target: %.1f",
|
||||
traits.get_visual_min_temperature(), traits.get_visual_max_temperature(),
|
||||
traits.get_visual_target_temperature_step());
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) {
|
||||
ESP_LOGCONFIG(tag, " Current: %.1f", traits.get_visual_current_temperature_step());
|
||||
ESP_LOGCONFIG(tag, " Current: %.1f", traits.get_visual_current_temperature_step());
|
||||
}
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY |
|
||||
climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) {
|
||||
ESP_LOGCONFIG(tag,
|
||||
" - Min humidity: %.0f\n"
|
||||
" - Max humidity: %.0f",
|
||||
" - Min humidity: %.0f\n"
|
||||
" - Max humidity: %.0f",
|
||||
traits.get_visual_min_humidity(), traits.get_visual_max_humidity());
|
||||
}
|
||||
if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
|
||||
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
|
||||
ESP_LOGCONFIG(tag, " Supports two-point target temperature");
|
||||
ESP_LOGCONFIG(tag, " [x] Supports two-point target temperature");
|
||||
}
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) {
|
||||
ESP_LOGCONFIG(tag, " Supports current temperature");
|
||||
ESP_LOGCONFIG(tag, " [x] Supports current temperature");
|
||||
}
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
|
||||
ESP_LOGCONFIG(tag, " Supports target humidity");
|
||||
ESP_LOGCONFIG(tag, " [x] Supports target humidity");
|
||||
}
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) {
|
||||
ESP_LOGCONFIG(tag, " Supports current humidity");
|
||||
ESP_LOGCONFIG(tag, " [x] Supports current humidity");
|
||||
}
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) {
|
||||
ESP_LOGCONFIG(tag, " Supports action");
|
||||
ESP_LOGCONFIG(tag, " [x] Supports action");
|
||||
}
|
||||
if (!traits.get_supported_modes().empty()) {
|
||||
ESP_LOGCONFIG(tag, " Supported modes:");
|
||||
ESP_LOGCONFIG(tag, " [x] Supported modes:");
|
||||
for (ClimateMode m : traits.get_supported_modes())
|
||||
ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_mode_to_string(m)));
|
||||
ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_mode_to_string(m)));
|
||||
}
|
||||
if (!traits.get_supported_fan_modes().empty()) {
|
||||
ESP_LOGCONFIG(tag, " Supported fan modes:");
|
||||
ESP_LOGCONFIG(tag, " [x] Supported fan modes:");
|
||||
for (ClimateFanMode m : traits.get_supported_fan_modes())
|
||||
ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_fan_mode_to_string(m)));
|
||||
ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_fan_mode_to_string(m)));
|
||||
}
|
||||
if (!traits.get_supported_custom_fan_modes().empty()) {
|
||||
ESP_LOGCONFIG(tag, " Supported custom fan modes:");
|
||||
ESP_LOGCONFIG(tag, " [x] Supported custom fan modes:");
|
||||
for (const std::string &s : traits.get_supported_custom_fan_modes())
|
||||
ESP_LOGCONFIG(tag, " - %s", s.c_str());
|
||||
ESP_LOGCONFIG(tag, " - %s", s.c_str());
|
||||
}
|
||||
if (!traits.get_supported_presets().empty()) {
|
||||
ESP_LOGCONFIG(tag, " Supported presets:");
|
||||
ESP_LOGCONFIG(tag, " [x] Supported presets:");
|
||||
for (ClimatePreset p : traits.get_supported_presets())
|
||||
ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_preset_to_string(p)));
|
||||
ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_preset_to_string(p)));
|
||||
}
|
||||
if (!traits.get_supported_custom_presets().empty()) {
|
||||
ESP_LOGCONFIG(tag, " Supported custom presets:");
|
||||
ESP_LOGCONFIG(tag, " [x] Supported custom presets:");
|
||||
for (const std::string &s : traits.get_supported_custom_presets())
|
||||
ESP_LOGCONFIG(tag, " - %s", s.c_str());
|
||||
ESP_LOGCONFIG(tag, " - %s", s.c_str());
|
||||
}
|
||||
if (!traits.get_supported_swing_modes().empty()) {
|
||||
ESP_LOGCONFIG(tag, " Supported swing modes:");
|
||||
ESP_LOGCONFIG(tag, " [x] Supported swing modes:");
|
||||
for (ClimateSwingMode m : traits.get_supported_swing_modes())
|
||||
ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_swing_mode_to_string(m)));
|
||||
ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_swing_mode_to_string(m)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -93,31 +93,30 @@ class ClimateCall {
|
||||
|
||||
void perform();
|
||||
|
||||
const optional<ClimateMode> &get_mode() const;
|
||||
const optional<float> &get_target_temperature() const;
|
||||
const optional<float> &get_target_temperature_low() const;
|
||||
const optional<float> &get_target_temperature_high() const;
|
||||
const optional<float> &get_target_humidity() const;
|
||||
|
||||
const optional<ClimateMode> &get_mode() const;
|
||||
const optional<ClimateFanMode> &get_fan_mode() const;
|
||||
const optional<ClimateSwingMode> &get_swing_mode() const;
|
||||
const optional<ClimatePreset> &get_preset() const;
|
||||
const optional<std::string> &get_custom_fan_mode() const;
|
||||
const optional<ClimatePreset> &get_preset() const;
|
||||
const optional<std::string> &get_custom_preset() const;
|
||||
|
||||
protected:
|
||||
void validate_();
|
||||
|
||||
Climate *const parent_;
|
||||
optional<ClimateMode> mode_;
|
||||
optional<float> target_temperature_;
|
||||
optional<float> target_temperature_low_;
|
||||
optional<float> target_temperature_high_;
|
||||
optional<float> target_humidity_;
|
||||
optional<ClimateMode> mode_;
|
||||
optional<ClimateFanMode> fan_mode_;
|
||||
optional<ClimateSwingMode> swing_mode_;
|
||||
optional<ClimatePreset> preset_;
|
||||
optional<std::string> custom_fan_mode_;
|
||||
optional<ClimatePreset> preset_;
|
||||
optional<std::string> custom_preset_;
|
||||
};
|
||||
|
||||
@@ -170,6 +169,47 @@ class Climate : public EntityBase {
|
||||
public:
|
||||
Climate() {}
|
||||
|
||||
/// The active mode of the climate device.
|
||||
ClimateMode mode{CLIMATE_MODE_OFF};
|
||||
|
||||
/// The active state of the climate device.
|
||||
ClimateAction action{CLIMATE_ACTION_OFF};
|
||||
|
||||
/// The current temperature of the climate device, as reported from the integration.
|
||||
float current_temperature{NAN};
|
||||
|
||||
/// The current humidity of the climate device, as reported from the integration.
|
||||
float current_humidity{NAN};
|
||||
|
||||
union {
|
||||
/// The target temperature of the climate device.
|
||||
float target_temperature;
|
||||
struct {
|
||||
/// The minimum target temperature of the climate device, for climate devices with split target temperature.
|
||||
float target_temperature_low{NAN};
|
||||
/// The maximum target temperature of the climate device, for climate devices with split target temperature.
|
||||
float target_temperature_high{NAN};
|
||||
};
|
||||
};
|
||||
|
||||
/// The target humidity of the climate device.
|
||||
float target_humidity;
|
||||
|
||||
/// The active fan mode of the climate device.
|
||||
optional<ClimateFanMode> fan_mode;
|
||||
|
||||
/// The active swing mode of the climate device.
|
||||
ClimateSwingMode swing_mode;
|
||||
|
||||
/// The active custom fan mode of the climate device.
|
||||
optional<std::string> custom_fan_mode;
|
||||
|
||||
/// The active preset of the climate device.
|
||||
optional<ClimatePreset> preset;
|
||||
|
||||
/// The active custom preset mode of the climate device.
|
||||
optional<std::string> custom_preset;
|
||||
|
||||
/** Add a callback for the climate device state, each time the state of the climate device is updated
|
||||
* (using publish_state), this callback will be called.
|
||||
*
|
||||
@@ -211,47 +251,6 @@ class Climate : public EntityBase {
|
||||
void set_visual_min_humidity_override(float visual_min_humidity_override);
|
||||
void set_visual_max_humidity_override(float visual_max_humidity_override);
|
||||
|
||||
/// The current temperature of the climate device, as reported from the integration.
|
||||
float current_temperature{NAN};
|
||||
|
||||
/// The current humidity of the climate device, as reported from the integration.
|
||||
float current_humidity{NAN};
|
||||
|
||||
union {
|
||||
/// The target temperature of the climate device.
|
||||
float target_temperature;
|
||||
struct {
|
||||
/// The minimum target temperature of the climate device, for climate devices with split target temperature.
|
||||
float target_temperature_low{NAN};
|
||||
/// The maximum target temperature of the climate device, for climate devices with split target temperature.
|
||||
float target_temperature_high{NAN};
|
||||
};
|
||||
};
|
||||
|
||||
/// The target humidity of the climate device.
|
||||
float target_humidity;
|
||||
|
||||
/// The active fan mode of the climate device.
|
||||
optional<ClimateFanMode> fan_mode;
|
||||
|
||||
/// The active preset of the climate device.
|
||||
optional<ClimatePreset> preset;
|
||||
|
||||
/// The active custom fan mode of the climate device.
|
||||
optional<std::string> custom_fan_mode;
|
||||
|
||||
/// The active custom preset mode of the climate device.
|
||||
optional<std::string> custom_preset;
|
||||
|
||||
/// The active mode of the climate device.
|
||||
ClimateMode mode{CLIMATE_MODE_OFF};
|
||||
|
||||
/// The active state of the climate device.
|
||||
ClimateAction action{CLIMATE_ACTION_OFF};
|
||||
|
||||
/// The active swing mode of the climate device.
|
||||
ClimateSwingMode swing_mode{CLIMATE_SWING_OFF};
|
||||
|
||||
protected:
|
||||
friend ClimateCall;
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <set>
|
||||
#include "climate_mode.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "climate_mode.h"
|
||||
#include <set>
|
||||
|
||||
namespace esphome {
|
||||
|
||||
@@ -109,12 +109,44 @@ class ClimateTraits {
|
||||
|
||||
void set_supported_modes(std::set<ClimateMode> modes) { this->supported_modes_ = std::move(modes); }
|
||||
void add_supported_mode(ClimateMode mode) { this->supported_modes_.insert(mode); }
|
||||
ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
|
||||
void set_supports_auto_mode(bool supports_auto_mode) { set_mode_support_(CLIMATE_MODE_AUTO, supports_auto_mode); }
|
||||
ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
|
||||
void set_supports_cool_mode(bool supports_cool_mode) { set_mode_support_(CLIMATE_MODE_COOL, supports_cool_mode); }
|
||||
ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
|
||||
void set_supports_heat_mode(bool supports_heat_mode) { set_mode_support_(CLIMATE_MODE_HEAT, supports_heat_mode); }
|
||||
ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
|
||||
void set_supports_heat_cool_mode(bool supported) { set_mode_support_(CLIMATE_MODE_HEAT_COOL, supported); }
|
||||
ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
|
||||
void set_supports_fan_only_mode(bool supports_fan_only_mode) {
|
||||
set_mode_support_(CLIMATE_MODE_FAN_ONLY, supports_fan_only_mode);
|
||||
}
|
||||
ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
|
||||
void set_supports_dry_mode(bool supports_dry_mode) { set_mode_support_(CLIMATE_MODE_DRY, supports_dry_mode); }
|
||||
bool supports_mode(ClimateMode mode) const { return this->supported_modes_.count(mode); }
|
||||
const std::set<ClimateMode> &get_supported_modes() const { return this->supported_modes_; }
|
||||
|
||||
void set_supported_fan_modes(std::set<ClimateFanMode> modes) { this->supported_fan_modes_ = std::move(modes); }
|
||||
void add_supported_fan_mode(ClimateFanMode mode) { this->supported_fan_modes_.insert(mode); }
|
||||
void add_supported_custom_fan_mode(const std::string &mode) { this->supported_custom_fan_modes_.insert(mode); }
|
||||
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
|
||||
void set_supports_fan_mode_on(bool supported) { set_fan_mode_support_(CLIMATE_FAN_ON, supported); }
|
||||
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
|
||||
void set_supports_fan_mode_off(bool supported) { set_fan_mode_support_(CLIMATE_FAN_OFF, supported); }
|
||||
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
|
||||
void set_supports_fan_mode_auto(bool supported) { set_fan_mode_support_(CLIMATE_FAN_AUTO, supported); }
|
||||
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
|
||||
void set_supports_fan_mode_low(bool supported) { set_fan_mode_support_(CLIMATE_FAN_LOW, supported); }
|
||||
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
|
||||
void set_supports_fan_mode_medium(bool supported) { set_fan_mode_support_(CLIMATE_FAN_MEDIUM, supported); }
|
||||
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
|
||||
void set_supports_fan_mode_high(bool supported) { set_fan_mode_support_(CLIMATE_FAN_HIGH, supported); }
|
||||
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
|
||||
void set_supports_fan_mode_middle(bool supported) { set_fan_mode_support_(CLIMATE_FAN_MIDDLE, supported); }
|
||||
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
|
||||
void set_supports_fan_mode_focus(bool supported) { set_fan_mode_support_(CLIMATE_FAN_FOCUS, supported); }
|
||||
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
|
||||
void set_supports_fan_mode_diffuse(bool supported) { set_fan_mode_support_(CLIMATE_FAN_DIFFUSE, supported); }
|
||||
bool supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.count(fan_mode); }
|
||||
bool get_supports_fan_modes() const {
|
||||
return !this->supported_fan_modes_.empty() || !this->supported_custom_fan_modes_.empty();
|
||||
@@ -146,6 +178,16 @@ 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_; }
|
||||
|
||||
@@ -8,10 +8,7 @@ static const char *const TAG = "climate_ir";
|
||||
|
||||
climate::ClimateTraits ClimateIR::traits() {
|
||||
auto traits = climate::ClimateTraits();
|
||||
if (this->sensor_ != nullptr) {
|
||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
|
||||
}
|
||||
|
||||
traits.set_supports_current_temperature(this->sensor_ != nullptr);
|
||||
traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_HEAT_COOL});
|
||||
if (this->supports_cool_)
|
||||
traits.add_supported_mode(climate::CLIMATE_MODE_COOL);
|
||||
@@ -22,6 +19,7 @@ climate::ClimateTraits ClimateIR::traits() {
|
||||
if (this->supports_fan_only_)
|
||||
traits.add_supported_mode(climate::CLIMATE_MODE_FAN_ONLY);
|
||||
|
||||
traits.set_supports_two_point_target_temperature(false);
|
||||
traits.set_visual_min_temperature(this->minimum_temperature_);
|
||||
traits.set_visual_max_temperature(this->maximum_temperature_);
|
||||
traits.set_visual_temperature_step(this->temperature_step_);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "cover.h"
|
||||
#include <strings.h>
|
||||
#include "esphome/core/log.h"
|
||||
#include <strings.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace cover {
|
||||
@@ -144,7 +144,21 @@ CoverCall &CoverCall::set_stop(bool stop) {
|
||||
bool CoverCall::get_stop() const { return this->stop_; }
|
||||
|
||||
CoverCall Cover::make_call() { return {this}; }
|
||||
|
||||
void Cover::open() {
|
||||
auto call = this->make_call();
|
||||
call.set_command_open();
|
||||
call.perform();
|
||||
}
|
||||
void Cover::close() {
|
||||
auto call = this->make_call();
|
||||
call.set_command_close();
|
||||
call.perform();
|
||||
}
|
||||
void Cover::stop() {
|
||||
auto call = this->make_call();
|
||||
call.set_command_stop();
|
||||
call.perform();
|
||||
}
|
||||
void Cover::add_on_state_callback(std::function<void()> &&f) { this->state_callback_.add(std::move(f)); }
|
||||
void Cover::publish_state(bool save) {
|
||||
this->position = clamp(this->position, 0.0f, 1.0f);
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include "esphome/core/entity_base.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
|
||||
#include "cover_traits.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -126,6 +125,25 @@ class Cover : public EntityBase, public EntityBase_DeviceClass {
|
||||
|
||||
/// Construct a new cover call used to control the cover.
|
||||
CoverCall make_call();
|
||||
/** Open the cover.
|
||||
*
|
||||
* This is a legacy method and may be removed later, please use `.make_call()` instead.
|
||||
*/
|
||||
ESPDEPRECATED("open() is deprecated, use make_call().set_command_open().perform() instead.", "2021.9")
|
||||
void open();
|
||||
/** Close the cover.
|
||||
*
|
||||
* This is a legacy method and may be removed later, please use `.make_call()` instead.
|
||||
*/
|
||||
ESPDEPRECATED("close() is deprecated, use make_call().set_command_close().perform() instead.", "2021.9")
|
||||
void close();
|
||||
/** Stop the cover.
|
||||
*
|
||||
* This is a legacy method and may be removed later, please use `.make_call()` instead.
|
||||
* As per solution from issue #2885 the call should include perform()
|
||||
*/
|
||||
ESPDEPRECATED("stop() is deprecated, use make_call().set_command_stop().perform() instead.", "2021.9")
|
||||
void stop();
|
||||
|
||||
void add_on_state_callback(std::function<void()> &&f);
|
||||
|
||||
|
||||
@@ -241,7 +241,9 @@ uint8_t DaikinArcClimate::humidity_() {
|
||||
|
||||
climate::ClimateTraits DaikinArcClimate::traits() {
|
||||
climate::ClimateTraits traits = climate_ir::ClimateIR::traits();
|
||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE | climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY);
|
||||
traits.set_supports_current_temperature(true);
|
||||
traits.set_supports_current_humidity(false);
|
||||
traits.set_supports_target_humidity(true);
|
||||
traits.set_visual_min_humidity(38);
|
||||
traits.set_visual_max_humidity(52);
|
||||
return traits;
|
||||
|
||||
@@ -82,14 +82,16 @@ class DemoClimate : public climate::Climate, public Component {
|
||||
climate::ClimateTraits traits{};
|
||||
switch (type_) {
|
||||
case DemoClimateType::TYPE_1:
|
||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE | climate::CLIMATE_SUPPORTS_ACTION);
|
||||
traits.set_supports_current_temperature(true);
|
||||
traits.set_supported_modes({
|
||||
climate::CLIMATE_MODE_OFF,
|
||||
climate::CLIMATE_MODE_HEAT,
|
||||
});
|
||||
traits.set_supports_action(true);
|
||||
traits.set_visual_temperature_step(0.5);
|
||||
break;
|
||||
case DemoClimateType::TYPE_2:
|
||||
traits.set_supports_current_temperature(false);
|
||||
traits.set_supported_modes({
|
||||
climate::CLIMATE_MODE_OFF,
|
||||
climate::CLIMATE_MODE_HEAT,
|
||||
@@ -98,7 +100,7 @@ class DemoClimate : public climate::Climate, public Component {
|
||||
climate::CLIMATE_MODE_DRY,
|
||||
climate::CLIMATE_MODE_FAN_ONLY,
|
||||
});
|
||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_ACTION);
|
||||
traits.set_supports_action(true);
|
||||
traits.set_supported_fan_modes({
|
||||
climate::CLIMATE_FAN_ON,
|
||||
climate::CLIMATE_FAN_OFF,
|
||||
@@ -121,8 +123,8 @@ class DemoClimate : public climate::Climate, public Component {
|
||||
traits.set_supported_custom_presets({"My Preset"});
|
||||
break;
|
||||
case DemoClimateType::TYPE_3:
|
||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE |
|
||||
climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE);
|
||||
traits.set_supports_current_temperature(true);
|
||||
traits.set_supports_two_point_target_temperature(true);
|
||||
traits.set_supported_modes({
|
||||
climate::CLIMATE_MODE_OFF,
|
||||
climate::CLIMATE_MODE_COOL,
|
||||
|
||||
@@ -103,7 +103,7 @@ bool EPaperBase::is_idle_() {
|
||||
if (this->busy_pin_ == nullptr) {
|
||||
return true;
|
||||
}
|
||||
return this->busy_pin_->digital_read();
|
||||
return !this->busy_pin_->digital_read();
|
||||
}
|
||||
|
||||
void EPaperBase::reset() {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import contextlib
|
||||
from dataclasses import dataclass
|
||||
import itertools
|
||||
import logging
|
||||
@@ -103,10 +102,6 @@ COMPILER_OPTIMIZATIONS = {
|
||||
"SIZE": "CONFIG_COMPILER_OPTIMIZATION_SIZE",
|
||||
}
|
||||
|
||||
# Socket limit configuration for ESP-IDF
|
||||
# ESP-IDF CONFIG_LWIP_MAX_SOCKETS has range 1-253, default 10
|
||||
DEFAULT_MAX_SOCKETS = 10 # ESP-IDF default
|
||||
|
||||
ARDUINO_ALLOWED_VARIANTS = [
|
||||
VARIANT_ESP32,
|
||||
VARIANT_ESP32C3,
|
||||
@@ -751,72 +746,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
FINAL_VALIDATE_SCHEMA = cv.Schema(final_validate)
|
||||
|
||||
|
||||
def _configure_lwip_max_sockets(conf: dict) -> None:
|
||||
"""Calculate and set CONFIG_LWIP_MAX_SOCKETS based on component needs.
|
||||
|
||||
Socket component tracks consumer needs via consume_sockets() called during config validation.
|
||||
This function runs in to_code() after all components have registered their socket needs.
|
||||
User-provided sdkconfig_options take precedence.
|
||||
"""
|
||||
from esphome.components.socket import KEY_SOCKET_CONSUMERS
|
||||
|
||||
# Check if user manually specified CONFIG_LWIP_MAX_SOCKETS
|
||||
user_max_sockets = conf.get(CONF_SDKCONFIG_OPTIONS, {}).get(
|
||||
"CONFIG_LWIP_MAX_SOCKETS"
|
||||
)
|
||||
|
||||
socket_consumers: dict[str, int] = CORE.data.get(KEY_SOCKET_CONSUMERS, {})
|
||||
total_sockets = sum(socket_consumers.values())
|
||||
|
||||
# Early return if no sockets registered and no user override
|
||||
if total_sockets == 0 and user_max_sockets is None:
|
||||
return
|
||||
|
||||
components_list = ", ".join(
|
||||
f"{name}={count}" for name, count in sorted(socket_consumers.items())
|
||||
)
|
||||
|
||||
# User specified their own value - respect it but warn if insufficient
|
||||
if user_max_sockets is not None:
|
||||
_LOGGER.info(
|
||||
"Using user-provided CONFIG_LWIP_MAX_SOCKETS: %s",
|
||||
user_max_sockets,
|
||||
)
|
||||
|
||||
# Warn if user's value is less than what components need
|
||||
if total_sockets > 0:
|
||||
user_sockets_int = 0
|
||||
with contextlib.suppress(ValueError, TypeError):
|
||||
user_sockets_int = int(user_max_sockets)
|
||||
|
||||
if user_sockets_int < total_sockets:
|
||||
_LOGGER.warning(
|
||||
"CONFIG_LWIP_MAX_SOCKETS is set to %d but your configuration "
|
||||
"needs %d sockets (registered: %s). You may experience socket "
|
||||
"exhaustion errors. Consider increasing to at least %d.",
|
||||
user_sockets_int,
|
||||
total_sockets,
|
||||
components_list,
|
||||
total_sockets,
|
||||
)
|
||||
# User's value already added via sdkconfig_options processing
|
||||
return
|
||||
|
||||
# Auto-calculate based on component needs
|
||||
# Use at least the ESP-IDF default (10), or the total needed by components
|
||||
max_sockets = max(DEFAULT_MAX_SOCKETS, total_sockets)
|
||||
|
||||
log_level = logging.INFO if max_sockets > DEFAULT_MAX_SOCKETS else logging.DEBUG
|
||||
_LOGGER.log(
|
||||
log_level,
|
||||
"Setting CONFIG_LWIP_MAX_SOCKETS to %d (registered: %s)",
|
||||
max_sockets,
|
||||
components_list,
|
||||
)
|
||||
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_MAX_SOCKETS", max_sockets)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
cg.add_platformio_option("board", config[CONF_BOARD])
|
||||
cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE])
|
||||
@@ -850,16 +779,6 @@ async def to_code(config):
|
||||
Path(__file__).parent / "post_build.py.script",
|
||||
)
|
||||
|
||||
# In testing mode, add IRAM fix script to allow linking grouped component tests
|
||||
# Similar to ESP8266's approach but for ESP-IDF
|
||||
if CORE.testing_mode:
|
||||
cg.add_build_flag("-DESPHOME_TESTING_MODE")
|
||||
add_extra_script(
|
||||
"pre",
|
||||
"iram_fix.py",
|
||||
Path(__file__).parent / "iram_fix.py.script",
|
||||
)
|
||||
|
||||
if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
|
||||
cg.add_platformio_option("framework", "espidf")
|
||||
cg.add_build_flag("-DUSE_ESP_IDF")
|
||||
@@ -886,7 +805,6 @@ async def to_code(config):
|
||||
add_idf_sdkconfig_option("CONFIG_AUTOSTART_ARDUINO", True)
|
||||
add_idf_sdkconfig_option("CONFIG_MBEDTLS_PSK_MODES", True)
|
||||
add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True)
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_PHY_REDUCE_TX_POWER", True)
|
||||
|
||||
cg.add_build_flag("-Wno-nonnull-compare")
|
||||
|
||||
@@ -937,9 +855,6 @@ async def to_code(config):
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES", False)
|
||||
if not advanced.get(CONF_ENABLE_LWIP_BRIDGE_INTERFACE, False):
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_BRIDGEIF_MAX_PORTS", 0)
|
||||
|
||||
_configure_lwip_max_sockets(conf)
|
||||
|
||||
if advanced.get(CONF_EXECUTE_FROM_PSRAM, False):
|
||||
add_idf_sdkconfig_option("CONFIG_SPIRAM_FETCH_INSTRUCTIONS", True)
|
||||
add_idf_sdkconfig_option("CONFIG_SPIRAM_RODATA", True)
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#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>
|
||||
@@ -53,16 +52,6 @@ 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(); }
|
||||
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
# pylint: disable=E0602
|
||||
Import("env") # noqa
|
||||
|
||||
# IRAM size for testing mode (2MB - large enough to accommodate grouped tests)
|
||||
TESTING_IRAM_SIZE = 0x200000
|
||||
|
||||
|
||||
def patch_idf_linker_script(source, target, env):
|
||||
"""Patch ESP-IDF linker script to increase IRAM size for testing mode."""
|
||||
# Check if we're in testing mode by looking for the define
|
||||
build_flags = env.get("BUILD_FLAGS", [])
|
||||
testing_mode = any("-DESPHOME_TESTING_MODE" in flag for flag in build_flags)
|
||||
|
||||
if not testing_mode:
|
||||
return
|
||||
|
||||
# For ESP-IDF, the linker scripts are generated in the build directory
|
||||
build_dir = env.subst("$BUILD_DIR")
|
||||
|
||||
# The memory.ld file is directly in the build directory
|
||||
memory_ld = os.path.join(build_dir, "memory.ld")
|
||||
|
||||
if not os.path.exists(memory_ld):
|
||||
print(f"ESPHome: Warning - could not find linker script at {memory_ld}")
|
||||
return
|
||||
|
||||
try:
|
||||
with open(memory_ld, "r") as f:
|
||||
content = f.read()
|
||||
except OSError as e:
|
||||
print(f"ESPHome: Error reading linker script: {e}")
|
||||
return
|
||||
|
||||
# Check if this file contains iram0_0_seg
|
||||
if 'iram0_0_seg' not in content:
|
||||
print(f"ESPHome: Warning - iram0_0_seg not found in {memory_ld}")
|
||||
return
|
||||
|
||||
# Look for iram0_0_seg definition and increase its length
|
||||
# ESP-IDF format can be:
|
||||
# iram0_0_seg (RX) : org = 0x40080000, len = 0x20000 + 0x0
|
||||
# or more complex with nested parentheses:
|
||||
# iram0_0_seg (RX) : org = (0x40370000 + 0x4000), len = (((0x403CB700 - (0x40378000 - 0x3FC88000)) - 0x3FC88000) + 0x8000 - 0x4000)
|
||||
# We want to change len to TESTING_IRAM_SIZE for testing
|
||||
|
||||
# Use a more robust approach: find the line and manually parse it
|
||||
lines = content.split('\n')
|
||||
for i, line in enumerate(lines):
|
||||
if 'iram0_0_seg' in line and 'len' in line:
|
||||
# Find the position of "len = " and replace everything after it until the end of the statement
|
||||
match = re.search(r'(iram0_0_seg\s*\([^)]*\)\s*:\s*org\s*=\s*(?:\([^)]+\)|0x[0-9a-fA-F]+)\s*,\s*len\s*=\s*)(.+?)(\s*)$', line)
|
||||
if match:
|
||||
lines[i] = f"{match.group(1)}{TESTING_IRAM_SIZE:#x}{match.group(3)}"
|
||||
break
|
||||
|
||||
updated = '\n'.join(lines)
|
||||
|
||||
if updated != content:
|
||||
with open(memory_ld, "w") as f:
|
||||
f.write(updated)
|
||||
print(f"ESPHome: Patched IRAM size to {TESTING_IRAM_SIZE:#x} in {memory_ld} for testing mode")
|
||||
else:
|
||||
print(f"ESPHome: Warning - could not patch iram0_0_seg in {memory_ld}")
|
||||
|
||||
|
||||
# Hook into the build process before linking
|
||||
# For ESP-IDF, we need to run this after the linker scripts are generated
|
||||
env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", patch_idf_linker_script)
|
||||
@@ -61,7 +61,12 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
|
||||
this->address_str_ = "";
|
||||
} else {
|
||||
char buf[18];
|
||||
format_mac_addr_upper(this->remote_bda_, buf);
|
||||
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);
|
||||
this->address_str_ = buf;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_MODE, CONF_PORT
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@ayufan"]
|
||||
AUTO_LOAD = ["camera"]
|
||||
@@ -14,27 +13,13 @@ Mode = esp32_camera_web_server_ns.enum("Mode")
|
||||
|
||||
MODES = {"STREAM": Mode.STREAM, "SNAPSHOT": Mode.SNAPSHOT}
|
||||
|
||||
|
||||
def _consume_camera_web_server_sockets(config: ConfigType) -> ConfigType:
|
||||
"""Register socket needs for camera web server."""
|
||||
from esphome.components import socket
|
||||
|
||||
# Each camera web server instance needs 1 listening socket + 2 client connections
|
||||
sockets_needed = 3
|
||||
socket.consume_sockets(sockets_needed, "esp32_camera_web_server")(config)
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(CameraWebServer),
|
||||
cv.Required(CONF_PORT): cv.port,
|
||||
cv.Required(CONF_MODE): cv.enum(MODES, upper=True),
|
||||
},
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
_consume_camera_web_server_sockets,
|
||||
)
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(CameraWebServer),
|
||||
cv.Required(CONF_PORT): cv.port,
|
||||
cv.Required(CONF_MODE): cv.enum(MODES, upper=True),
|
||||
},
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
|
||||
@@ -95,7 +95,7 @@ async def to_code(config):
|
||||
if framework_ver >= cv.Version(5, 5, 0):
|
||||
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.1.5")
|
||||
esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.3")
|
||||
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.6.1")
|
||||
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.5.11")
|
||||
else:
|
||||
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.13.0")
|
||||
esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0")
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import binary_sensor, esp32_ble, improv_base, output
|
||||
from esphome.components import binary_sensor, esp32_ble, output
|
||||
from esphome.components.esp32_ble import BTLoggers
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_ON_STATE, CONF_TRIGGER_ID
|
||||
|
||||
AUTO_LOAD = ["esp32_ble_server", "improv_base"]
|
||||
AUTO_LOAD = ["esp32_ble_server"]
|
||||
CODEOWNERS = ["@jesserockz"]
|
||||
DEPENDENCIES = ["wifi", "esp32"]
|
||||
|
||||
@@ -20,7 +20,6 @@ CONF_ON_STOP = "on_stop"
|
||||
CONF_STATUS_INDICATOR = "status_indicator"
|
||||
CONF_WIFI_TIMEOUT = "wifi_timeout"
|
||||
|
||||
|
||||
improv_ns = cg.esphome_ns.namespace("improv")
|
||||
Error = improv_ns.enum("Error")
|
||||
State = improv_ns.enum("State")
|
||||
@@ -44,63 +43,55 @@ ESP32ImprovStoppedTrigger = esp32_improv_ns.class_(
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(ESP32ImprovComponent),
|
||||
cv.Required(CONF_AUTHORIZER): cv.Any(
|
||||
cv.none, cv.use_id(binary_sensor.BinarySensor)
|
||||
),
|
||||
cv.Optional(CONF_STATUS_INDICATOR): cv.use_id(output.BinaryOutput),
|
||||
cv.Optional(
|
||||
CONF_IDENTIFY_DURATION, default="10s"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(
|
||||
CONF_AUTHORIZED_DURATION, default="1min"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(
|
||||
CONF_WIFI_TIMEOUT, default="1min"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_ON_PROVISIONED): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
ESP32ImprovProvisionedTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_PROVISIONING): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
ESP32ImprovProvisioningTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_START): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
ESP32ImprovStartTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_STATE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
ESP32ImprovStateTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_STOP): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
ESP32ImprovStoppedTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(improv_base.IMPROV_SCHEMA)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(ESP32ImprovComponent),
|
||||
cv.Required(CONF_AUTHORIZER): cv.Any(
|
||||
cv.none, cv.use_id(binary_sensor.BinarySensor)
|
||||
),
|
||||
cv.Optional(CONF_STATUS_INDICATOR): cv.use_id(output.BinaryOutput),
|
||||
cv.Optional(
|
||||
CONF_IDENTIFY_DURATION, default="10s"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(
|
||||
CONF_AUTHORIZED_DURATION, default="1min"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(
|
||||
CONF_WIFI_TIMEOUT, default="1min"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_ON_PROVISIONED): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
ESP32ImprovProvisionedTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_PROVISIONING): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
ESP32ImprovProvisioningTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_START): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESP32ImprovStartTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_STATE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESP32ImprovStateTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_STOP): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
ESP32ImprovStoppedTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
@@ -111,8 +102,7 @@ async def to_code(config):
|
||||
await cg.register_component(var, config)
|
||||
|
||||
cg.add_define("USE_IMPROV")
|
||||
|
||||
await improv_base.setup_improv_core(var, config)
|
||||
cg.add_library("improv/Improv", "1.2.4")
|
||||
|
||||
cg.add(var.set_identify_duration(config[CONF_IDENTIFY_DURATION]))
|
||||
cg.add(var.set_authorized_duration(config[CONF_AUTHORIZED_DURATION]))
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
#include "esp32_improv_component.h"
|
||||
|
||||
#include "esphome/components/bytebuffer/bytebuffer.h"
|
||||
#include "esphome/components/esp32_ble/ble.h"
|
||||
#include "esphome/components/esp32_ble_server/ble_2902.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/components/bytebuffer/bytebuffer.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
@@ -384,32 +384,17 @@ void ESP32ImprovComponent::check_wifi_connection_() {
|
||||
this->connecting_sta_ = {};
|
||||
this->cancel_timeout("wifi-connect-timeout");
|
||||
|
||||
// Build URL list with minimal allocations
|
||||
// Maximum 3 URLs: custom next_url + ESPHOME_MY_LINK + webserver URL
|
||||
std::string url_strings[3];
|
||||
size_t url_count = 0;
|
||||
|
||||
// Add next_url if configured (should be first per Improv BLE spec)
|
||||
std::string next_url = this->get_formatted_next_url_();
|
||||
if (!next_url.empty()) {
|
||||
url_strings[url_count++] = std::move(next_url);
|
||||
}
|
||||
|
||||
// Add default URLs for backward compatibility
|
||||
url_strings[url_count++] = ESPHOME_MY_LINK;
|
||||
std::vector<std::string> urls = {ESPHOME_MY_LINK};
|
||||
#ifdef USE_WEBSERVER
|
||||
for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) {
|
||||
if (ip.is_ip4()) {
|
||||
char url_buffer[64];
|
||||
snprintf(url_buffer, sizeof(url_buffer), "http://%s:%d", ip.str().c_str(), USE_WEBSERVER_PORT);
|
||||
url_strings[url_count++] = url_buffer;
|
||||
std::string webserver_url = "http://" + ip.str() + ":" + to_string(USE_WEBSERVER_PORT);
|
||||
urls.push_back(webserver_url);
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
// Pass to build_rpc_response using vector constructor from iterators to avoid extra copies
|
||||
std::vector<uint8_t> data = improv::build_rpc_response(
|
||||
improv::WIFI_SETTINGS, std::vector<std::string>(url_strings, url_strings + url_count));
|
||||
std::vector<uint8_t> data = improv::build_rpc_response(improv::WIFI_SETTINGS, urls);
|
||||
this->send_response_(data);
|
||||
} else if (this->is_active() && this->state_ != improv::STATE_PROVISIONED) {
|
||||
ESP_LOGD(TAG, "WiFi provisioned externally");
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
#include "esphome/components/esp32_ble_server/ble_characteristic.h"
|
||||
#include "esphome/components/esp32_ble_server/ble_server.h"
|
||||
#include "esphome/components/improv_base/improv_base.h"
|
||||
#include "esphome/components/wifi/wifi_component.h"
|
||||
|
||||
#ifdef USE_ESP32_IMPROV_STATE_CALLBACK
|
||||
@@ -33,7 +32,7 @@ namespace esp32_improv {
|
||||
|
||||
using namespace esp32_ble_server;
|
||||
|
||||
class ESP32ImprovComponent : public Component, public improv_base::ImprovBase {
|
||||
class ESP32ImprovComponent : public Component {
|
||||
public:
|
||||
ESP32ImprovComponent();
|
||||
void dump_config() override;
|
||||
|
||||
@@ -103,16 +103,7 @@ def ota_esphome_final_validate(config):
|
||||
)
|
||||
|
||||
|
||||
def _consume_ota_sockets(config: ConfigType) -> ConfigType:
|
||||
"""Register socket needs for OTA component."""
|
||||
from esphome.components import socket
|
||||
|
||||
# OTA needs 1 listening socket (client connections are temporary during updates)
|
||||
socket.consume_sockets(1, "ota")(config)
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(ESPHomeOTAComponent),
|
||||
@@ -139,8 +130,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
}
|
||||
)
|
||||
.extend(BASE_OTA_SCHEMA)
|
||||
.extend(cv.COMPONENT_SCHEMA),
|
||||
_consume_ota_sockets,
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = ota_esphome_final_validate
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
"""ESP-NOW transport platform for packet_transport component."""
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.packet_transport import (
|
||||
PacketTransport,
|
||||
new_packet_transport,
|
||||
transport_schema,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.core import HexInt
|
||||
from esphome.cpp_types import PollingComponent
|
||||
|
||||
from .. import ESPNowComponent, espnow_ns
|
||||
|
||||
CODEOWNERS = ["@EasilyBoredEngineer"]
|
||||
DEPENDENCIES = ["espnow"]
|
||||
|
||||
ESPNowTransport = espnow_ns.class_("ESPNowTransport", PacketTransport, PollingComponent)
|
||||
|
||||
CONF_ESPNOW_ID = "espnow_id"
|
||||
CONF_PEER_ADDRESS = "peer_address"
|
||||
|
||||
CONFIG_SCHEMA = transport_schema(ESPNowTransport).extend(
|
||||
{
|
||||
cv.GenerateID(CONF_ESPNOW_ID): cv.use_id(ESPNowComponent),
|
||||
cv.Optional(CONF_PEER_ADDRESS, default="FF:FF:FF:FF:FF:FF"): cv.mac_address,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
"""Set up the ESP-NOW transport component."""
|
||||
var, _ = await new_packet_transport(config)
|
||||
|
||||
await cg.register_parented(var, config[CONF_ESPNOW_ID])
|
||||
|
||||
# Set peer address - convert MAC to parts array like ESP-NOW does
|
||||
mac = config[CONF_PEER_ADDRESS]
|
||||
cg.add(var.set_peer_address([HexInt(x) for x in mac.parts]))
|
||||
@@ -1,97 +0,0 @@
|
||||
#include "espnow_transport.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace espnow {
|
||||
|
||||
static const char *const TAG = "espnow.transport";
|
||||
|
||||
bool ESPNowTransport::should_send() { return this->parent_ != nullptr && !this->parent_->is_failed(); }
|
||||
|
||||
void ESPNowTransport::setup() {
|
||||
packet_transport::PacketTransport::setup();
|
||||
|
||||
if (this->parent_ == nullptr) {
|
||||
ESP_LOGE(TAG, "ESPNow component not set");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Registering ESP-NOW handlers");
|
||||
ESP_LOGI(TAG, "Peer address: %02X:%02X:%02X:%02X:%02X:%02X", this->peer_address_[0], this->peer_address_[1],
|
||||
this->peer_address_[2], this->peer_address_[3], this->peer_address_[4], this->peer_address_[5]);
|
||||
|
||||
// Register received handler
|
||||
this->parent_->register_received_handler(static_cast<ESPNowReceivedPacketHandler *>(this));
|
||||
|
||||
// Register broadcasted handler
|
||||
this->parent_->register_broadcasted_handler(static_cast<ESPNowBroadcastedHandler *>(this));
|
||||
}
|
||||
|
||||
void ESPNowTransport::update() {
|
||||
packet_transport::PacketTransport::update();
|
||||
this->updated_ = true;
|
||||
}
|
||||
|
||||
void ESPNowTransport::send_packet(const std::vector<uint8_t> &buf) const {
|
||||
if (this->parent_ == nullptr) {
|
||||
ESP_LOGE(TAG, "ESPNow component not set");
|
||||
return;
|
||||
}
|
||||
|
||||
if (buf.empty()) {
|
||||
ESP_LOGW(TAG, "Attempted to send empty packet");
|
||||
return;
|
||||
}
|
||||
|
||||
if (buf.size() > ESP_NOW_MAX_DATA_LEN) {
|
||||
ESP_LOGE(TAG, "Packet too large: %zu bytes (max %d)", buf.size(), ESP_NOW_MAX_DATA_LEN);
|
||||
return;
|
||||
}
|
||||
|
||||
// Send to configured peer address
|
||||
this->parent_->send(this->peer_address_.data(), buf.data(), buf.size(), [](esp_err_t err) {
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Send failed: %d", err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool ESPNowTransport::on_received(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) {
|
||||
ESP_LOGV(TAG, "Received packet of size %u from %02X:%02X:%02X:%02X:%02X:%02X", size, info.src_addr[0],
|
||||
info.src_addr[1], info.src_addr[2], info.src_addr[3], info.src_addr[4], info.src_addr[5]);
|
||||
|
||||
if (data == nullptr || size == 0) {
|
||||
ESP_LOGW(TAG, "Received empty or null packet");
|
||||
return false;
|
||||
}
|
||||
|
||||
this->packet_buffer_.resize(size);
|
||||
memcpy(this->packet_buffer_.data(), data, size);
|
||||
this->process_(this->packet_buffer_);
|
||||
return false; // Allow other handlers to run
|
||||
}
|
||||
|
||||
bool ESPNowTransport::on_broadcasted(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) {
|
||||
ESP_LOGV(TAG, "Received broadcast packet of size %u from %02X:%02X:%02X:%02X:%02X:%02X", size, info.src_addr[0],
|
||||
info.src_addr[1], info.src_addr[2], info.src_addr[3], info.src_addr[4], info.src_addr[5]);
|
||||
|
||||
if (data == nullptr || size == 0) {
|
||||
ESP_LOGW(TAG, "Received empty or null broadcast packet");
|
||||
return false;
|
||||
}
|
||||
|
||||
this->packet_buffer_.resize(size);
|
||||
memcpy(this->packet_buffer_.data(), data, size);
|
||||
this->process_(this->packet_buffer_);
|
||||
return false; // Allow other handlers to run
|
||||
}
|
||||
|
||||
} // namespace espnow
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESP32
|
||||
@@ -1,44 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "../espnow_component.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/packet_transport/packet_transport.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace esphome {
|
||||
namespace espnow {
|
||||
|
||||
class ESPNowTransport : public packet_transport::PacketTransport,
|
||||
public Parented<ESPNowComponent>,
|
||||
public ESPNowReceivedPacketHandler,
|
||||
public ESPNowBroadcastedHandler {
|
||||
public:
|
||||
void setup() override;
|
||||
void update() override;
|
||||
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
|
||||
|
||||
void set_peer_address(peer_address_t address) {
|
||||
memcpy(this->peer_address_.data(), address.data(), ESP_NOW_ETH_ALEN);
|
||||
}
|
||||
|
||||
// ESPNow handler interface
|
||||
bool on_received(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override;
|
||||
bool on_broadcasted(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override;
|
||||
|
||||
protected:
|
||||
void send_packet(const std::vector<uint8_t> &buf) const override;
|
||||
size_t get_max_packet_size() override { return ESP_NOW_MAX_DATA_LEN; }
|
||||
bool should_send() override;
|
||||
|
||||
peer_address_t peer_address_{{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}};
|
||||
std::vector<uint8_t> packet_buffer_;
|
||||
};
|
||||
|
||||
} // namespace espnow
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESP32
|
||||
@@ -38,6 +38,7 @@ IS_PLATFORM_COMPONENT = True
|
||||
|
||||
fan_ns = cg.esphome_ns.namespace("fan")
|
||||
Fan = fan_ns.class_("Fan", cg.EntityBase)
|
||||
FanState = fan_ns.class_("Fan", Fan, cg.Component)
|
||||
|
||||
FanDirection = fan_ns.enum("FanDirection", is_class=True)
|
||||
FAN_DIRECTION_ENUM = {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "fan.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "fan_state.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace fan {
|
||||
|
||||
16
esphome/components/fan/fan_state.cpp
Normal file
16
esphome/components/fan/fan_state.cpp
Normal file
@@ -0,0 +1,16 @@
|
||||
#include "fan_state.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace fan {
|
||||
|
||||
static const char *const TAG = "fan";
|
||||
|
||||
void FanState::setup() {
|
||||
auto restore = this->restore_state_();
|
||||
if (restore)
|
||||
restore->to_call(*this).perform();
|
||||
}
|
||||
float FanState::get_setup_priority() const { return setup_priority::DATA - 1.0f; }
|
||||
|
||||
} // namespace fan
|
||||
} // namespace esphome
|
||||
34
esphome/components/fan/fan_state.h
Normal file
34
esphome/components/fan/fan_state.h
Normal file
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "fan.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace fan {
|
||||
|
||||
enum ESPDEPRECATED("LegacyFanDirection members are deprecated, use FanDirection instead.",
|
||||
"2022.2") LegacyFanDirection {
|
||||
FAN_DIRECTION_FORWARD = 0,
|
||||
FAN_DIRECTION_REVERSE = 1
|
||||
};
|
||||
|
||||
class ESPDEPRECATED("FanState is deprecated, use Fan instead.", "2022.2") FanState : public Fan, public Component {
|
||||
public:
|
||||
FanState() = default;
|
||||
|
||||
/// Get the traits of this fan.
|
||||
FanTraits get_traits() override { return this->traits_; }
|
||||
/// Set the traits of this fan (i.e. what features it supports).
|
||||
void set_traits(const FanTraits &traits) { this->traits_ = traits; }
|
||||
|
||||
void setup() override;
|
||||
float get_setup_priority() const override;
|
||||
|
||||
protected:
|
||||
void control(const FanCall &call) override { this->publish_state(); }
|
||||
|
||||
FanTraits traits_{};
|
||||
};
|
||||
|
||||
} // namespace fan
|
||||
} // namespace esphome
|
||||
@@ -65,7 +65,7 @@ HaierClimateBase::HaierClimateBase()
|
||||
{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH});
|
||||
this->traits_.set_supported_swing_modes({climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH,
|
||||
climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL});
|
||||
this->traits_.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
|
||||
this->traits_.set_supports_current_temperature(true);
|
||||
}
|
||||
|
||||
HaierClimateBase::~HaierClimateBase() {}
|
||||
|
||||
@@ -16,8 +16,7 @@ void HDC1080Component::setup() {
|
||||
|
||||
// if configuration fails - there is a problem
|
||||
if (this->write_register(HDC1080_CMD_CONFIGURATION, config, 2) != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "Failed to configure HDC1080");
|
||||
this->status_set_warning();
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,42 +6,31 @@
|
||||
namespace esphome {
|
||||
namespace improv_base {
|
||||
|
||||
static constexpr const char DEVICE_NAME_PLACEHOLDER[] = "{{device_name}}";
|
||||
static constexpr size_t DEVICE_NAME_PLACEHOLDER_LEN = sizeof(DEVICE_NAME_PLACEHOLDER) - 1;
|
||||
static constexpr const char IP_ADDRESS_PLACEHOLDER[] = "{{ip_address}}";
|
||||
static constexpr size_t IP_ADDRESS_PLACEHOLDER_LEN = sizeof(IP_ADDRESS_PLACEHOLDER) - 1;
|
||||
|
||||
static void replace_all_in_place(std::string &str, const char *placeholder, size_t placeholder_len,
|
||||
const std::string &replacement) {
|
||||
size_t pos = 0;
|
||||
const size_t replacement_len = replacement.length();
|
||||
while ((pos = str.find(placeholder, pos)) != std::string::npos) {
|
||||
str.replace(pos, placeholder_len, replacement);
|
||||
pos += replacement_len;
|
||||
}
|
||||
}
|
||||
|
||||
std::string ImprovBase::get_formatted_next_url_() {
|
||||
if (this->next_url_.empty()) {
|
||||
return "";
|
||||
}
|
||||
std::string copy = this->next_url_;
|
||||
// Device name
|
||||
std::size_t pos = this->next_url_.find("{{device_name}}");
|
||||
if (pos != std::string::npos) {
|
||||
const std::string &device_name = App.get_name();
|
||||
copy.replace(pos, 15, device_name);
|
||||
}
|
||||
|
||||
std::string formatted_url = this->next_url_;
|
||||
|
||||
// Replace all occurrences of {{device_name}}
|
||||
replace_all_in_place(formatted_url, DEVICE_NAME_PLACEHOLDER, DEVICE_NAME_PLACEHOLDER_LEN, App.get_name());
|
||||
|
||||
// Replace all occurrences of {{ip_address}}
|
||||
for (auto &ip : network::get_ip_addresses()) {
|
||||
if (ip.is_ip4()) {
|
||||
replace_all_in_place(formatted_url, IP_ADDRESS_PLACEHOLDER, IP_ADDRESS_PLACEHOLDER_LEN, ip.str());
|
||||
break;
|
||||
// Ip address
|
||||
pos = this->next_url_.find("{{ip_address}}");
|
||||
if (pos != std::string::npos) {
|
||||
for (auto &ip : network::get_ip_addresses()) {
|
||||
if (ip.is_ip4()) {
|
||||
std::string ipa = ip.str();
|
||||
copy.replace(pos, 14, ipa);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Note: {{esphome_version}} is replaced at code generation time in Python
|
||||
|
||||
return formatted_url;
|
||||
return copy;
|
||||
}
|
||||
|
||||
} // namespace improv_base
|
||||
|
||||
@@ -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", format_hex_pretty(data).c_str());
|
||||
ESP_LOGV(TAG, "Data: %s", hexencode(data).c_str());
|
||||
|
||||
float value = (float) get_16bit(0);
|
||||
for (int i = 0; i < data[3]; i++)
|
||||
|
||||
@@ -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,6 +17,8 @@
|
||||
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);
|
||||
|
||||
|
||||
@@ -104,200 +104,5 @@ 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
|
||||
|
||||
@@ -406,7 +406,7 @@ void LightCall::transform_parameters_() {
|
||||
}
|
||||
}
|
||||
ColorMode LightCall::compute_color_mode_() {
|
||||
const auto &supported_modes = this->parent_->get_traits().get_supported_color_modes();
|
||||
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,19 +425,20 @@ 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.
|
||||
// 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();
|
||||
std::set<ColorMode> suitable_modes = this->get_suitable_color_modes_();
|
||||
|
||||
// Don't change if the current mode is in the intersection (suitable AND supported)
|
||||
if (ColorModeMask::mask_contains(intersection, current_mode)) {
|
||||
// Don't change if the current mode is suitable.
|
||||
if (suitable_modes.count(current_mode) > 0) {
|
||||
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.
|
||||
if (intersection != 0) {
|
||||
ColorMode mode = ColorModeMask::first_mode_from_mask(intersection);
|
||||
for (auto mode : suitable_modes) {
|
||||
if (supported_modes.count(mode) == 0)
|
||||
continue;
|
||||
|
||||
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;
|
||||
@@ -450,7 +451,7 @@ ColorMode LightCall::compute_color_mode_() {
|
||||
LOG_STR_ARG(color_mode_to_human(color_mode)));
|
||||
return color_mode;
|
||||
}
|
||||
color_mode_bitmask_t LightCall::get_suitable_color_modes_mask_() {
|
||||
std::set<ColorMode> LightCall::get_suitable_color_modes_() {
|
||||
bool has_white = this->has_white() && this->white_ > 0.0f;
|
||||
bool has_ct = this->has_color_temperature();
|
||||
bool has_cwww =
|
||||
@@ -458,44 +459,36 @@ color_mode_bitmask_t LightCall::get_suitable_color_modes_mask_() {
|
||||
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 ColorModeMask({ColorMode::WHITE, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE,
|
||||
ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE})
|
||||
.get_mask();
|
||||
return {ColorMode::WHITE, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE,
|
||||
ColorMode::RGB_COLD_WARM_WHITE};
|
||||
case KEY(false, true, false, false): // ct only
|
||||
return ColorModeMask({ColorMode::COLOR_TEMPERATURE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE,
|
||||
ColorMode::RGB_COLD_WARM_WHITE})
|
||||
.get_mask();
|
||||
return {ColorMode::COLOR_TEMPERATURE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE,
|
||||
ColorMode::RGB_COLD_WARM_WHITE};
|
||||
case KEY(true, true, false, false): // white + ct
|
||||
return ColorModeMask(
|
||||
{ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE})
|
||||
.get_mask();
|
||||
return {ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE};
|
||||
case KEY(false, false, true, false): // cwww only
|
||||
return ColorModeMask({ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE}).get_mask();
|
||||
return {ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE};
|
||||
case KEY(false, false, false, false): // none
|
||||
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();
|
||||
return {ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE, ColorMode::RGB,
|
||||
ColorMode::WHITE, ColorMode::COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE};
|
||||
case KEY(true, false, false, true): // rgb + white
|
||||
return ColorModeMask({ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE})
|
||||
.get_mask();
|
||||
return {ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE};
|
||||
case KEY(false, true, false, true): // rgb + ct
|
||||
case KEY(true, true, false, true): // rgb + white + ct
|
||||
return ColorModeMask({ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}).get_mask();
|
||||
return {ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE};
|
||||
case KEY(false, false, true, true): // rgb + cwww
|
||||
return ColorModeMask({ColorMode::RGB_COLD_WARM_WHITE}).get_mask();
|
||||
return {ColorMode::RGB_COLD_WARM_WHITE};
|
||||
case KEY(false, false, false, true): // rgb only
|
||||
return ColorModeMask({ColorMode::RGB, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE,
|
||||
ColorMode::RGB_COLD_WARM_WHITE})
|
||||
.get_mask();
|
||||
return {ColorMode::RGB, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE};
|
||||
default:
|
||||
return 0; // conflicting flags
|
||||
return {}; // conflicting flags
|
||||
}
|
||||
|
||||
#undef KEY
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "light_color_values.h"
|
||||
#include <set>
|
||||
|
||||
namespace esphome {
|
||||
|
||||
@@ -185,8 +186,8 @@ class LightCall {
|
||||
|
||||
//// Compute the color mode that should be used for this call.
|
||||
ColorMode compute_color_mode_();
|
||||
/// Get potential color modes bitmask for this light call.
|
||||
color_mode_bitmask_t get_suitable_color_modes_mask_();
|
||||
/// Get potential color modes for this light call.
|
||||
std::set<ColorMode> get_suitable_color_modes_();
|
||||
/// Some color modes also can be set using non-native parameters, transform those calls.
|
||||
void transform_parameters_();
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ 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);
|
||||
|
||||
@@ -191,9 +191,11 @@ 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,
|
||||
@@ -207,6 +209,7 @@ 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) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "color_mode.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "color_mode.h"
|
||||
#include <set>
|
||||
|
||||
namespace esphome {
|
||||
|
||||
@@ -18,17 +19,38 @@ class LightTraits {
|
||||
public:
|
||||
LightTraits() = default;
|
||||
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
bool supports_color_mode(ColorMode color_mode) const { return this->supported_color_modes_.contains(color_mode); }
|
||||
bool supports_color_mode(ColorMode color_mode) const { return this->supported_color_modes_.count(color_mode); }
|
||||
bool supports_color_capability(ColorCapability color_capability) const {
|
||||
return this->supported_color_modes_.has_capability(color_capability);
|
||||
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));
|
||||
}
|
||||
|
||||
float get_min_mireds() const { return this->min_mireds_; }
|
||||
@@ -37,9 +59,19 @@ 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
|
||||
|
||||
@@ -13,7 +13,6 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core import CORE, Lambda, coroutine_with_priority
|
||||
from esphome.coroutine import CoroPriority
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
DEPENDENCIES = ["network"]
|
||||
@@ -47,19 +46,6 @@ SERVICE_SCHEMA = cv.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _consume_mdns_sockets(config: ConfigType) -> ConfigType:
|
||||
"""Register socket needs for mDNS component."""
|
||||
if config.get(CONF_DISABLED):
|
||||
return config
|
||||
|
||||
from esphome.components import socket
|
||||
|
||||
# mDNS needs 2 sockets (IPv4 + IPv6 multicast)
|
||||
socket.consume_sockets(2, "mdns")(config)
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
@@ -69,7 +55,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
}
|
||||
),
|
||||
_remove_id_if_disabled,
|
||||
_consume_mdns_sockets,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -31,17 +31,18 @@ void MDNSComponent::setup() {
|
||||
mdns_instance_name_set(this->hostname_.c_str());
|
||||
|
||||
for (const auto &service : services) {
|
||||
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];
|
||||
std::vector<mdns_txt_item_t> txt_records;
|
||||
for (const auto &record : service.txt_records) {
|
||||
mdns_txt_item_t it{};
|
||||
// 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
|
||||
txt_records[i].key = MDNS_STR_ARG(record.key);
|
||||
txt_records[i].value = MDNS_STR_ARG(record.value);
|
||||
it.key = MDNS_STR_ARG(record.key);
|
||||
it.value = MDNS_STR_ARG(record.value);
|
||||
txt_records.push_back(it);
|
||||
}
|
||||
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.get(), service.txt_records.size());
|
||||
txt_records.data(), 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));
|
||||
|
||||
@@ -77,7 +77,7 @@ void AirConditioner::control(const ClimateCall &call) {
|
||||
|
||||
ClimateTraits AirConditioner::traits() {
|
||||
auto traits = ClimateTraits();
|
||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
|
||||
traits.set_supports_current_temperature(true);
|
||||
traits.set_visual_min_temperature(17);
|
||||
traits.set_visual_max_temperature(30);
|
||||
traits.set_visual_temperature_step(0.5);
|
||||
|
||||
@@ -30,19 +30,6 @@ wave_4_3 = DriverChip(
|
||||
"blue": [14, 38, 18, 17, 10],
|
||||
},
|
||||
)
|
||||
|
||||
wave_4_3.extend(
|
||||
"WAVESHARE-5-1024X600",
|
||||
width=1024,
|
||||
height=600,
|
||||
hsync_back_porch=145,
|
||||
hsync_front_porch=170,
|
||||
hsync_pulse_width=30,
|
||||
vsync_back_porch=23,
|
||||
vsync_front_porch=12,
|
||||
vsync_pulse_width=2,
|
||||
)
|
||||
|
||||
wave_4_3.extend(
|
||||
"ESP32-S3-TOUCH-LCD-7-800X480",
|
||||
enable_pin=[{"ch422g": None, "number": 2}, {"ch422g": None, "number": 6}],
|
||||
|
||||
@@ -52,9 +52,8 @@ const uint8_t MITSUBISHI_BYTE16 = 0x00;
|
||||
|
||||
climate::ClimateTraits MitsubishiClimate::traits() {
|
||||
auto traits = climate::ClimateTraits();
|
||||
if (this->sensor_ != nullptr) {
|
||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
|
||||
}
|
||||
traits.set_supports_current_temperature(this->sensor_ != nullptr);
|
||||
traits.set_supports_action(false);
|
||||
traits.set_visual_min_temperature(MITSUBISHI_TEMP_MIN);
|
||||
traits.set_visual_max_temperature(MITSUBISHI_TEMP_MAX);
|
||||
traits.set_visual_temperature_step(1.0f);
|
||||
|
||||
@@ -58,7 +58,6 @@ from esphome.const import (
|
||||
PlatformFramework,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DEPENDENCIES = ["network"]
|
||||
|
||||
@@ -211,15 +210,6 @@ def validate_fingerprint(value):
|
||||
return value
|
||||
|
||||
|
||||
def _consume_mqtt_sockets(config: ConfigType) -> ConfigType:
|
||||
"""Register socket needs for MQTT component."""
|
||||
from esphome.components import socket
|
||||
|
||||
# MQTT needs 1 socket for the broker connection
|
||||
socket.consume_sockets(1, "mqtt")(config)
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
@@ -316,7 +306,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
),
|
||||
validate_config,
|
||||
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX]),
|
||||
_consume_mqtt_sockets,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -140,8 +140,11 @@ void MQTTClientComponent::send_device_info_() {
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
root[api::global_api_server->get_noise_ctx()->has_psk() ? "api_encryption" : "api_encryption_supported"] =
|
||||
"Noise_NNpsk0_25519_ChaChaPoly_SHA256";
|
||||
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";
|
||||
}
|
||||
#endif
|
||||
},
|
||||
2, this->discovery_info_.retain);
|
||||
|
||||
@@ -85,20 +85,24 @@ bool MQTTComponent::send_discovery_() {
|
||||
}
|
||||
|
||||
// Fields from EntityBase
|
||||
root[MQTT_NAME] = this->get_entity()->has_own_name() ? this->friendly_name() : "";
|
||||
|
||||
if (this->get_entity()->has_own_name()) {
|
||||
root[MQTT_NAME] = this->friendly_name();
|
||||
} else {
|
||||
root[MQTT_NAME] = "";
|
||||
}
|
||||
if (this->is_disabled_by_default())
|
||||
root[MQTT_ENABLED_BY_DEFAULT] = false;
|
||||
if (!this->get_icon().empty())
|
||||
root[MQTT_ICON] = this->get_icon();
|
||||
|
||||
const auto entity_category = this->get_entity()->get_entity_category();
|
||||
switch (entity_category) {
|
||||
switch (this->get_entity()->get_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] = entity_category == ENTITY_CATEGORY_CONFIG ? "config" : "diagnostic";
|
||||
root[MQTT_ENTITY_CATEGORY] = "diagnostic";
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -109,14 +113,20 @@ bool MQTTComponent::send_discovery_() {
|
||||
if (this->command_retain_)
|
||||
root[MQTT_COMMAND_RETAIN] = true;
|
||||
|
||||
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;
|
||||
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 MQTTDiscoveryInfo &discovery_info = global_mqtt_client->get_discovery_info();
|
||||
@@ -135,8 +145,10 @@ bool MQTTComponent::send_discovery_() {
|
||||
if (discovery_info.object_id_generator == MQTT_DEVICE_NAME_OBJECT_ID_GENERATOR)
|
||||
root[MQTT_OBJECT_ID] = node_name + "_" + this->get_default_object_id_();
|
||||
|
||||
const std::string &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_friendly_name = App.get_friendly_name();
|
||||
if (node_friendly_name.empty()) {
|
||||
node_friendly_name = node_name;
|
||||
}
|
||||
std::string node_area = App.get_area();
|
||||
|
||||
JsonObject device_info = root[MQTT_DEVICE].to<JsonObject>();
|
||||
@@ -146,9 +158,13 @@ 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, '.');
|
||||
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);
|
||||
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);
|
||||
}
|
||||
#else
|
||||
device_info[MQTT_DEVICE_SW_VERSION] = ESPHOME_VERSION " (" + App.get_compilation_time() + ")";
|
||||
device_info[MQTT_DEVICE_MODEL] = ESPHOME_BOARD;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
#ifdef USE_MQTT
|
||||
#ifdef USE_FAN
|
||||
|
||||
#include "esphome/components/fan/fan.h"
|
||||
#include "esphome/components/fan/fan_state.h"
|
||||
#include "mqtt_component.h"
|
||||
|
||||
namespace esphome {
|
||||
|
||||
@@ -69,12 +69,6 @@ void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscovery
|
||||
if (traits.supports_color_capability(ColorCapability::BRIGHTNESS))
|
||||
root["brightness"] = true;
|
||||
|
||||
if (traits.supports_color_mode(ColorMode::COLOR_TEMPERATURE) ||
|
||||
traits.supports_color_mode(ColorMode::COLD_WARM_WHITE)) {
|
||||
root[MQTT_MIN_MIREDS] = traits.get_min_mireds();
|
||||
root[MQTT_MAX_MIREDS] = traits.get_max_mireds();
|
||||
}
|
||||
|
||||
if (this->state_->supports_effects()) {
|
||||
root["effect"] = true;
|
||||
JsonArray effect_list = root[MQTT_EFFECT_LIST].to<JsonArray>();
|
||||
|
||||
@@ -1291,6 +1291,9 @@ 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
|
||||
|
||||
@@ -54,10 +54,11 @@ void PIDClimate::control(const climate::ClimateCall &call) {
|
||||
}
|
||||
climate::ClimateTraits PIDClimate::traits() {
|
||||
auto traits = climate::ClimateTraits();
|
||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE | climate::CLIMATE_SUPPORTS_ACTION);
|
||||
traits.set_supports_current_temperature(true);
|
||||
traits.set_supports_two_point_target_temperature(false);
|
||||
|
||||
if (this->humidity_sensor_ != nullptr)
|
||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY);
|
||||
traits.set_supports_current_humidity(true);
|
||||
|
||||
traits.set_supported_modes({climate::CLIMATE_MODE_OFF});
|
||||
if (supports_cool_())
|
||||
@@ -67,6 +68,7 @@ climate::ClimateTraits PIDClimate::traits() {
|
||||
if (supports_heat_() && supports_cool_())
|
||||
traits.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL);
|
||||
|
||||
traits.set_supports_action(true);
|
||||
return traits;
|
||||
}
|
||||
void PIDClimate::dump_config() {
|
||||
|
||||
@@ -62,7 +62,7 @@ CONF_WARNING_MPPT_OVERLOAD = "warning_mppt_overload"
|
||||
CONF_WARNING_BATTERY_TOO_LOW_TO_CHARGE = "warning_battery_too_low_to_charge"
|
||||
CONF_FAULT_DC_DC_OVER_CURRENT = "fault_dc_dc_over_current"
|
||||
CONF_FAULT_CODE = "fault_code"
|
||||
CONF_WARNING_LOW_PV_ENERGY = "warning_low_pv_energy"
|
||||
CONF_WARNUNG_LOW_PV_ENERGY = "warnung_low_pv_energy"
|
||||
CONF_WARNING_HIGH_AC_INPUT_DURING_BUS_SOFT_START = (
|
||||
"warning_high_ac_input_during_bus_soft_start"
|
||||
)
|
||||
@@ -122,7 +122,7 @@ TYPES = [
|
||||
CONF_WARNING_BATTERY_TOO_LOW_TO_CHARGE,
|
||||
CONF_FAULT_DC_DC_OVER_CURRENT,
|
||||
CONF_FAULT_CODE,
|
||||
CONF_WARNING_LOW_PV_ENERGY,
|
||||
CONF_WARNUNG_LOW_PV_ENERGY,
|
||||
CONF_WARNING_HIGH_AC_INPUT_DURING_BUS_SOFT_START,
|
||||
CONF_WARNING_BATTERY_EQUALIZATION,
|
||||
]
|
||||
|
||||
@@ -13,7 +13,7 @@ void PipsolarOutput::write_state(float state) {
|
||||
|
||||
if (std::find(this->possible_values_.begin(), this->possible_values_.end(), state) != this->possible_values_.end()) {
|
||||
ESP_LOGD(TAG, "Will write: %s out of value %f / %02.0f", tmp, state, state);
|
||||
this->parent_->queue_command(std::string(tmp));
|
||||
this->parent_->switch_command(std::string(tmp));
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Will not write: %s as it is not in list of allowed values", tmp);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,6 @@
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace pipsolar {
|
||||
@@ -29,17 +28,10 @@ struct PollingCommand {
|
||||
bool needs_update;
|
||||
};
|
||||
|
||||
struct QFLAGValues {
|
||||
esphome::optional<bool> silence_buzzer_open_buzzer;
|
||||
esphome::optional<bool> overload_bypass_function;
|
||||
esphome::optional<bool> lcd_escape_to_default;
|
||||
esphome::optional<bool> overload_restart_function;
|
||||
esphome::optional<bool> over_temperature_restart_function;
|
||||
esphome::optional<bool> backlight_on;
|
||||
esphome::optional<bool> alarm_on_when_primary_source_interrupt;
|
||||
esphome::optional<bool> fault_code_record;
|
||||
esphome::optional<bool> power_saving;
|
||||
};
|
||||
#define PIPSOLAR_VALUED_ENTITY_(type, name, polling_command, value_type) \
|
||||
protected: \
|
||||
value_type value_##name##_; \
|
||||
PIPSOLAR_ENTITY_(type, name, polling_command)
|
||||
|
||||
#define PIPSOLAR_ENTITY_(type, name, polling_command) \
|
||||
protected: \
|
||||
@@ -51,123 +43,126 @@ struct QFLAGValues {
|
||||
this->add_polling_command_(#polling_command, POLLING_##polling_command); \
|
||||
}
|
||||
|
||||
#define PIPSOLAR_SENSOR(name, polling_command) PIPSOLAR_ENTITY_(sensor::Sensor, name, polling_command)
|
||||
#define PIPSOLAR_SENSOR(name, polling_command, value_type) \
|
||||
PIPSOLAR_VALUED_ENTITY_(sensor::Sensor, name, polling_command, value_type)
|
||||
#define PIPSOLAR_SWITCH(name, polling_command) PIPSOLAR_ENTITY_(switch_::Switch, name, polling_command)
|
||||
#define PIPSOLAR_BINARY_SENSOR(name, polling_command) \
|
||||
PIPSOLAR_ENTITY_(binary_sensor::BinarySensor, name, polling_command)
|
||||
#define PIPSOLAR_BINARY_SENSOR(name, polling_command, value_type) \
|
||||
PIPSOLAR_VALUED_ENTITY_(binary_sensor::BinarySensor, name, polling_command, value_type)
|
||||
#define PIPSOLAR_VALUED_TEXT_SENSOR(name, polling_command, value_type) \
|
||||
PIPSOLAR_VALUED_ENTITY_(text_sensor::TextSensor, name, polling_command, value_type)
|
||||
#define PIPSOLAR_TEXT_SENSOR(name, polling_command) PIPSOLAR_ENTITY_(text_sensor::TextSensor, name, polling_command)
|
||||
|
||||
class Pipsolar : public uart::UARTDevice, public PollingComponent {
|
||||
// QPIGS values
|
||||
PIPSOLAR_SENSOR(grid_voltage, QPIGS)
|
||||
PIPSOLAR_SENSOR(grid_frequency, QPIGS)
|
||||
PIPSOLAR_SENSOR(ac_output_voltage, QPIGS)
|
||||
PIPSOLAR_SENSOR(ac_output_frequency, QPIGS)
|
||||
PIPSOLAR_SENSOR(ac_output_apparent_power, QPIGS)
|
||||
PIPSOLAR_SENSOR(ac_output_active_power, QPIGS)
|
||||
PIPSOLAR_SENSOR(output_load_percent, QPIGS)
|
||||
PIPSOLAR_SENSOR(bus_voltage, QPIGS)
|
||||
PIPSOLAR_SENSOR(battery_voltage, QPIGS)
|
||||
PIPSOLAR_SENSOR(battery_charging_current, QPIGS)
|
||||
PIPSOLAR_SENSOR(battery_capacity_percent, QPIGS)
|
||||
PIPSOLAR_SENSOR(inverter_heat_sink_temperature, QPIGS)
|
||||
PIPSOLAR_SENSOR(pv_input_current_for_battery, QPIGS)
|
||||
PIPSOLAR_SENSOR(pv_input_voltage, QPIGS)
|
||||
PIPSOLAR_SENSOR(battery_voltage_scc, QPIGS)
|
||||
PIPSOLAR_SENSOR(battery_discharge_current, QPIGS)
|
||||
PIPSOLAR_BINARY_SENSOR(add_sbu_priority_version, QPIGS)
|
||||
PIPSOLAR_BINARY_SENSOR(configuration_status, QPIGS)
|
||||
PIPSOLAR_BINARY_SENSOR(scc_firmware_version, QPIGS)
|
||||
PIPSOLAR_BINARY_SENSOR(load_status, QPIGS)
|
||||
PIPSOLAR_BINARY_SENSOR(battery_voltage_to_steady_while_charging, QPIGS)
|
||||
PIPSOLAR_BINARY_SENSOR(charging_status, QPIGS)
|
||||
PIPSOLAR_BINARY_SENSOR(scc_charging_status, QPIGS)
|
||||
PIPSOLAR_BINARY_SENSOR(ac_charging_status, QPIGS)
|
||||
PIPSOLAR_SENSOR(battery_voltage_offset_for_fans_on, QPIGS) //.1 scale
|
||||
PIPSOLAR_SENSOR(eeprom_version, QPIGS)
|
||||
PIPSOLAR_SENSOR(pv_charging_power, QPIGS)
|
||||
PIPSOLAR_BINARY_SENSOR(charging_to_floating_mode, QPIGS)
|
||||
PIPSOLAR_BINARY_SENSOR(switch_on, QPIGS)
|
||||
PIPSOLAR_BINARY_SENSOR(dustproof_installed, QPIGS)
|
||||
PIPSOLAR_SENSOR(grid_voltage, QPIGS, float)
|
||||
PIPSOLAR_SENSOR(grid_frequency, QPIGS, float)
|
||||
PIPSOLAR_SENSOR(ac_output_voltage, QPIGS, float)
|
||||
PIPSOLAR_SENSOR(ac_output_frequency, QPIGS, float)
|
||||
PIPSOLAR_SENSOR(ac_output_apparent_power, QPIGS, int)
|
||||
PIPSOLAR_SENSOR(ac_output_active_power, QPIGS, int)
|
||||
PIPSOLAR_SENSOR(output_load_percent, QPIGS, int)
|
||||
PIPSOLAR_SENSOR(bus_voltage, QPIGS, int)
|
||||
PIPSOLAR_SENSOR(battery_voltage, QPIGS, float)
|
||||
PIPSOLAR_SENSOR(battery_charging_current, QPIGS, int)
|
||||
PIPSOLAR_SENSOR(battery_capacity_percent, QPIGS, int)
|
||||
PIPSOLAR_SENSOR(inverter_heat_sink_temperature, QPIGS, int)
|
||||
PIPSOLAR_SENSOR(pv_input_current_for_battery, QPIGS, float)
|
||||
PIPSOLAR_SENSOR(pv_input_voltage, QPIGS, float)
|
||||
PIPSOLAR_SENSOR(battery_voltage_scc, QPIGS, float)
|
||||
PIPSOLAR_SENSOR(battery_discharge_current, QPIGS, int)
|
||||
PIPSOLAR_BINARY_SENSOR(add_sbu_priority_version, QPIGS, int)
|
||||
PIPSOLAR_BINARY_SENSOR(configuration_status, QPIGS, int)
|
||||
PIPSOLAR_BINARY_SENSOR(scc_firmware_version, QPIGS, int)
|
||||
PIPSOLAR_BINARY_SENSOR(load_status, QPIGS, int)
|
||||
PIPSOLAR_BINARY_SENSOR(battery_voltage_to_steady_while_charging, QPIGS, int)
|
||||
PIPSOLAR_BINARY_SENSOR(charging_status, QPIGS, int)
|
||||
PIPSOLAR_BINARY_SENSOR(scc_charging_status, QPIGS, int)
|
||||
PIPSOLAR_BINARY_SENSOR(ac_charging_status, QPIGS, int)
|
||||
PIPSOLAR_SENSOR(battery_voltage_offset_for_fans_on, QPIGS, int) //.1 scale
|
||||
PIPSOLAR_SENSOR(eeprom_version, QPIGS, int)
|
||||
PIPSOLAR_SENSOR(pv_charging_power, QPIGS, int)
|
||||
PIPSOLAR_BINARY_SENSOR(charging_to_floating_mode, QPIGS, int)
|
||||
PIPSOLAR_BINARY_SENSOR(switch_on, QPIGS, int)
|
||||
PIPSOLAR_BINARY_SENSOR(dustproof_installed, QPIGS, int)
|
||||
|
||||
// QPIRI values
|
||||
PIPSOLAR_SENSOR(grid_rating_voltage, QPIRI)
|
||||
PIPSOLAR_SENSOR(grid_rating_current, QPIRI)
|
||||
PIPSOLAR_SENSOR(ac_output_rating_voltage, QPIRI)
|
||||
PIPSOLAR_SENSOR(ac_output_rating_frequency, QPIRI)
|
||||
PIPSOLAR_SENSOR(ac_output_rating_current, QPIRI)
|
||||
PIPSOLAR_SENSOR(ac_output_rating_apparent_power, QPIRI)
|
||||
PIPSOLAR_SENSOR(ac_output_rating_active_power, QPIRI)
|
||||
PIPSOLAR_SENSOR(battery_rating_voltage, QPIRI)
|
||||
PIPSOLAR_SENSOR(battery_recharge_voltage, QPIRI)
|
||||
PIPSOLAR_SENSOR(battery_under_voltage, QPIRI)
|
||||
PIPSOLAR_SENSOR(battery_bulk_voltage, QPIRI)
|
||||
PIPSOLAR_SENSOR(battery_float_voltage, QPIRI)
|
||||
PIPSOLAR_SENSOR(battery_type, QPIRI)
|
||||
PIPSOLAR_SENSOR(current_max_ac_charging_current, QPIRI)
|
||||
PIPSOLAR_SENSOR(current_max_charging_current, QPIRI)
|
||||
PIPSOLAR_SENSOR(input_voltage_range, QPIRI)
|
||||
PIPSOLAR_SENSOR(output_source_priority, QPIRI)
|
||||
PIPSOLAR_SENSOR(charger_source_priority, QPIRI)
|
||||
PIPSOLAR_SENSOR(parallel_max_num, QPIRI)
|
||||
PIPSOLAR_SENSOR(machine_type, QPIRI)
|
||||
PIPSOLAR_SENSOR(topology, QPIRI)
|
||||
PIPSOLAR_SENSOR(output_mode, QPIRI)
|
||||
PIPSOLAR_SENSOR(battery_redischarge_voltage, QPIRI)
|
||||
PIPSOLAR_SENSOR(pv_ok_condition_for_parallel, QPIRI)
|
||||
PIPSOLAR_SENSOR(pv_power_balance, QPIRI)
|
||||
PIPSOLAR_SENSOR(grid_rating_voltage, QPIRI, float)
|
||||
PIPSOLAR_SENSOR(grid_rating_current, QPIRI, float)
|
||||
PIPSOLAR_SENSOR(ac_output_rating_voltage, QPIRI, float)
|
||||
PIPSOLAR_SENSOR(ac_output_rating_frequency, QPIRI, float)
|
||||
PIPSOLAR_SENSOR(ac_output_rating_current, QPIRI, float)
|
||||
PIPSOLAR_SENSOR(ac_output_rating_apparent_power, QPIRI, int)
|
||||
PIPSOLAR_SENSOR(ac_output_rating_active_power, QPIRI, int)
|
||||
PIPSOLAR_SENSOR(battery_rating_voltage, QPIRI, float)
|
||||
PIPSOLAR_SENSOR(battery_recharge_voltage, QPIRI, float)
|
||||
PIPSOLAR_SENSOR(battery_under_voltage, QPIRI, float)
|
||||
PIPSOLAR_SENSOR(battery_bulk_voltage, QPIRI, float)
|
||||
PIPSOLAR_SENSOR(battery_float_voltage, QPIRI, float)
|
||||
PIPSOLAR_SENSOR(battery_type, QPIRI, int)
|
||||
PIPSOLAR_SENSOR(current_max_ac_charging_current, QPIRI, int)
|
||||
PIPSOLAR_SENSOR(current_max_charging_current, QPIRI, int)
|
||||
PIPSOLAR_SENSOR(input_voltage_range, QPIRI, int)
|
||||
PIPSOLAR_SENSOR(output_source_priority, QPIRI, int)
|
||||
PIPSOLAR_SENSOR(charger_source_priority, QPIRI, int)
|
||||
PIPSOLAR_SENSOR(parallel_max_num, QPIRI, int)
|
||||
PIPSOLAR_SENSOR(machine_type, QPIRI, int)
|
||||
PIPSOLAR_SENSOR(topology, QPIRI, int)
|
||||
PIPSOLAR_SENSOR(output_mode, QPIRI, int)
|
||||
PIPSOLAR_SENSOR(battery_redischarge_voltage, QPIRI, float)
|
||||
PIPSOLAR_SENSOR(pv_ok_condition_for_parallel, QPIRI, int)
|
||||
PIPSOLAR_SENSOR(pv_power_balance, QPIRI, int)
|
||||
|
||||
// QMOD values
|
||||
PIPSOLAR_TEXT_SENSOR(device_mode, QMOD)
|
||||
PIPSOLAR_VALUED_TEXT_SENSOR(device_mode, QMOD, char)
|
||||
|
||||
// QFLAG values
|
||||
PIPSOLAR_BINARY_SENSOR(silence_buzzer_open_buzzer, QFLAG)
|
||||
PIPSOLAR_BINARY_SENSOR(overload_bypass_function, QFLAG)
|
||||
PIPSOLAR_BINARY_SENSOR(lcd_escape_to_default, QFLAG)
|
||||
PIPSOLAR_BINARY_SENSOR(overload_restart_function, QFLAG)
|
||||
PIPSOLAR_BINARY_SENSOR(over_temperature_restart_function, QFLAG)
|
||||
PIPSOLAR_BINARY_SENSOR(backlight_on, QFLAG)
|
||||
PIPSOLAR_BINARY_SENSOR(alarm_on_when_primary_source_interrupt, QFLAG)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_code_record, QFLAG)
|
||||
PIPSOLAR_BINARY_SENSOR(power_saving, QFLAG)
|
||||
PIPSOLAR_BINARY_SENSOR(silence_buzzer_open_buzzer, QFLAG, int)
|
||||
PIPSOLAR_BINARY_SENSOR(overload_bypass_function, QFLAG, int)
|
||||
PIPSOLAR_BINARY_SENSOR(lcd_escape_to_default, QFLAG, int)
|
||||
PIPSOLAR_BINARY_SENSOR(overload_restart_function, QFLAG, int)
|
||||
PIPSOLAR_BINARY_SENSOR(over_temperature_restart_function, QFLAG, int)
|
||||
PIPSOLAR_BINARY_SENSOR(backlight_on, QFLAG, int)
|
||||
PIPSOLAR_BINARY_SENSOR(alarm_on_when_primary_source_interrupt, QFLAG, int)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_code_record, QFLAG, int)
|
||||
PIPSOLAR_BINARY_SENSOR(power_saving, QFLAG, int)
|
||||
|
||||
// QPIWS values
|
||||
PIPSOLAR_BINARY_SENSOR(warnings_present, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(faults_present, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_power_loss, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_inverter_fault, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_bus_over, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_bus_under, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_bus_soft_fail, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_line_fail, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_opvshort, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_inverter_voltage_too_low, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_inverter_voltage_too_high, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_over_temperature, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_fan_lock, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_battery_voltage_high, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_battery_low_alarm, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_battery_under_shutdown, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_battery_derating, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_over_load, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_eeprom_failed, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_inverter_over_current, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_inverter_soft_failed, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_self_test_failed, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_op_dc_voltage_over, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_battery_open, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_current_sensor_failed, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_battery_short, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_power_limit, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_pv_voltage_high, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_mppt_overload, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_mppt_overload, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_battery_too_low_to_charge, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_dc_dc_over_current, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_code, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_low_pv_energy, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_high_ac_input_during_bus_soft_start, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_battery_equalization, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(warnings_present, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(faults_present, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_power_loss, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_inverter_fault, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_bus_over, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_bus_under, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_bus_soft_fail, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_line_fail, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_opvshort, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_inverter_voltage_too_low, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_inverter_voltage_too_high, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_over_temperature, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_fan_lock, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_battery_voltage_high, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_battery_low_alarm, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_battery_under_shutdown, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_battery_derating, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_over_load, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_eeprom_failed, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_inverter_over_current, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_inverter_soft_failed, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_self_test_failed, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_op_dc_voltage_over, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_battery_open, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_current_sensor_failed, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_battery_short, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_power_limit, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_pv_voltage_high, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_mppt_overload, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_mppt_overload, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_battery_too_low_to_charge, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_dc_dc_over_current, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_code, QPIWS, int)
|
||||
PIPSOLAR_BINARY_SENSOR(warnung_low_pv_energy, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_high_ac_input_during_bus_soft_start, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_battery_equalization, QPIWS, bool)
|
||||
|
||||
PIPSOLAR_TEXT_SENSOR(last_qpigs, QPIGS)
|
||||
PIPSOLAR_TEXT_SENSOR(last_qpiri, QPIRI)
|
||||
@@ -185,14 +180,14 @@ class Pipsolar : public uart::UARTDevice, public PollingComponent {
|
||||
PIPSOLAR_SWITCH(pv_ok_condition_for_parallel_switch, QPIRI)
|
||||
PIPSOLAR_SWITCH(pv_power_balance_switch, QPIRI)
|
||||
|
||||
void queue_command(const std::string &command);
|
||||
void switch_command(const std::string &command);
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
void update() override;
|
||||
|
||||
protected:
|
||||
static const size_t PIPSOLAR_READ_BUFFER_LENGTH = 128; // maximum supported answer length
|
||||
static const size_t PIPSOLAR_READ_BUFFER_LENGTH = 110; // maximum supported answer length
|
||||
static const size_t COMMAND_QUEUE_LENGTH = 10;
|
||||
static const size_t COMMAND_TIMEOUT = 5000;
|
||||
static const size_t POLLING_COMMANDS_MAX = 15;
|
||||
@@ -203,26 +198,7 @@ class Pipsolar : public uart::UARTDevice, public PollingComponent {
|
||||
uint16_t pipsolar_crc_(uint8_t *msg, uint8_t len);
|
||||
bool send_next_command_();
|
||||
bool send_next_poll_();
|
||||
|
||||
void handle_qpiri_(const char *message);
|
||||
void handle_qpigs_(const char *message);
|
||||
void handle_qmod_(const char *message);
|
||||
void handle_qflag_(const char *message);
|
||||
void handle_qpiws_(const char *message);
|
||||
void handle_qt_(const char *message);
|
||||
void handle_qmn_(const char *message);
|
||||
|
||||
void skip_start_(const char *message, size_t *pos);
|
||||
void skip_field_(const char *message, size_t *pos);
|
||||
std::string read_field_(const char *message, size_t *pos);
|
||||
|
||||
void read_float_sensor_(const char *message, size_t *pos, sensor::Sensor *sensor);
|
||||
void read_int_sensor_(const char *message, size_t *pos, sensor::Sensor *sensor);
|
||||
|
||||
void publish_binary_sensor_(esphome::optional<bool> b, binary_sensor::BinarySensor *sensor);
|
||||
|
||||
esphome::optional<bool> get_bit_(std::string bits, uint8_t bit_pos);
|
||||
|
||||
void queue_command_(const char *command, uint8_t length);
|
||||
std::string command_queue_[COMMAND_QUEUE_LENGTH];
|
||||
uint8_t command_queue_position_ = 0;
|
||||
uint8_t read_buffer_[PIPSOLAR_READ_BUFFER_LENGTH];
|
||||
@@ -237,10 +213,11 @@ class Pipsolar : public uart::UARTDevice, public PollingComponent {
|
||||
STATE_POLL_COMPLETE = 3,
|
||||
STATE_COMMAND_COMPLETE = 4,
|
||||
STATE_POLL_CHECKED = 5,
|
||||
STATE_POLL_DECODED = 6,
|
||||
};
|
||||
|
||||
uint8_t last_polling_command_ = 0;
|
||||
PollingCommand enabled_polling_commands_[POLLING_COMMANDS_MAX];
|
||||
PollingCommand used_polling_commands_[POLLING_COMMANDS_MAX];
|
||||
};
|
||||
|
||||
} // namespace pipsolar
|
||||
|
||||
@@ -11,11 +11,11 @@ void PipsolarSwitch::dump_config() { LOG_SWITCH("", "Pipsolar Switch", this); }
|
||||
void PipsolarSwitch::write_state(bool state) {
|
||||
if (state) {
|
||||
if (!this->on_command_.empty()) {
|
||||
this->parent_->queue_command(this->on_command_);
|
||||
this->parent_->switch_command(this->on_command_);
|
||||
}
|
||||
} else {
|
||||
if (!this->off_command_.empty()) {
|
||||
this->parent_->queue_command(this->off_command_);
|
||||
this->parent_->switch_command(this->off_command_);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,26 +45,13 @@ 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 'queued' and 'parallel' mode.",
|
||||
"The option 'max_runs' is only valid in 'queue' 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
|
||||
|
||||
|
||||
@@ -119,7 +106,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.int_range(min=0, max=100),
|
||||
cv.Optional(CONF_MAX_RUNS): cv.positive_int,
|
||||
cv.Optional(CONF_PARAMETERS, default={}): cv.Schema(
|
||||
{
|
||||
validate_parameter_name: validate_parameter_type,
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
#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 {
|
||||
|
||||
@@ -97,41 +96,23 @@ 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_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!"),
|
||||
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!"),
|
||||
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_));
|
||||
// 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_++;
|
||||
this->num_runs_++;
|
||||
this->var_queue_.push(std::make_tuple(x...));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -141,46 +122,29 @@ template<typename... Ts> class QueueingScript : public Script<Ts...>, public Com
|
||||
}
|
||||
|
||||
void stop() override {
|
||||
// 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;
|
||||
this->num_runs_ = 0;
|
||||
Script<Ts...>::stop();
|
||||
}
|
||||
|
||||
void loop() override {
|
||||
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());
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
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_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
|
||||
int num_runs_ = 0;
|
||||
int max_runs_ = 0;
|
||||
std::queue<std::tuple<Ts...>> var_queue_;
|
||||
};
|
||||
|
||||
/** A script type that executes new instances in parallel.
|
||||
|
||||
@@ -251,9 +251,6 @@ 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
|
||||
)
|
||||
@@ -261,12 +258,9 @@ ThrottleAverageFilter = sensor_ns.class_("ThrottleAverageFilter", Filter, cg.Com
|
||||
LambdaFilter = sensor_ns.class_("LambdaFilter", Filter)
|
||||
OffsetFilter = sensor_ns.class_("OffsetFilter", Filter)
|
||||
MultiplyFilter = sensor_ns.class_("MultiplyFilter", Filter)
|
||||
ValueListFilter = sensor_ns.class_("ValueListFilter", Filter)
|
||||
FilterOutValueFilter = sensor_ns.class_("FilterOutValueFilter", ValueListFilter)
|
||||
FilterOutValueFilter = sensor_ns.class_("FilterOutValueFilter", Filter)
|
||||
ThrottleFilter = sensor_ns.class_("ThrottleFilter", Filter)
|
||||
ThrottleWithPriorityFilter = sensor_ns.class_(
|
||||
"ThrottleWithPriorityFilter", ValueListFilter
|
||||
)
|
||||
ThrottleWithPriorityFilter = sensor_ns.class_("ThrottleWithPriorityFilter", Filter)
|
||||
TimeoutFilter = sensor_ns.class_("TimeoutFilter", Filter, cg.Component)
|
||||
DebounceFilter = sensor_ns.class_("DebounceFilter", Filter, cg.Component)
|
||||
HeartbeatFilter = sensor_ns.class_("HeartbeatFilter", Filter, cg.Component)
|
||||
@@ -458,21 +452,14 @@ async def skip_initial_filter_to_code(config, filter_id):
|
||||
return cg.new_Pvariable(filter_id, config)
|
||||
|
||||
|
||||
@FILTER_REGISTRY.register("min", Filter, MIN_SCHEMA)
|
||||
@FILTER_REGISTRY.register("min", MinFilter, MIN_SCHEMA)
|
||||
async def min_filter_to_code(config, filter_id):
|
||||
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)
|
||||
return cg.new_Pvariable(
|
||||
filter_id,
|
||||
config[CONF_WINDOW_SIZE],
|
||||
config[CONF_SEND_EVERY],
|
||||
config[CONF_SEND_FIRST_AT],
|
||||
)
|
||||
|
||||
|
||||
MAX_SCHEMA = cv.All(
|
||||
@@ -487,18 +474,14 @@ MAX_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
@FILTER_REGISTRY.register("max", Filter, MAX_SCHEMA)
|
||||
@FILTER_REGISTRY.register("max", MaxFilter, MAX_SCHEMA)
|
||||
async def max_filter_to_code(config, filter_id):
|
||||
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)
|
||||
return cg.new_Pvariable(
|
||||
filter_id,
|
||||
config[CONF_WINDOW_SIZE],
|
||||
config[CONF_SEND_EVERY],
|
||||
config[CONF_SEND_FIRST_AT],
|
||||
)
|
||||
|
||||
|
||||
SLIDING_AVERAGE_SCHEMA = cv.All(
|
||||
@@ -515,20 +498,16 @@ SLIDING_AVERAGE_SCHEMA = cv.All(
|
||||
|
||||
@FILTER_REGISTRY.register(
|
||||
"sliding_window_moving_average",
|
||||
Filter,
|
||||
SlidingWindowMovingAverageFilter,
|
||||
SLIDING_AVERAGE_SCHEMA,
|
||||
)
|
||||
async def sliding_window_moving_average_filter_to_code(config, filter_id):
|
||||
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)
|
||||
return cg.new_Pvariable(
|
||||
filter_id,
|
||||
config[CONF_WINDOW_SIZE],
|
||||
config[CONF_SEND_EVERY],
|
||||
config[CONF_SEND_FIRST_AT],
|
||||
)
|
||||
|
||||
|
||||
EXPONENTIAL_AVERAGE_SCHEMA = cv.All(
|
||||
|
||||
@@ -32,76 +32,50 @@ void Filter::initialize(Sensor *parent, Filter *next) {
|
||||
this->next_ = next;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
// 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);
|
||||
|
||||
// Check if we should send a result
|
||||
if (++this->send_at_ >= this->send_every_) {
|
||||
this->send_at_ = 0;
|
||||
float result = this->compute_result();
|
||||
ESP_LOGVV(TAG, "SlidingWindowFilter(%p)::new_value(%f) SENDING %f", this, value, result);
|
||||
return result;
|
||||
|
||||
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;
|
||||
}
|
||||
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) {
|
||||
@@ -117,39 +91,136 @@ 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)
|
||||
: SortedWindowFilter(window_size, send_every, send_first_at), quantile_(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_);
|
||||
|
||||
float QuantileFilter::compute_result() {
|
||||
FixedVector<float> values = this->get_window_values_();
|
||||
if (values.empty())
|
||||
return NAN;
|
||||
if (++this->send_at_ >= this->send_every_) {
|
||||
this->send_at_ = 0;
|
||||
|
||||
size_t position = ceilf(values.size() * this->quantile_) - 1;
|
||||
ESP_LOGVV(TAG, "QuantileFilter(%p)::position: %zu/%zu", this, position + 1, values.size());
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// 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];
|
||||
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 {};
|
||||
}
|
||||
|
||||
// MinFilter
|
||||
float MinFilter::compute_result() { return this->find_extremum_<std::less<float>>(); }
|
||||
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 {};
|
||||
}
|
||||
|
||||
// MaxFilter
|
||||
float MaxFilter::compute_result() { return this->find_extremum_<std::greater<float>>(); }
|
||||
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 {};
|
||||
}
|
||||
|
||||
// SlidingWindowMovingAverageFilter
|
||||
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++;
|
||||
}
|
||||
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();
|
||||
}
|
||||
return valid_count ? sum / valid_count : NAN;
|
||||
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 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 {};
|
||||
}
|
||||
|
||||
// ExponentialMovingAverageFilter
|
||||
@@ -228,40 +299,27 @@ MultiplyFilter::MultiplyFilter(TemplatableValue<float> multiplier) : multiplier_
|
||||
|
||||
optional<float> MultiplyFilter::new_value(float value) { return value * this->multiplier_.value(); }
|
||||
|
||||
// ValueListFilter (base class)
|
||||
ValueListFilter::ValueListFilter(std::initializer_list<TemplatableValue<float>> values) : values_(values) {}
|
||||
|
||||
bool ValueListFilter::value_matches_any_(float sensor_value) {
|
||||
int8_t accuracy = this->parent_->get_accuracy_decimals();
|
||||
float accuracy_mult = powf(10.0f, accuracy);
|
||||
float rounded_sensor = roundf(accuracy_mult * sensor_value);
|
||||
|
||||
for (auto &filter_value : this->values_) {
|
||||
float fv = filter_value.value();
|
||||
|
||||
// Handle NaN comparison
|
||||
if (std::isnan(fv)) {
|
||||
if (std::isnan(sensor_value))
|
||||
return true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Compare rounded values
|
||||
if (roundf(accuracy_mult * fv) == rounded_sensor)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// FilterOutValueFilter
|
||||
FilterOutValueFilter::FilterOutValueFilter(std::initializer_list<TemplatableValue<float>> values_to_filter_out)
|
||||
: ValueListFilter(values_to_filter_out) {}
|
||||
FilterOutValueFilter::FilterOutValueFilter(std::vector<TemplatableValue<float>> values_to_filter_out)
|
||||
: values_to_filter_out_(std::move(values_to_filter_out)) {}
|
||||
|
||||
optional<float> FilterOutValueFilter::new_value(float value) {
|
||||
if (this->value_matches_any_(value))
|
||||
return {}; // Filter out
|
||||
return value; // Pass through
|
||||
int8_t accuracy = this->parent_->get_accuracy_decimals();
|
||||
float accuracy_mult = powf(10.0f, accuracy);
|
||||
for (auto filter_value : this->values_to_filter_out_) {
|
||||
if (std::isnan(filter_value.value())) {
|
||||
if (std::isnan(value)) {
|
||||
return {};
|
||||
}
|
||||
continue;
|
||||
}
|
||||
float rounded_filter_out = roundf(accuracy_mult * filter_value.value());
|
||||
float rounded_value = roundf(accuracy_mult * value);
|
||||
if (rounded_filter_out == rounded_value) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// ThrottleFilter
|
||||
@@ -276,15 +334,33 @@ optional<float> ThrottleFilter::new_value(float value) {
|
||||
}
|
||||
|
||||
// ThrottleWithPriorityFilter
|
||||
ThrottleWithPriorityFilter::ThrottleWithPriorityFilter(
|
||||
uint32_t min_time_between_inputs, std::initializer_list<TemplatableValue<float>> prioritized_values)
|
||||
: ValueListFilter(prioritized_values), min_time_between_inputs_(min_time_between_inputs) {}
|
||||
ThrottleWithPriorityFilter::ThrottleWithPriorityFilter(uint32_t min_time_between_inputs,
|
||||
std::vector<TemplatableValue<float>> prioritized_values)
|
||||
: min_time_between_inputs_(min_time_between_inputs), prioritized_values_(std::move(prioritized_values)) {}
|
||||
|
||||
optional<float> ThrottleWithPriorityFilter::new_value(float value) {
|
||||
bool is_prioritized_value = false;
|
||||
int8_t accuracy = this->parent_->get_accuracy_decimals();
|
||||
float accuracy_mult = powf(10.0f, accuracy);
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
// Allow value through if: no previous input, time expired, or is prioritized
|
||||
if (this->last_input_ == 0 || now - this->last_input_ >= min_time_between_inputs_ ||
|
||||
this->value_matches_any_(value)) {
|
||||
// First, determine if the new value is one of the prioritized values
|
||||
for (auto prioritized_value : this->prioritized_values_) {
|
||||
if (std::isnan(prioritized_value.value())) {
|
||||
if (std::isnan(value)) {
|
||||
is_prioritized_value = true;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
float rounded_prioritized_value = roundf(accuracy_mult * prioritized_value.value());
|
||||
float rounded_value = roundf(accuracy_mult * value);
|
||||
if (rounded_prioritized_value == rounded_value) {
|
||||
is_prioritized_value = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Finally, determine if the new value should be throttled and pass it through if not
|
||||
if (this->last_input_ == 0 || now - this->last_input_ >= min_time_between_inputs_ || is_prioritized_value) {
|
||||
this->last_input_ = now;
|
||||
return value;
|
||||
}
|
||||
@@ -467,78 +543,5 @@ 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
|
||||
|
||||
@@ -44,75 +44,11 @@ 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 <window_size> values and pushes it out every <send_every>.
|
||||
* Takes the quantile of the last <send_every> values and pushes it out every <send_every>.
|
||||
*/
|
||||
class QuantileFilter : public SortedWindowFilter {
|
||||
class QuantileFilter : public Filter {
|
||||
public:
|
||||
/** Construct a QuantileFilter.
|
||||
*
|
||||
@@ -125,18 +61,25 @@ class QuantileFilter : public SortedWindowFilter {
|
||||
*/
|
||||
explicit QuantileFilter(size_t window_size, size_t send_every, size_t send_first_at, float quantile);
|
||||
|
||||
void set_quantile(float quantile) { this->quantile_ = 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);
|
||||
|
||||
protected:
|
||||
float compute_result() override;
|
||||
std::deque<float> queue_;
|
||||
size_t send_every_;
|
||||
size_t send_at_;
|
||||
size_t window_size_;
|
||||
float quantile_;
|
||||
};
|
||||
|
||||
/** Simple median filter.
|
||||
*
|
||||
* Takes the median of the last <window_size> values and pushes it out every <send_every>.
|
||||
* Takes the median of the last <send_every> values and pushes it out every <send_every>.
|
||||
*/
|
||||
class MedianFilter : public SortedWindowFilter {
|
||||
class MedianFilter : public Filter {
|
||||
public:
|
||||
/** Construct a MedianFilter.
|
||||
*
|
||||
@@ -146,10 +89,18 @@ class MedianFilter : public SortedWindowFilter {
|
||||
* on startup being published on the first *raw* value, so with no filter applied. Must be less than or equal to
|
||||
* send_every.
|
||||
*/
|
||||
using SortedWindowFilter::SortedWindowFilter;
|
||||
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);
|
||||
|
||||
protected:
|
||||
float compute_result() override;
|
||||
std::deque<float> queue_;
|
||||
size_t send_every_;
|
||||
size_t send_at_;
|
||||
size_t window_size_;
|
||||
};
|
||||
|
||||
/** Simple skip filter.
|
||||
@@ -172,9 +123,9 @@ class SkipInitialFilter : public Filter {
|
||||
|
||||
/** Simple min filter.
|
||||
*
|
||||
* Takes the min of the last <window_size> values and pushes it out every <send_every>.
|
||||
* Takes the min of the last <send_every> values and pushes it out every <send_every>.
|
||||
*/
|
||||
class MinFilter : public MinMaxFilter {
|
||||
class MinFilter : public Filter {
|
||||
public:
|
||||
/** Construct a MinFilter.
|
||||
*
|
||||
@@ -184,17 +135,25 @@ class MinFilter : public MinMaxFilter {
|
||||
* on startup being published on the first *raw* value, so with no filter applied. Must be less than or equal to
|
||||
* send_every.
|
||||
*/
|
||||
using MinMaxFilter::MinMaxFilter;
|
||||
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);
|
||||
|
||||
protected:
|
||||
float compute_result() override;
|
||||
std::deque<float> queue_;
|
||||
size_t send_every_;
|
||||
size_t send_at_;
|
||||
size_t window_size_;
|
||||
};
|
||||
|
||||
/** Simple max filter.
|
||||
*
|
||||
* Takes the max of the last <window_size> values and pushes it out every <send_every>.
|
||||
* Takes the max of the last <send_every> values and pushes it out every <send_every>.
|
||||
*/
|
||||
class MaxFilter : public MinMaxFilter {
|
||||
class MaxFilter : public Filter {
|
||||
public:
|
||||
/** Construct a MaxFilter.
|
||||
*
|
||||
@@ -204,10 +163,18 @@ class MaxFilter : public MinMaxFilter {
|
||||
* on startup being published on the first *raw* value, so with no filter applied. Must be less than or equal to
|
||||
* send_every.
|
||||
*/
|
||||
using MinMaxFilter::MinMaxFilter;
|
||||
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);
|
||||
|
||||
protected:
|
||||
float compute_result() override;
|
||||
std::deque<float> queue_;
|
||||
size_t send_every_;
|
||||
size_t send_at_;
|
||||
size_t window_size_;
|
||||
};
|
||||
|
||||
/** Simple sliding window moving average filter.
|
||||
@@ -215,7 +182,7 @@ class MaxFilter : public MinMaxFilter {
|
||||
* Essentially just takes takes the average of the last window_size values and pushes them out
|
||||
* every send_every.
|
||||
*/
|
||||
class SlidingWindowMovingAverageFilter : public SlidingWindowFilter {
|
||||
class SlidingWindowMovingAverageFilter : public Filter {
|
||||
public:
|
||||
/** Construct a SlidingWindowMovingAverageFilter.
|
||||
*
|
||||
@@ -225,10 +192,18 @@ class SlidingWindowMovingAverageFilter : public SlidingWindowFilter {
|
||||
* on startup being published on the first *raw* value, so with no filter applied. Must be less than or equal to
|
||||
* send_every.
|
||||
*/
|
||||
using SlidingWindowFilter::SlidingWindowFilter;
|
||||
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);
|
||||
|
||||
protected:
|
||||
float compute_result() override;
|
||||
std::deque<float> queue_;
|
||||
size_t send_every_;
|
||||
size_t send_at_;
|
||||
size_t window_size_;
|
||||
};
|
||||
|
||||
/** Simple exponential moving average filter.
|
||||
@@ -317,28 +292,15 @@ class MultiplyFilter : public Filter {
|
||||
TemplatableValue<float> multiplier_;
|
||||
};
|
||||
|
||||
/** Base class for filters that compare sensor values against a list of configured values.
|
||||
*
|
||||
* This base class provides common functionality for filters that need to check if a sensor
|
||||
* value matches any value in a configured list, with proper handling of NaN values and
|
||||
* accuracy-based rounding for comparisons.
|
||||
*/
|
||||
class ValueListFilter : public Filter {
|
||||
protected:
|
||||
explicit ValueListFilter(std::initializer_list<TemplatableValue<float>> values);
|
||||
|
||||
/// Check if sensor value matches any configured value (with accuracy rounding)
|
||||
bool value_matches_any_(float sensor_value);
|
||||
|
||||
FixedVector<TemplatableValue<float>> values_;
|
||||
};
|
||||
|
||||
/// A simple filter that only forwards the filter chain if it doesn't receive `value_to_filter_out`.
|
||||
class FilterOutValueFilter : public ValueListFilter {
|
||||
class FilterOutValueFilter : public Filter {
|
||||
public:
|
||||
explicit FilterOutValueFilter(std::initializer_list<TemplatableValue<float>> values_to_filter_out);
|
||||
explicit FilterOutValueFilter(std::vector<TemplatableValue<float>> values_to_filter_out);
|
||||
|
||||
optional<float> new_value(float value) override;
|
||||
|
||||
protected:
|
||||
std::vector<TemplatableValue<float>> values_to_filter_out_;
|
||||
};
|
||||
|
||||
class ThrottleFilter : public Filter {
|
||||
@@ -353,16 +315,17 @@ class ThrottleFilter : public Filter {
|
||||
};
|
||||
|
||||
/// Same as 'throttle' but will immediately publish values contained in `value_to_prioritize`.
|
||||
class ThrottleWithPriorityFilter : public ValueListFilter {
|
||||
class ThrottleWithPriorityFilter : public Filter {
|
||||
public:
|
||||
explicit ThrottleWithPriorityFilter(uint32_t min_time_between_inputs,
|
||||
std::initializer_list<TemplatableValue<float>> prioritized_values);
|
||||
std::vector<TemplatableValue<float>> prioritized_values);
|
||||
|
||||
optional<float> new_value(float value) override;
|
||||
|
||||
protected:
|
||||
uint32_t last_input_{0};
|
||||
uint32_t min_time_between_inputs_;
|
||||
std::vector<TemplatableValue<float>> prioritized_values_;
|
||||
};
|
||||
|
||||
class TimeoutFilter : public Filter, public Component {
|
||||
@@ -513,81 +476,5 @@ 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
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from collections.abc import Callable, MutableMapping
|
||||
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.core import CORE
|
||||
@@ -11,32 +9,6 @@ IMPLEMENTATION_LWIP_TCP = "lwip_tcp"
|
||||
IMPLEMENTATION_LWIP_SOCKETS = "lwip_sockets"
|
||||
IMPLEMENTATION_BSD_SOCKETS = "bsd_sockets"
|
||||
|
||||
# Socket tracking infrastructure
|
||||
# Components register their socket needs and platforms read this to configure appropriately
|
||||
KEY_SOCKET_CONSUMERS = "socket_consumers"
|
||||
|
||||
|
||||
def consume_sockets(
|
||||
value: int, consumer: str
|
||||
) -> Callable[[MutableMapping], MutableMapping]:
|
||||
"""Register socket usage for a component.
|
||||
|
||||
Args:
|
||||
value: Number of sockets needed by the component
|
||||
consumer: Name of the component consuming the sockets
|
||||
|
||||
Returns:
|
||||
A validator function that records the socket usage
|
||||
"""
|
||||
|
||||
def _consume_sockets(config: MutableMapping) -> MutableMapping:
|
||||
consumers: dict[str, int] = CORE.data.setdefault(KEY_SOCKET_CONSUMERS, {})
|
||||
consumers[consumer] = consumers.get(consumer, 0) + value
|
||||
return config
|
||||
|
||||
return _consume_sockets
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.SplitDefault(
|
||||
|
||||
@@ -6,7 +6,7 @@ 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
|
||||
|
||||
from .jinja import Jinja, JinjaError, JinjaStr, has_jinja
|
||||
from .jinja import Jinja, JinjaStr, TemplateError, TemplateRuntimeError, has_jinja
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -57,12 +57,17 @@ def _expand_jinja(value, orig_value, path, jinja, ignore_missing):
|
||||
"->".join(str(x) for x in path),
|
||||
err.message,
|
||||
)
|
||||
except JinjaError as err:
|
||||
except (
|
||||
TemplateError,
|
||||
TemplateRuntimeError,
|
||||
RuntimeError,
|
||||
ArithmeticError,
|
||||
AttributeError,
|
||||
TypeError,
|
||||
) as err:
|
||||
raise cv.Invalid(
|
||||
f"{err.error_name()} Error evaluating jinja expression '{value}': {str(err.parent())}."
|
||||
f"\nEvaluation stack: (most recent evaluation last)\n{err.stack_trace_str()}"
|
||||
f"\nRelevant context:\n{err.context_trace_str()}"
|
||||
f"\nSee {'->'.join(str(x) for x in path)}",
|
||||
f"{type(err).__name__} Error evaluating jinja expression '{value}': {str(err)}."
|
||||
f" See {'->'.join(str(x) for x in path)}",
|
||||
path,
|
||||
)
|
||||
return value
|
||||
|
||||
@@ -6,8 +6,6 @@ import re
|
||||
import jinja2 as jinja
|
||||
from jinja2.sandbox import SandboxedEnvironment
|
||||
|
||||
from esphome.yaml_util import ESPLiteralValue
|
||||
|
||||
TemplateError = jinja.TemplateError
|
||||
TemplateSyntaxError = jinja.TemplateSyntaxError
|
||||
TemplateRuntimeError = jinja.TemplateRuntimeError
|
||||
@@ -28,20 +26,18 @@ def has_jinja(st):
|
||||
return detect_jinja_re.search(st) is not None
|
||||
|
||||
|
||||
# SAFE_GLOBALS defines a allowlist of built-in functions or modules that are considered safe to expose
|
||||
# SAFE_GLOBAL_FUNCTIONS defines a allowlist of built-in functions that are considered safe to expose
|
||||
# in Jinja templates or other sandboxed evaluation contexts. Only functions that do not allow
|
||||
# arbitrary code execution, file access, or other security risks are included.
|
||||
#
|
||||
# The following functions are considered safe:
|
||||
# - math: The entire math module is injected, allowing access to mathematical functions like sin, cos, sqrt, etc.
|
||||
# - ord: Converts a character to its Unicode code point integer.
|
||||
# - chr: Converts an integer to its corresponding Unicode character.
|
||||
# - len: Returns the length of a sequence or collection.
|
||||
#
|
||||
# These functions were chosen because they are pure, have no side effects, and do not provide access
|
||||
# to the file system, environment, or other potentially sensitive resources.
|
||||
SAFE_GLOBALS = {
|
||||
"math": math, # Inject entire math module
|
||||
SAFE_GLOBAL_FUNCTIONS = {
|
||||
"ord": ord,
|
||||
"chr": chr,
|
||||
"len": len,
|
||||
@@ -60,62 +56,22 @@ class JinjaStr(str):
|
||||
later in the main substitutions pass.
|
||||
"""
|
||||
|
||||
Undefined = object()
|
||||
|
||||
def __new__(cls, value: str, upvalues=None):
|
||||
if isinstance(value, JinjaStr):
|
||||
base = str(value)
|
||||
merged = {**value.upvalues, **(upvalues or {})}
|
||||
else:
|
||||
base = value
|
||||
merged = dict(upvalues or {})
|
||||
obj = super().__new__(cls, base)
|
||||
obj.upvalues = merged
|
||||
obj.result = JinjaStr.Undefined
|
||||
obj = super().__new__(cls, value)
|
||||
obj.upvalues = upvalues or {}
|
||||
return obj
|
||||
|
||||
|
||||
class JinjaError(Exception):
|
||||
def __init__(self, context_trace: dict, expr: str):
|
||||
self.context_trace = context_trace
|
||||
self.eval_stack = [expr]
|
||||
|
||||
def parent(self):
|
||||
return self.__context__
|
||||
|
||||
def error_name(self):
|
||||
return type(self.parent()).__name__
|
||||
|
||||
def context_trace_str(self):
|
||||
return "\n".join(
|
||||
f" {k} = {repr(v)} ({type(v).__name__})"
|
||||
for k, v in self.context_trace.items()
|
||||
)
|
||||
|
||||
def stack_trace_str(self):
|
||||
return "\n".join(
|
||||
f" {len(self.eval_stack) - i}: {expr}{i == 0 and ' <-- ' + self.error_name() or ''}"
|
||||
for i, expr in enumerate(self.eval_stack)
|
||||
)
|
||||
def __init__(self, value: str, upvalues=None):
|
||||
self.upvalues = upvalues or {}
|
||||
|
||||
|
||||
class TrackerContext(jinja.runtime.Context):
|
||||
def resolve_or_missing(self, key):
|
||||
val = super().resolve_or_missing(key)
|
||||
if isinstance(val, JinjaStr):
|
||||
self.environment.context_trace[key] = val
|
||||
val, _ = self.environment.expand(val)
|
||||
self.environment.context_trace[key] = val
|
||||
return val
|
||||
|
||||
|
||||
class Jinja(SandboxedEnvironment):
|
||||
class Jinja:
|
||||
"""
|
||||
Wraps a Jinja environment
|
||||
"""
|
||||
|
||||
def __init__(self, context_vars):
|
||||
super().__init__(
|
||||
self.env = SandboxedEnvironment(
|
||||
trim_blocks=True,
|
||||
lstrip_blocks=True,
|
||||
block_start_string="<%",
|
||||
@@ -126,20 +82,13 @@ class Jinja(SandboxedEnvironment):
|
||||
variable_end_string="}",
|
||||
undefined=jinja.StrictUndefined,
|
||||
)
|
||||
self.context_class = TrackerContext
|
||||
self.add_extension("jinja2.ext.do")
|
||||
self.context_trace = {}
|
||||
self.env.add_extension("jinja2.ext.do")
|
||||
self.env.globals["math"] = math # Inject entire math module
|
||||
self.context_vars = {**context_vars}
|
||||
for k, v in self.context_vars.items():
|
||||
if isinstance(v, ESPLiteralValue):
|
||||
continue
|
||||
if isinstance(v, str) and not isinstance(v, JinjaStr) and has_jinja(v):
|
||||
self.context_vars[k] = JinjaStr(v, self.context_vars)
|
||||
|
||||
self.globals = {
|
||||
**self.globals,
|
||||
self.env.globals = {
|
||||
**self.env.globals,
|
||||
**self.context_vars,
|
||||
**SAFE_GLOBALS,
|
||||
**SAFE_GLOBAL_FUNCTIONS,
|
||||
}
|
||||
|
||||
def safe_eval(self, expr):
|
||||
@@ -161,43 +110,23 @@ class Jinja(SandboxedEnvironment):
|
||||
result = None
|
||||
override_vars = {}
|
||||
if isinstance(content_str, JinjaStr):
|
||||
if content_str.result is not JinjaStr.Undefined:
|
||||
return content_str.result, None
|
||||
# If `value` is already a JinjaStr, it means we are trying to evaluate it again
|
||||
# in a parent pass.
|
||||
# Hopefully, all required variables are visible now.
|
||||
override_vars = content_str.upvalues
|
||||
|
||||
old_trace = self.context_trace
|
||||
self.context_trace = {}
|
||||
try:
|
||||
template = self.from_string(content_str)
|
||||
template = self.env.from_string(content_str)
|
||||
result = self.safe_eval(template.render(override_vars))
|
||||
if isinstance(result, Undefined):
|
||||
print("" + result) # force a UndefinedError exception
|
||||
# This happens when the expression is simply an undefined variable. Jinja does not
|
||||
# raise an exception, instead we get "Undefined".
|
||||
# Trigger an UndefinedError exception so we skip to below.
|
||||
print("" + result)
|
||||
except (TemplateSyntaxError, UndefinedError) as err:
|
||||
# `content_str` contains a Jinja expression that refers to a variable that is undefined
|
||||
# in this scope. Perhaps it refers to a root substitution that is not visible yet.
|
||||
# Therefore, return `content_str` as a JinjaStr, which contains the variables
|
||||
# Therefore, return the original `content_str` as a JinjaStr, which contains the variables
|
||||
# that are actually visible to it at this point to postpone evaluation.
|
||||
return JinjaStr(content_str, {**self.context_vars, **override_vars}), err
|
||||
except JinjaError as err:
|
||||
err.context_trace = {**self.context_trace, **err.context_trace}
|
||||
err.eval_stack.append(content_str)
|
||||
raise err
|
||||
except (
|
||||
TemplateError,
|
||||
TemplateRuntimeError,
|
||||
RuntimeError,
|
||||
ArithmeticError,
|
||||
AttributeError,
|
||||
TypeError,
|
||||
) as err:
|
||||
raise JinjaError(self.context_trace, content_str) from err
|
||||
finally:
|
||||
self.context_trace = old_trace
|
||||
|
||||
if isinstance(content_str, JinjaStr):
|
||||
content_str.result = result
|
||||
|
||||
return result, None
|
||||
|
||||
@@ -27,14 +27,6 @@ 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
|
||||
|
||||
@@ -7,13 +7,24 @@ namespace touchscreen {
|
||||
|
||||
static const char *const TAG = "touchscreen";
|
||||
|
||||
void TouchscreenInterrupt::gpio_intr(TouchscreenInterrupt *store) { store->touched = true; }
|
||||
void TouchscreenInterrupt::gpio_intr(TouchscreenInterrupt *store) {
|
||||
bool new_state = store->isr_pin_.digital_read();
|
||||
if (new_state != store->inverted) {
|
||||
store->touched = true;
|
||||
if (store->component_ != nullptr) {
|
||||
store->component_->enable_loop_soon_any_context();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Touchscreen::attach_interrupt_(InternalGPIOPin *irq_pin, esphome::gpio::InterruptType type) {
|
||||
this->store_.isr_pin_ = irq_pin->to_isr();
|
||||
this->store_.component_ = this;
|
||||
this->store_.inverted = irq_pin->is_inverted();
|
||||
irq_pin->attach_interrupt(TouchscreenInterrupt::gpio_intr, &this->store_, type);
|
||||
this->store_.init = true;
|
||||
this->store_.touched = false;
|
||||
ESP_LOGD(TAG, "Attach Touch Interupt");
|
||||
ESP_LOGD(TAG, "Attach Touch Interrupt");
|
||||
}
|
||||
|
||||
void Touchscreen::call_setup() {
|
||||
@@ -71,6 +82,8 @@ void Touchscreen::loop() {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this->store_.init)
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
void Touchscreen::add_raw_touch_position_(uint8_t id, int16_t x_raw, int16_t y_raw, int16_t z_raw) {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/components/display/display.h"
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome {
|
||||
namespace touchscreen {
|
||||
@@ -30,9 +30,12 @@ struct TouchPoint {
|
||||
using TouchPoints_t = std::vector<TouchPoint>;
|
||||
|
||||
struct TouchscreenInterrupt {
|
||||
ISRInternalGPIOPin isr_pin_;
|
||||
volatile bool touched{true};
|
||||
bool init{false};
|
||||
bool inverted{false};
|
||||
static void gpio_intr(TouchscreenInterrupt *store);
|
||||
Component *component_{nullptr};
|
||||
};
|
||||
|
||||
class TouchListener {
|
||||
|
||||
@@ -283,11 +283,8 @@ void TuyaClimate::control_fan_mode_(const climate::ClimateCall &call) {
|
||||
|
||||
climate::ClimateTraits TuyaClimate::traits() {
|
||||
auto traits = climate::ClimateTraits();
|
||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_ACTION);
|
||||
if (this->current_temperature_id_.has_value()) {
|
||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
|
||||
}
|
||||
|
||||
traits.set_supports_action(true);
|
||||
traits.set_supports_current_temperature(this->current_temperature_id_.has_value());
|
||||
if (supports_heat_)
|
||||
traits.add_supported_mode(climate::CLIMATE_MODE_HEAT);
|
||||
if (supports_cool_)
|
||||
|
||||
@@ -17,12 +17,6 @@ UponorSmatrixDevice = uponor_smatrix_ns.class_(
|
||||
"UponorSmatrixDevice", cg.Parented.template(UponorSmatrixComponent)
|
||||
)
|
||||
|
||||
|
||||
device_address = cv.All(
|
||||
cv.hex_int,
|
||||
cv.Range(min=0x1000000, max=0xFFFFFFFF, msg="Expected a 32 bit device address"),
|
||||
)
|
||||
|
||||
CONF_UPONOR_SMATRIX_ID = "uponor_smatrix_id"
|
||||
CONF_TIME_DEVICE_ADDRESS = "time_device_address"
|
||||
|
||||
@@ -30,12 +24,9 @@ CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(UponorSmatrixComponent),
|
||||
cv.Optional(CONF_ADDRESS): cv.invalid(
|
||||
f"The '{CONF_ADDRESS}' option has been removed. "
|
||||
"Use full 32 bit addresses in the device definitions instead."
|
||||
),
|
||||
cv.Optional(CONF_ADDRESS): cv.hex_uint16_t,
|
||||
cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
|
||||
cv.Optional(CONF_TIME_DEVICE_ADDRESS): device_address,
|
||||
cv.Optional(CONF_TIME_DEVICE_ADDRESS): cv.hex_uint16_t,
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
@@ -56,7 +47,7 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
UPONOR_SMATRIX_DEVICE_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_UPONOR_SMATRIX_ID): cv.use_id(UponorSmatrixComponent),
|
||||
cv.Required(CONF_ADDRESS): device_address,
|
||||
cv.Required(CONF_ADDRESS): cv.hex_uint16_t,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -67,15 +58,17 @@ async def to_code(config):
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
|
||||
if address := config.get(CONF_ADDRESS):
|
||||
cg.add(var.set_system_address(address))
|
||||
if time_id := config.get(CONF_TIME_ID):
|
||||
time_ = await cg.get_variable(time_id)
|
||||
cg.add(var.set_time_id(time_))
|
||||
if time_device_address := config.get(CONF_TIME_DEVICE_ADDRESS):
|
||||
cg.add(var.set_time_device_address(time_device_address))
|
||||
if time_device_address := config.get(CONF_TIME_DEVICE_ADDRESS):
|
||||
cg.add(var.set_time_device_address(time_device_address))
|
||||
|
||||
|
||||
async def register_uponor_smatrix_device(var, config):
|
||||
parent = await cg.get_variable(config[CONF_UPONOR_SMATRIX_ID])
|
||||
cg.add(var.set_parent(parent))
|
||||
cg.add(var.set_address(config[CONF_ADDRESS]))
|
||||
cg.add(var.set_device_address(config[CONF_ADDRESS]))
|
||||
cg.add(parent.register_device(var))
|
||||
|
||||
@@ -10,7 +10,7 @@ static const char *const TAG = "uponor_smatrix.climate";
|
||||
|
||||
void UponorSmatrixClimate::dump_config() {
|
||||
LOG_CLIMATE("", "Uponor Smatrix Climate", this);
|
||||
ESP_LOGCONFIG(TAG, " Device address: 0x%08X", this->address_);
|
||||
ESP_LOGCONFIG(TAG, " Device address: 0x%04X", this->address_);
|
||||
}
|
||||
|
||||
void UponorSmatrixClimate::loop() {
|
||||
@@ -30,9 +30,10 @@ void UponorSmatrixClimate::loop() {
|
||||
|
||||
climate::ClimateTraits UponorSmatrixClimate::traits() {
|
||||
auto traits = climate::ClimateTraits();
|
||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE | climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY |
|
||||
climate::CLIMATE_SUPPORTS_ACTION);
|
||||
traits.set_supports_current_temperature(true);
|
||||
traits.set_supports_current_humidity(true);
|
||||
traits.set_supported_modes({climate::CLIMATE_MODE_HEAT});
|
||||
traits.set_supports_action(true);
|
||||
traits.set_supported_presets({climate::CLIMATE_PRESET_ECO});
|
||||
traits.set_visual_min_temperature(this->min_temperature_);
|
||||
traits.set_visual_max_temperature(this->max_temperature_);
|
||||
|
||||
@@ -9,7 +9,7 @@ static const char *const TAG = "uponor_smatrix.sensor";
|
||||
void UponorSmatrixSensor::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Uponor Smatrix Sensor\n"
|
||||
" Device address: 0x%08X",
|
||||
" Device address: 0x%04X",
|
||||
this->address_);
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||
LOG_SENSOR(" ", "External Temperature", this->external_temperature_sensor_);
|
||||
|
||||
@@ -18,10 +18,11 @@ void UponorSmatrixComponent::setup() {
|
||||
|
||||
void UponorSmatrixComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Uponor Smatrix");
|
||||
ESP_LOGCONFIG(TAG, " System address: 0x%04X", this->address_);
|
||||
#ifdef USE_TIME
|
||||
if (this->time_id_ != nullptr) {
|
||||
ESP_LOGCONFIG(TAG, " Time synchronization: YES");
|
||||
ESP_LOGCONFIG(TAG, " Time master device address: 0x%08X", this->time_device_address_);
|
||||
ESP_LOGCONFIG(TAG, " Time master device address: 0x%04X", this->time_device_address_);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -30,7 +31,7 @@ void UponorSmatrixComponent::dump_config() {
|
||||
if (!this->unknown_devices_.empty()) {
|
||||
ESP_LOGCONFIG(TAG, " Detected unknown device addresses:");
|
||||
for (auto device_address : this->unknown_devices_) {
|
||||
ESP_LOGCONFIG(TAG, " 0x%08X", device_address);
|
||||
ESP_LOGCONFIG(TAG, " 0x%04X", device_address);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,7 +89,8 @@ bool UponorSmatrixComponent::parse_byte_(uint8_t byte) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t device_address = encode_uint32(packet[0], packet[1], packet[2], packet[3]);
|
||||
uint16_t system_address = encode_uint16(packet[0], packet[1]);
|
||||
uint16_t device_address = encode_uint16(packet[2], packet[3]);
|
||||
uint16_t crc = encode_uint16(packet[packet_len - 1], packet[packet_len - 2]);
|
||||
|
||||
uint16_t computed_crc = crc16(packet, packet_len - 2);
|
||||
@@ -97,14 +99,24 @@ bool UponorSmatrixComponent::parse_byte_(uint8_t byte) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "Received packet: addr=%08X, data=%s, crc=%04X", device_address,
|
||||
ESP_LOGV(TAG, "Received packet: sys=%04X, dev=%04X, data=%s, crc=%04X", system_address, device_address,
|
||||
format_hex(&packet[4], packet_len - 6).c_str(), crc);
|
||||
|
||||
// Detect or check system address
|
||||
if (this->address_ == 0) {
|
||||
ESP_LOGI(TAG, "Using detected system address 0x%04X", system_address);
|
||||
this->address_ = system_address;
|
||||
} else if (this->address_ != system_address) {
|
||||
// This should never happen except if the system address was set or detected incorrectly, so warn the user.
|
||||
ESP_LOGW(TAG, "Received packet from unknown system address 0x%04X", system_address);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Handle packet
|
||||
size_t data_len = (packet_len - 6) / 3;
|
||||
if (data_len == 0) {
|
||||
if (packet[4] == UPONOR_ID_REQUEST)
|
||||
ESP_LOGVV(TAG, "Ignoring request packet for device 0x%08X", device_address);
|
||||
ESP_LOGVV(TAG, "Ignoring request packet for device 0x%04X", device_address);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -129,7 +141,7 @@ bool UponorSmatrixComponent::parse_byte_(uint8_t byte) {
|
||||
if (data[i].id == UPONOR_ID_DATETIME1)
|
||||
found_time = true;
|
||||
if (found_temperature && found_time) {
|
||||
ESP_LOGI(TAG, "Using detected time device address 0x%08X", device_address);
|
||||
ESP_LOGI(TAG, "Using detected time device address 0x%04X", device_address);
|
||||
this->time_device_address_ = device_address;
|
||||
break;
|
||||
}
|
||||
@@ -148,7 +160,7 @@ bool UponorSmatrixComponent::parse_byte_(uint8_t byte) {
|
||||
|
||||
// Log unknown device addresses
|
||||
if (!found && !this->unknown_devices_.count(device_address)) {
|
||||
ESP_LOGI(TAG, "Received packet for unknown device address 0x%08X ", device_address);
|
||||
ESP_LOGI(TAG, "Received packet for unknown device address 0x%04X ", device_address);
|
||||
this->unknown_devices_.insert(device_address);
|
||||
}
|
||||
|
||||
@@ -156,16 +168,16 @@ bool UponorSmatrixComponent::parse_byte_(uint8_t byte) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UponorSmatrixComponent::send(uint32_t device_address, const UponorSmatrixData *data, size_t data_len) {
|
||||
if (device_address == 0 || data == nullptr || data_len == 0)
|
||||
bool UponorSmatrixComponent::send(uint16_t device_address, const UponorSmatrixData *data, size_t data_len) {
|
||||
if (this->address_ == 0 || device_address == 0 || data == nullptr || data_len == 0)
|
||||
return false;
|
||||
|
||||
// Assemble packet for send queue. All fields are big-endian except for the little-endian checksum.
|
||||
std::vector<uint8_t> packet;
|
||||
packet.reserve(6 + 3 * data_len);
|
||||
|
||||
packet.push_back(device_address >> 24);
|
||||
packet.push_back(device_address >> 16);
|
||||
packet.push_back(this->address_ >> 8);
|
||||
packet.push_back(this->address_ >> 0);
|
||||
packet.push_back(device_address >> 8);
|
||||
packet.push_back(device_address >> 0);
|
||||
|
||||
|
||||
@@ -71,21 +71,23 @@ class UponorSmatrixComponent : public uart::UARTDevice, public Component {
|
||||
void dump_config() override;
|
||||
void loop() override;
|
||||
|
||||
void set_system_address(uint16_t address) { this->address_ = address; }
|
||||
void register_device(UponorSmatrixDevice *device) { this->devices_.push_back(device); }
|
||||
|
||||
bool send(uint32_t device_address, const UponorSmatrixData *data, size_t data_len);
|
||||
bool send(uint16_t device_address, const UponorSmatrixData *data, size_t data_len);
|
||||
|
||||
#ifdef USE_TIME
|
||||
void set_time_id(time::RealTimeClock *time_id) { this->time_id_ = time_id; }
|
||||
void set_time_device_address(uint32_t address) { this->time_device_address_ = address; }
|
||||
void set_time_device_address(uint16_t address) { this->time_device_address_ = address; }
|
||||
void send_time() { this->send_time_requested_ = true; }
|
||||
#endif
|
||||
|
||||
protected:
|
||||
bool parse_byte_(uint8_t byte);
|
||||
|
||||
uint16_t address_;
|
||||
std::vector<UponorSmatrixDevice *> devices_;
|
||||
std::set<uint32_t> unknown_devices_;
|
||||
std::set<uint16_t> unknown_devices_;
|
||||
|
||||
std::vector<uint8_t> rx_buffer_;
|
||||
std::queue<std::vector<uint8_t>> tx_queue_;
|
||||
@@ -94,7 +96,7 @@ class UponorSmatrixComponent : public uart::UARTDevice, public Component {
|
||||
|
||||
#ifdef USE_TIME
|
||||
time::RealTimeClock *time_id_{nullptr};
|
||||
uint32_t time_device_address_;
|
||||
uint16_t time_device_address_;
|
||||
bool send_time_requested_;
|
||||
bool do_send_time_();
|
||||
#endif
|
||||
@@ -102,7 +104,7 @@ class UponorSmatrixComponent : public uart::UARTDevice, public Component {
|
||||
|
||||
class UponorSmatrixDevice : public Parented<UponorSmatrixComponent> {
|
||||
public:
|
||||
void set_address(uint32_t address) { this->address_ = address; }
|
||||
void set_device_address(uint16_t address) { this->address_ = address; }
|
||||
|
||||
virtual void on_device_data(const UponorSmatrixData *data, size_t data_len) = 0;
|
||||
bool send(const UponorSmatrixData *data, size_t data_len) {
|
||||
@@ -111,7 +113,7 @@ class UponorSmatrixDevice : public Parented<UponorSmatrixComponent> {
|
||||
|
||||
protected:
|
||||
friend UponorSmatrixComponent;
|
||||
uint32_t address_;
|
||||
uint16_t address_;
|
||||
};
|
||||
|
||||
inline float raw_to_celsius(uint16_t raw) {
|
||||
|
||||
@@ -136,18 +136,6 @@ def _final_validate_sorting(config: ConfigType) -> ConfigType:
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate_sorting
|
||||
|
||||
|
||||
def _consume_web_server_sockets(config: ConfigType) -> ConfigType:
|
||||
"""Register socket needs for web_server component."""
|
||||
from esphome.components import socket
|
||||
|
||||
# Web server needs 1 listening socket + typically 2 concurrent client connections
|
||||
# (browser makes 2 connections for page + event stream)
|
||||
sockets_needed = 3
|
||||
socket.consume_sockets(sockets_needed, "web_server")(config)
|
||||
return config
|
||||
|
||||
|
||||
sorting_group = {
|
||||
cv.Required(CONF_ID): cv.declare_id(cg.int_),
|
||||
cv.Required(CONF_NAME): cv.string,
|
||||
@@ -217,7 +205,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
validate_local,
|
||||
validate_sorting_groups,
|
||||
validate_ota,
|
||||
_consume_web_server_sockets,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -407,8 +407,7 @@ async def to_code(config):
|
||||
|
||||
cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
|
||||
cg.add(var.set_power_save_mode(config[CONF_POWER_SAVE_MODE]))
|
||||
if config[CONF_FAST_CONNECT]:
|
||||
cg.add_define("USE_WIFI_FAST_CONNECT")
|
||||
cg.add(var.set_fast_connect(config[CONF_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]))
|
||||
|
||||
@@ -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);
|
||||
#ifdef USE_WIFI_FAST_CONNECT
|
||||
this->fast_connect_pref_ = global_preferences->make_preference<wifi::SavedWifiFastConnectSettings>(hash + 1, false);
|
||||
#endif
|
||||
if (this->fast_connect_) {
|
||||
this->fast_connect_pref_ = global_preferences->make_preference<wifi::SavedWifiFastConnectSettings>(hash + 1, false);
|
||||
}
|
||||
|
||||
SavedWifiSettings save{};
|
||||
if (this->pref_.load(&save)) {
|
||||
@@ -108,16 +108,16 @@ void WiFiComponent::start() {
|
||||
ESP_LOGV(TAG, "Setting Power Save Option failed");
|
||||
}
|
||||
|
||||
#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_];
|
||||
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();
|
||||
}
|
||||
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,20 +168,13 @@ void WiFiComponent::loop() {
|
||||
case WIFI_COMPONENT_STATE_COOLDOWN: {
|
||||
this->status_set_warning(LOG_STR("waiting to reconnect"));
|
||||
if (millis() - this->action_started_ > 5000) {
|
||||
#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->fast_connect_ || 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;
|
||||
}
|
||||
@@ -251,6 +244,7 @@ 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; }
|
||||
@@ -613,12 +607,10 @@ void WiFiComponent::check_scanning_finished() {
|
||||
for (auto &ap : this->sta_) {
|
||||
if (res.matches(ap)) {
|
||||
res.set_matches(true);
|
||||
// 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());
|
||||
if (!this->has_sta_priority(res.get_bssid())) {
|
||||
this->set_sta_priority(res.get_bssid(), ap.get_priority());
|
||||
}
|
||||
res.set_priority(this->get_sta_priority(bssid));
|
||||
res.set_priority(this->get_sta_priority(res.get_bssid()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -637,9 +629,8 @@ void WiFiComponent::check_scanning_finished() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Build connection params directly into selected_ap_ to avoid extra copy
|
||||
const WiFiScanResult &scan_res = this->scan_result_[0];
|
||||
WiFiAP &selected = this->selected_ap_;
|
||||
WiFiAP connect_params;
|
||||
WiFiScanResult scan_res = this->scan_result_[0];
|
||||
for (auto &config : this->sta_) {
|
||||
// search for matching STA config, at least one will match (from checks before)
|
||||
if (!scan_res.matches(config)) {
|
||||
@@ -648,38 +639,37 @@ void WiFiComponent::check_scanning_finished() {
|
||||
|
||||
if (config.get_hidden()) {
|
||||
// selected network is hidden, we use the data from the config
|
||||
selected.set_hidden(true);
|
||||
selected.set_ssid(config.get_ssid());
|
||||
// Clear channel and BSSID for hidden networks - there might be multiple hidden networks
|
||||
connect_params.set_hidden(true);
|
||||
connect_params.set_ssid(config.get_ssid());
|
||||
// don't set BSSID and channel, 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).
|
||||
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());
|
||||
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());
|
||||
}
|
||||
// copy manual IP (if set)
|
||||
selected.set_manual_ip(config.get_manual_ip());
|
||||
connect_params.set_manual_ip(config.get_manual_ip());
|
||||
|
||||
#ifdef USE_WIFI_WPA2_EAP
|
||||
// copy EAP parameters (if set)
|
||||
selected.set_eap(config.get_eap());
|
||||
connect_params.set_eap(config.get_eap());
|
||||
#endif
|
||||
|
||||
// copy password (if set)
|
||||
selected.set_password(config.get_password());
|
||||
connect_params.set_password(config.get_password());
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
yield();
|
||||
|
||||
this->start_connecting(this->selected_ap_, false);
|
||||
this->selected_ap_ = connect_params;
|
||||
this->start_connecting(connect_params, false);
|
||||
}
|
||||
|
||||
void WiFiComponent::dump_config() {
|
||||
@@ -729,9 +719,9 @@ void WiFiComponent::check_connecting_finished() {
|
||||
this->scan_result_.shrink_to_fit();
|
||||
}
|
||||
|
||||
#ifdef USE_WIFI_FAST_CONNECT
|
||||
this->save_fast_connect_settings_();
|
||||
#endif
|
||||
if (this->fast_connect_) {
|
||||
this->save_fast_connect_settings_();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -779,31 +769,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_)) {
|
||||
#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();
|
||||
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_];
|
||||
} else {
|
||||
// Try next AP
|
||||
this->ap_index_++;
|
||||
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_++;
|
||||
}
|
||||
}
|
||||
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_++;
|
||||
}
|
||||
@@ -849,7 +839,6 @@ bool WiFiComponent::is_esp32_improv_active_() {
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef USE_WIFI_FAST_CONNECT
|
||||
bool WiFiComponent::load_fast_connect_settings_() {
|
||||
SavedWifiFastConnectSettings fast_connect_save{};
|
||||
|
||||
@@ -884,7 +873,6 @@ 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; }
|
||||
@@ -914,7 +902,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) const {
|
||||
bool WiFiScanResult::matches(const WiFiAP &config) {
|
||||
if (config.get_hidden()) {
|
||||
// User configured a hidden network, only match actually hidden networks
|
||||
// don't match SSID
|
||||
|
||||
@@ -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) const;
|
||||
bool matches(const WiFiAP &config);
|
||||
|
||||
bool get_matches() const;
|
||||
void set_matches(bool matches);
|
||||
@@ -240,6 +240,7 @@ 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();
|
||||
@@ -363,10 +364,8 @@ 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);
|
||||
@@ -400,9 +399,7 @@ 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_;
|
||||
@@ -414,17 +411,14 @@ 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
|
||||
#ifdef USE_WIFI_FAST_CONNECT
|
||||
bool fast_connect_{false};
|
||||
bool trying_loaded_ap_{false};
|
||||
#endif
|
||||
bool retry_hidden_{false};
|
||||
bool has_ap_{false};
|
||||
bool handled_connected_state_{false};
|
||||
|
||||
@@ -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)) {
|
||||
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);
|
||||
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_done_ = true;
|
||||
}
|
||||
|
||||
@@ -776,12 +776,13 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
|
||||
}
|
||||
|
||||
uint16_t number = it.number;
|
||||
auto records = std::make_unique<wifi_ap_record_t[]>(number);
|
||||
err = esp_wifi_scan_get_ap_records(&number, records.get());
|
||||
std::vector<wifi_ap_record_t> records(number);
|
||||
err = esp_wifi_scan_get_ap_records(&number, records.data());
|
||||
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++) {
|
||||
@@ -789,8 +790,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));
|
||||
scan_result_.emplace_back(bssid, ssid, record.primary, record.rssi, record.authmode != WIFI_AUTH_OPEN,
|
||||
ssid.empty());
|
||||
WiFiScanResult result(bssid, ssid, record.primary, record.rssi, record.authmode != WIFI_AUTH_OPEN, ssid.empty());
|
||||
scan_result_.push_back(result);
|
||||
}
|
||||
|
||||
} else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_START) {
|
||||
|
||||
@@ -419,9 +419,9 @@ void WiFiComponent::wifi_scan_done_callback_() {
|
||||
uint8_t *bssid = WiFi.BSSID(i);
|
||||
int32_t channel = WiFi.channel(i);
|
||||
|
||||
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);
|
||||
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);
|
||||
}
|
||||
WiFi.scanDelete();
|
||||
this->scan_done_ = true;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user