mirror of
https://github.com/esphome/esphome.git
synced 2025-10-24 20:53:48 +01:00
Merge branch 'text_sensor_filters' into integration
This commit is contained in:
111
.github/workflows/ci-memory-impact-comment.yml
vendored
Normal file
111
.github/workflows/ci-memory-impact-comment.yml
vendored
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
---
|
||||||
|
name: Memory Impact Comment (Forks)
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: ["CI"]
|
||||||
|
types: [completed]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
actions: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
memory-impact-comment:
|
||||||
|
name: Post memory impact comment (fork PRs only)
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
# Only run for PRs from forks that had successful CI runs
|
||||||
|
if: >
|
||||||
|
github.event.workflow_run.event == 'pull_request' &&
|
||||||
|
github.event.workflow_run.conclusion == 'success' &&
|
||||||
|
github.event.workflow_run.head_repository.full_name != github.repository
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
steps:
|
||||||
|
- name: Get PR details
|
||||||
|
id: pr
|
||||||
|
run: |
|
||||||
|
# Get PR details by searching for PR with matching head SHA
|
||||||
|
# The workflow_run.pull_requests field is often empty for forks
|
||||||
|
# Use paginate to handle repos with many open PRs
|
||||||
|
head_sha="${{ github.event.workflow_run.head_sha }}"
|
||||||
|
pr_data=$(gh api --paginate "/repos/${{ github.repository }}/pulls" \
|
||||||
|
--jq ".[] | select(.head.sha == \"$head_sha\") | {number: .number, base_ref: .base.ref}" \
|
||||||
|
| head -n 1)
|
||||||
|
|
||||||
|
if [ -z "$pr_data" ]; then
|
||||||
|
echo "No PR found for SHA $head_sha, skipping"
|
||||||
|
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
pr_number=$(echo "$pr_data" | jq -r '.number')
|
||||||
|
base_ref=$(echo "$pr_data" | jq -r '.base_ref')
|
||||||
|
|
||||||
|
echo "pr_number=$pr_number" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "base_ref=$base_ref" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Found PR #$pr_number targeting base branch: $base_ref"
|
||||||
|
|
||||||
|
- name: Check out code from base repository
|
||||||
|
if: steps.pr.outputs.skip != 'true'
|
||||||
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
with:
|
||||||
|
# Always check out from the base repository (esphome/esphome), never from forks
|
||||||
|
# Use the PR's target branch to ensure we run trusted code from the main repo
|
||||||
|
repository: ${{ github.repository }}
|
||||||
|
ref: ${{ steps.pr.outputs.base_ref }}
|
||||||
|
|
||||||
|
- name: Restore Python
|
||||||
|
if: steps.pr.outputs.skip != 'true'
|
||||||
|
uses: ./.github/actions/restore-python
|
||||||
|
with:
|
||||||
|
python-version: "3.11"
|
||||||
|
cache-key: ${{ hashFiles('.cache-key') }}
|
||||||
|
|
||||||
|
- name: Download memory analysis artifacts
|
||||||
|
if: steps.pr.outputs.skip != 'true'
|
||||||
|
run: |
|
||||||
|
run_id="${{ github.event.workflow_run.id }}"
|
||||||
|
echo "Downloading artifacts from workflow run $run_id"
|
||||||
|
|
||||||
|
mkdir -p memory-analysis
|
||||||
|
|
||||||
|
# Download target analysis artifact
|
||||||
|
if gh run download --name "memory-analysis-target" --dir memory-analysis --repo "${{ github.repository }}" "$run_id"; then
|
||||||
|
echo "Downloaded memory-analysis-target artifact."
|
||||||
|
else
|
||||||
|
echo "No memory-analysis-target artifact found."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Download PR analysis artifact
|
||||||
|
if gh run download --name "memory-analysis-pr" --dir memory-analysis --repo "${{ github.repository }}" "$run_id"; then
|
||||||
|
echo "Downloaded memory-analysis-pr artifact."
|
||||||
|
else
|
||||||
|
echo "No memory-analysis-pr artifact found."
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Check if artifacts exist
|
||||||
|
id: check
|
||||||
|
if: steps.pr.outputs.skip != 'true'
|
||||||
|
run: |
|
||||||
|
if [ -f ./memory-analysis/memory-analysis-target.json ] && [ -f ./memory-analysis/memory-analysis-pr.json ]; then
|
||||||
|
echo "found=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "found=false" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Memory analysis artifacts not found, skipping comment"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Post or update PR comment
|
||||||
|
if: steps.pr.outputs.skip != 'true' && steps.check.outputs.found == 'true'
|
||||||
|
env:
|
||||||
|
PR_NUMBER: ${{ steps.pr.outputs.pr_number }}
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
# Pass PR number and JSON file paths directly to Python script
|
||||||
|
# Let Python parse the JSON to avoid shell injection risks
|
||||||
|
# The script will validate and sanitize all inputs
|
||||||
|
python script/ci_memory_impact_comment.py \
|
||||||
|
--pr-number "$PR_NUMBER" \
|
||||||
|
--target-json ./memory-analysis/memory-analysis-target.json \
|
||||||
|
--pr-json ./memory-analysis/memory-analysis-pr.json
|
||||||
255
.github/workflows/ci.yml
vendored
255
.github/workflows/ci.yml
vendored
@@ -170,11 +170,13 @@ jobs:
|
|||||||
outputs:
|
outputs:
|
||||||
integration-tests: ${{ steps.determine.outputs.integration-tests }}
|
integration-tests: ${{ steps.determine.outputs.integration-tests }}
|
||||||
clang-tidy: ${{ steps.determine.outputs.clang-tidy }}
|
clang-tidy: ${{ steps.determine.outputs.clang-tidy }}
|
||||||
|
clang-tidy-mode: ${{ steps.determine.outputs.clang-tidy-mode }}
|
||||||
python-linters: ${{ steps.determine.outputs.python-linters }}
|
python-linters: ${{ steps.determine.outputs.python-linters }}
|
||||||
changed-components: ${{ steps.determine.outputs.changed-components }}
|
changed-components: ${{ steps.determine.outputs.changed-components }}
|
||||||
changed-components-with-tests: ${{ steps.determine.outputs.changed-components-with-tests }}
|
changed-components-with-tests: ${{ steps.determine.outputs.changed-components-with-tests }}
|
||||||
directly-changed-components-with-tests: ${{ steps.determine.outputs.directly-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 }}
|
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 }}
|
memory_impact: ${{ steps.determine.outputs.memory-impact }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
@@ -200,11 +202,13 @@ jobs:
|
|||||||
# Extract individual fields
|
# Extract individual fields
|
||||||
echo "integration-tests=$(echo "$output" | jq -r '.integration_tests')" >> $GITHUB_OUTPUT
|
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=$(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 "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT
|
||||||
echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT
|
echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT
|
||||||
echo "changed-components-with-tests=$(echo "$output" | jq -c '.changed_components_with_tests')" >> $GITHUB_OUTPUT
|
echo "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 "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 "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
|
echo "memory-impact=$(echo "$output" | jq -c '.memory_impact')" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
integration-tests:
|
integration-tests:
|
||||||
@@ -243,7 +247,7 @@ jobs:
|
|||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
pytest -vv --no-cov --tb=native -n auto tests/integration/
|
pytest -vv --no-cov --tb=native -n auto tests/integration/
|
||||||
|
|
||||||
clang-tidy:
|
clang-tidy-single:
|
||||||
name: ${{ matrix.name }}
|
name: ${{ matrix.name }}
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
needs:
|
needs:
|
||||||
@@ -261,22 +265,6 @@ jobs:
|
|||||||
name: Run script/clang-tidy for ESP8266
|
name: Run script/clang-tidy for ESP8266
|
||||||
options: --environment esp8266-arduino-tidy --grep USE_ESP8266
|
options: --environment esp8266-arduino-tidy --grep USE_ESP8266
|
||||||
pio_cache_key: tidyesp8266
|
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
|
- id: clang-tidy
|
||||||
name: Run script/clang-tidy for ESP32 IDF
|
name: Run script/clang-tidy for ESP32 IDF
|
||||||
options: --environment esp32-idf-tidy --grep USE_ESP_IDF
|
options: --environment esp32-idf-tidy --grep USE_ESP_IDF
|
||||||
@@ -357,6 +345,166 @@ jobs:
|
|||||||
# yamllint disable-line rule:line-length
|
# yamllint disable-line rule:line-length
|
||||||
if: always()
|
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:
|
test-build-components-splitter:
|
||||||
name: Split components for intelligent grouping (40 weighted per batch)
|
name: Split components for intelligent grouping (40 weighted per batch)
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
@@ -641,6 +789,12 @@ jobs:
|
|||||||
--output-env \
|
--output-env \
|
||||||
--output-json memory-analysis-target.json
|
--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
|
- 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'
|
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
|
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||||
@@ -720,6 +874,13 @@ jobs:
|
|||||||
python script/ci_memory_impact_extract.py \
|
python script/ci_memory_impact_extract.py \
|
||||||
--output-env \
|
--output-env \
|
||||||
--output-json memory-analysis-pr.json
|
--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
|
- name: Upload memory analysis JSON
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||||
with:
|
with:
|
||||||
@@ -736,10 +897,12 @@ jobs:
|
|||||||
- determine-jobs
|
- determine-jobs
|
||||||
- memory-impact-target-branch
|
- memory-impact-target-branch
|
||||||
- memory-impact-pr-branch
|
- memory-impact-pr-branch
|
||||||
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.memory_impact).should_run == 'true' && needs.memory-impact-target-branch.outputs.skip != 'true'
|
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && fromJSON(needs.determine-jobs.outputs.memory_impact).should_run == 'true' && needs.memory-impact-target-branch.outputs.skip != 'true'
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
@@ -749,65 +912,29 @@ jobs:
|
|||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||||
- name: Download target analysis JSON
|
- name: Download target analysis JSON
|
||||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: memory-analysis-target
|
name: memory-analysis-target
|
||||||
path: ./memory-analysis
|
path: ./memory-analysis
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
- name: Download PR analysis JSON
|
- name: Download PR analysis JSON
|
||||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: memory-analysis-pr
|
name: memory-analysis-pr
|
||||||
path: ./memory-analysis
|
path: ./memory-analysis
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
- name: Post or update PR comment
|
- name: Post or update PR comment
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ github.token }}
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||||
COMPONENTS: ${{ toJSON(fromJSON(needs.determine-jobs.outputs.memory_impact).components) }}
|
|
||||||
PLATFORM: ${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}
|
|
||||||
TARGET_RAM: ${{ needs.memory-impact-target-branch.outputs.ram_usage }}
|
|
||||||
TARGET_FLASH: ${{ needs.memory-impact-target-branch.outputs.flash_usage }}
|
|
||||||
PR_RAM: ${{ needs.memory-impact-pr-branch.outputs.ram_usage }}
|
|
||||||
PR_FLASH: ${{ needs.memory-impact-pr-branch.outputs.flash_usage }}
|
|
||||||
TARGET_CACHE_HIT: ${{ needs.memory-impact-target-branch.outputs.cache_hit }}
|
|
||||||
run: |
|
run: |
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
|
|
||||||
# Check if analysis JSON files exist
|
# Pass JSON file paths directly to Python script
|
||||||
target_json_arg=""
|
# All data is extracted from JSON files for security
|
||||||
pr_json_arg=""
|
|
||||||
|
|
||||||
if [ -f ./memory-analysis/memory-analysis-target.json ]; then
|
|
||||||
echo "Found target analysis JSON"
|
|
||||||
target_json_arg="--target-json ./memory-analysis/memory-analysis-target.json"
|
|
||||||
else
|
|
||||||
echo "No target analysis JSON found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -f ./memory-analysis/memory-analysis-pr.json ]; then
|
|
||||||
echo "Found PR analysis JSON"
|
|
||||||
pr_json_arg="--pr-json ./memory-analysis/memory-analysis-pr.json"
|
|
||||||
else
|
|
||||||
echo "No PR analysis JSON found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Add cache flag if target was cached
|
|
||||||
cache_flag=""
|
|
||||||
if [ "$TARGET_CACHE_HIT" == "true" ]; then
|
|
||||||
cache_flag="--target-cache-hit"
|
|
||||||
fi
|
|
||||||
|
|
||||||
python script/ci_memory_impact_comment.py \
|
python script/ci_memory_impact_comment.py \
|
||||||
--pr-number "${{ github.event.pull_request.number }}" \
|
--pr-number "$PR_NUMBER" \
|
||||||
--components "$COMPONENTS" \
|
--target-json ./memory-analysis/memory-analysis-target.json \
|
||||||
--platform "$PLATFORM" \
|
--pr-json ./memory-analysis/memory-analysis-pr.json
|
||||||
--target-ram "$TARGET_RAM" \
|
|
||||||
--target-flash "$TARGET_FLASH" \
|
|
||||||
--pr-ram "$PR_RAM" \
|
|
||||||
--pr-flash "$PR_FLASH" \
|
|
||||||
$target_json_arg \
|
|
||||||
$pr_json_arg \
|
|
||||||
$cache_flag
|
|
||||||
|
|
||||||
ci-status:
|
ci-status:
|
||||||
name: CI Status
|
name: CI Status
|
||||||
@@ -818,7 +945,9 @@ jobs:
|
|||||||
- pylint
|
- pylint
|
||||||
- pytest
|
- pytest
|
||||||
- integration-tests
|
- integration-tests
|
||||||
- clang-tidy
|
- clang-tidy-single
|
||||||
|
- clang-tidy-nosplit
|
||||||
|
- clang-tidy-split
|
||||||
- determine-jobs
|
- determine-jobs
|
||||||
- test-build-components-splitter
|
- test-build-components-splitter
|
||||||
- test-build-components-split
|
- test-build-components-split
|
||||||
|
|||||||
@@ -161,6 +161,7 @@ esphome/components/esp32_rmt_led_strip/* @jesserockz
|
|||||||
esphome/components/esp8266/* @esphome/core
|
esphome/components/esp8266/* @esphome/core
|
||||||
esphome/components/esp_ldo/* @clydebarrow
|
esphome/components/esp_ldo/* @clydebarrow
|
||||||
esphome/components/espnow/* @jesserockz
|
esphome/components/espnow/* @jesserockz
|
||||||
|
esphome/components/espnow/packet_transport/* @EasilyBoredEngineer
|
||||||
esphome/components/ethernet_info/* @gtjadsonsantos
|
esphome/components/ethernet_info/* @gtjadsonsantos
|
||||||
esphome/components/event/* @nohat
|
esphome/components/event/* @nohat
|
||||||
esphome/components/exposure_notifications/* @OttoWinter
|
esphome/components/exposure_notifications/* @OttoWinter
|
||||||
|
|||||||
@@ -62,6 +62,40 @@ from esphome.util import (
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_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):
|
class ArgsProtocol(Protocol):
|
||||||
device: list[str] | None
|
device: list[str] | None
|
||||||
@@ -899,6 +933,54 @@ def command_idedata(args: ArgsProtocol, config: ConfigType) -> int:
|
|||||||
return 0
|
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:
|
def command_rename(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||||
new_name = args.name
|
new_name = args.name
|
||||||
for c in new_name:
|
for c in new_name:
|
||||||
@@ -1014,6 +1096,7 @@ POST_CONFIG_ACTIONS = {
|
|||||||
"idedata": command_idedata,
|
"idedata": command_idedata,
|
||||||
"rename": command_rename,
|
"rename": command_rename,
|
||||||
"discover": command_discover,
|
"discover": command_discover,
|
||||||
|
"analyze-memory": command_analyze_memory,
|
||||||
}
|
}
|
||||||
|
|
||||||
SIMPLE_CONFIG_ACTIONS = [
|
SIMPLE_CONFIG_ACTIONS = [
|
||||||
@@ -1310,6 +1393,14 @@ def parse_args(argv):
|
|||||||
)
|
)
|
||||||
parser_rename.add_argument("name", help="The new name for the device.", type=str)
|
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
|
# Keep backward compatibility with the old command line format of
|
||||||
# esphome <config> <command>.
|
# esphome <config> <command>.
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class Anova : public climate::Climate, public esphome::ble_client::BLEClientNode
|
|||||||
void dump_config() override;
|
void dump_config() override;
|
||||||
climate::ClimateTraits traits() override {
|
climate::ClimateTraits traits() override {
|
||||||
auto traits = climate::ClimateTraits();
|
auto traits = climate::ClimateTraits();
|
||||||
traits.set_supports_current_temperature(true);
|
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
|
||||||
traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::ClimateMode::CLIMATE_MODE_HEAT});
|
traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::ClimateMode::CLIMATE_MODE_HEAT});
|
||||||
traits.set_visual_min_temperature(25.0);
|
traits.set_visual_min_temperature(25.0);
|
||||||
traits.set_visual_max_temperature(100.0);
|
traits.set_visual_max_temperature(100.0);
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ namespace bang_bang {
|
|||||||
|
|
||||||
static const char *const TAG = "bang_bang.climate";
|
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() {
|
void BangBangClimate::setup() {
|
||||||
this->sensor_->add_on_state_callback([this](float state) {
|
this->sensor_->add_on_state_callback([this](float state) {
|
||||||
this->current_temperature = state;
|
this->current_temperature = state;
|
||||||
@@ -31,53 +34,63 @@ void BangBangClimate::setup() {
|
|||||||
restore->to_call(this).perform();
|
restore->to_call(this).perform();
|
||||||
} else {
|
} else {
|
||||||
// restore from defaults, change_away handles those for us
|
// restore from defaults, change_away handles those for us
|
||||||
if (supports_cool_ && supports_heat_) {
|
if (this->supports_cool_ && this->supports_heat_) {
|
||||||
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
|
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
|
||||||
} else if (supports_cool_) {
|
} else if (this->supports_cool_) {
|
||||||
this->mode = climate::CLIMATE_MODE_COOL;
|
this->mode = climate::CLIMATE_MODE_COOL;
|
||||||
} else if (supports_heat_) {
|
} else if (this->supports_heat_) {
|
||||||
this->mode = climate::CLIMATE_MODE_HEAT;
|
this->mode = climate::CLIMATE_MODE_HEAT;
|
||||||
}
|
}
|
||||||
this->change_away_(false);
|
this->change_away_(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void BangBangClimate::control(const climate::ClimateCall &call) {
|
void BangBangClimate::control(const climate::ClimateCall &call) {
|
||||||
if (call.get_mode().has_value())
|
if (call.get_mode().has_value()) {
|
||||||
this->mode = *call.get_mode();
|
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();
|
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();
|
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->change_away_(*call.get_preset() == climate::CLIMATE_PRESET_AWAY);
|
||||||
|
}
|
||||||
|
|
||||||
this->compute_state_();
|
this->compute_state_();
|
||||||
this->publish_state();
|
this->publish_state();
|
||||||
}
|
}
|
||||||
|
|
||||||
climate::ClimateTraits BangBangClimate::traits() {
|
climate::ClimateTraits BangBangClimate::traits() {
|
||||||
auto traits = climate::ClimateTraits();
|
auto traits = climate::ClimateTraits();
|
||||||
traits.set_supports_current_temperature(true);
|
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE |
|
||||||
if (this->humidity_sensor_ != nullptr)
|
climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE | climate::CLIMATE_SUPPORTS_ACTION);
|
||||||
traits.set_supports_current_humidity(true);
|
if (this->humidity_sensor_ != nullptr) {
|
||||||
|
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY);
|
||||||
|
}
|
||||||
traits.set_supported_modes({
|
traits.set_supported_modes({
|
||||||
climate::CLIMATE_MODE_OFF,
|
climate::CLIMATE_MODE_OFF,
|
||||||
});
|
});
|
||||||
if (supports_cool_)
|
if (this->supports_cool_) {
|
||||||
traits.add_supported_mode(climate::CLIMATE_MODE_COOL);
|
traits.add_supported_mode(climate::CLIMATE_MODE_COOL);
|
||||||
if (supports_heat_)
|
}
|
||||||
|
if (this->supports_heat_) {
|
||||||
traits.add_supported_mode(climate::CLIMATE_MODE_HEAT);
|
traits.add_supported_mode(climate::CLIMATE_MODE_HEAT);
|
||||||
if (supports_cool_ && supports_heat_)
|
}
|
||||||
|
if (this->supports_cool_ && this->supports_heat_) {
|
||||||
traits.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL);
|
traits.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL);
|
||||||
traits.set_supports_two_point_target_temperature(true);
|
}
|
||||||
if (supports_away_) {
|
if (this->supports_away_) {
|
||||||
traits.set_supported_presets({
|
traits.set_supported_presets({
|
||||||
climate::CLIMATE_PRESET_HOME,
|
climate::CLIMATE_PRESET_HOME,
|
||||||
climate::CLIMATE_PRESET_AWAY,
|
climate::CLIMATE_PRESET_AWAY,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
traits.set_supports_action(true);
|
|
||||||
return traits;
|
return traits;
|
||||||
}
|
}
|
||||||
|
|
||||||
void BangBangClimate::compute_state_() {
|
void BangBangClimate::compute_state_() {
|
||||||
if (this->mode == climate::CLIMATE_MODE_OFF) {
|
if (this->mode == climate::CLIMATE_MODE_OFF) {
|
||||||
this->switch_to_action_(climate::CLIMATE_ACTION_OFF);
|
this->switch_to_action_(climate::CLIMATE_ACTION_OFF);
|
||||||
@@ -122,6 +135,7 @@ void BangBangClimate::compute_state_() {
|
|||||||
|
|
||||||
this->switch_to_action_(target_action);
|
this->switch_to_action_(target_action);
|
||||||
}
|
}
|
||||||
|
|
||||||
void BangBangClimate::switch_to_action_(climate::ClimateAction action) {
|
void BangBangClimate::switch_to_action_(climate::ClimateAction action) {
|
||||||
if (action == this->action) {
|
if (action == this->action) {
|
||||||
// already in target mode
|
// already in target mode
|
||||||
@@ -166,6 +180,7 @@ void BangBangClimate::switch_to_action_(climate::ClimateAction action) {
|
|||||||
this->prev_trigger_ = trig;
|
this->prev_trigger_ = trig;
|
||||||
this->publish_state();
|
this->publish_state();
|
||||||
}
|
}
|
||||||
|
|
||||||
void BangBangClimate::change_away_(bool away) {
|
void BangBangClimate::change_away_(bool away) {
|
||||||
if (!away) {
|
if (!away) {
|
||||||
this->target_temperature_low = this->normal_config_.default_temperature_low;
|
this->target_temperature_low = this->normal_config_.default_temperature_low;
|
||||||
@@ -176,22 +191,26 @@ void BangBangClimate::change_away_(bool away) {
|
|||||||
}
|
}
|
||||||
this->preset = away ? climate::CLIMATE_PRESET_AWAY : climate::CLIMATE_PRESET_HOME;
|
this->preset = away ? climate::CLIMATE_PRESET_AWAY : climate::CLIMATE_PRESET_HOME;
|
||||||
}
|
}
|
||||||
|
|
||||||
void BangBangClimate::set_normal_config(const BangBangClimateTargetTempConfig &normal_config) {
|
void BangBangClimate::set_normal_config(const BangBangClimateTargetTempConfig &normal_config) {
|
||||||
this->normal_config_ = normal_config;
|
this->normal_config_ = normal_config;
|
||||||
}
|
}
|
||||||
|
|
||||||
void BangBangClimate::set_away_config(const BangBangClimateTargetTempConfig &away_config) {
|
void BangBangClimate::set_away_config(const BangBangClimateTargetTempConfig &away_config) {
|
||||||
this->supports_away_ = true;
|
this->supports_away_ = true;
|
||||||
this->away_config_ = away_config;
|
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_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; }
|
||||||
void BangBangClimate::set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_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_idle_trigger() const { return this->idle_trigger_; }
|
||||||
Trigger<> *BangBangClimate::get_cool_trigger() const { return this->cool_trigger_; }
|
Trigger<> *BangBangClimate::get_cool_trigger() const { return this->cool_trigger_; }
|
||||||
void BangBangClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
|
|
||||||
Trigger<> *BangBangClimate::get_heat_trigger() const { return this->heat_trigger_; }
|
Trigger<> *BangBangClimate::get_heat_trigger() const { return this->heat_trigger_; }
|
||||||
|
|
||||||
|
void BangBangClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
|
||||||
void BangBangClimate::set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; }
|
void BangBangClimate::set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; }
|
||||||
|
|
||||||
void BangBangClimate::dump_config() {
|
void BangBangClimate::dump_config() {
|
||||||
LOG_CLIMATE("", "Bang Bang Climate", this);
|
LOG_CLIMATE("", "Bang Bang Climate", this);
|
||||||
ESP_LOGCONFIG(TAG,
|
ESP_LOGCONFIG(TAG,
|
||||||
|
|||||||
@@ -25,14 +25,15 @@ class BangBangClimate : public climate::Climate, public Component {
|
|||||||
|
|
||||||
void set_sensor(sensor::Sensor *sensor);
|
void set_sensor(sensor::Sensor *sensor);
|
||||||
void set_humidity_sensor(sensor::Sensor *humidity_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);
|
void set_supports_cool(bool supports_cool);
|
||||||
Trigger<> *get_heat_trigger() const;
|
|
||||||
void set_supports_heat(bool supports_heat);
|
void set_supports_heat(bool supports_heat);
|
||||||
void set_normal_config(const BangBangClimateTargetTempConfig &normal_config);
|
void set_normal_config(const BangBangClimateTargetTempConfig &normal_config);
|
||||||
void set_away_config(const BangBangClimateTargetTempConfig &away_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:
|
protected:
|
||||||
/// Override control to change settings of the climate device.
|
/// Override control to change settings of the climate device.
|
||||||
void control(const climate::ClimateCall &call) override;
|
void control(const climate::ClimateCall &call) override;
|
||||||
@@ -56,16 +57,10 @@ class BangBangClimate : public climate::Climate, public Component {
|
|||||||
*
|
*
|
||||||
* In idle mode, the controller is assumed to have both heating and cooling disabled.
|
* In idle mode, the controller is assumed to have both heating and cooling disabled.
|
||||||
*/
|
*/
|
||||||
Trigger<> *idle_trigger_;
|
Trigger<> *idle_trigger_{nullptr};
|
||||||
/** The trigger to call when the controller should switch to cooling mode.
|
/** The trigger to call when the controller should switch to cooling mode.
|
||||||
*/
|
*/
|
||||||
Trigger<> *cool_trigger_;
|
Trigger<> *cool_trigger_{nullptr};
|
||||||
/** 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.
|
/** 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
|
* A null value for this attribute means that the controller has no heating action
|
||||||
@@ -73,15 +68,23 @@ class BangBangClimate : public climate::Climate, public Component {
|
|||||||
* (blinds open) is possible.
|
* (blinds open) is possible.
|
||||||
*/
|
*/
|
||||||
Trigger<> *heat_trigger_{nullptr};
|
Trigger<> *heat_trigger_{nullptr};
|
||||||
bool supports_heat_{false};
|
|
||||||
/** A reference to the trigger that was previously active.
|
/** A reference to the trigger that was previously active.
|
||||||
*
|
*
|
||||||
* This is so that the previous trigger can be stopped before enabling a new one.
|
* This is so that the previous trigger can be stopped before enabling a new one.
|
||||||
*/
|
*/
|
||||||
Trigger<> *prev_trigger_{nullptr};
|
Trigger<> *prev_trigger_{nullptr};
|
||||||
|
|
||||||
BangBangClimateTargetTempConfig normal_config_{};
|
/** Whether the controller supports cooling/heating
|
||||||
|
*
|
||||||
|
* A false value for this attribute means that the controller has no respective action
|
||||||
|
* (for example a thermostat, where only heating and not-heating is possible).
|
||||||
|
*/
|
||||||
|
bool supports_cool_{false};
|
||||||
|
bool supports_heat_{false};
|
||||||
|
|
||||||
bool supports_away_{false};
|
bool supports_away_{false};
|
||||||
|
|
||||||
|
BangBangClimateTargetTempConfig normal_config_{};
|
||||||
BangBangClimateTargetTempConfig away_config_{};
|
BangBangClimateTargetTempConfig away_config_{};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -33,8 +33,7 @@ class BedJetClimate : public climate::Climate, public BedJetClient, public Polli
|
|||||||
|
|
||||||
climate::ClimateTraits traits() override {
|
climate::ClimateTraits traits() override {
|
||||||
auto traits = climate::ClimateTraits();
|
auto traits = climate::ClimateTraits();
|
||||||
traits.set_supports_action(true);
|
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_ACTION | climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
|
||||||
traits.set_supports_current_temperature(true);
|
|
||||||
traits.set_supported_modes({
|
traits.set_supported_modes({
|
||||||
climate::CLIMATE_MODE_OFF,
|
climate::CLIMATE_MODE_OFF,
|
||||||
climate::CLIMATE_MODE_HEAT,
|
climate::CLIMATE_MODE_HEAT,
|
||||||
|
|||||||
@@ -6,6 +6,42 @@ namespace climate {
|
|||||||
|
|
||||||
static const char *const TAG = "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() {
|
void ClimateCall::perform() {
|
||||||
this->parent_->control_callback_.call(*this);
|
this->parent_->control_callback_.call(*this);
|
||||||
ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str());
|
ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str());
|
||||||
@@ -50,47 +86,46 @@ void ClimateCall::perform() {
|
|||||||
}
|
}
|
||||||
this->parent_->control(*this);
|
this->parent_->control(*this);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClimateCall::validate_() {
|
void ClimateCall::validate_() {
|
||||||
auto traits = this->parent_->get_traits();
|
auto traits = this->parent_->get_traits();
|
||||||
if (this->mode_.has_value()) {
|
if (this->mode_.has_value()) {
|
||||||
auto mode = *this->mode_;
|
auto mode = *this->mode_;
|
||||||
if (!traits.supports_mode(mode)) {
|
if (!traits.supports_mode(mode)) {
|
||||||
ESP_LOGW(TAG, " Mode %s is not supported by this device!", LOG_STR_ARG(climate_mode_to_string(mode)));
|
ESP_LOGW(TAG, " Mode %s not supported", LOG_STR_ARG(climate_mode_to_string(mode)));
|
||||||
this->mode_.reset();
|
this->mode_.reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this->custom_fan_mode_.has_value()) {
|
if (this->custom_fan_mode_.has_value()) {
|
||||||
auto custom_fan_mode = *this->custom_fan_mode_;
|
auto custom_fan_mode = *this->custom_fan_mode_;
|
||||||
if (!traits.supports_custom_fan_mode(custom_fan_mode)) {
|
if (!traits.supports_custom_fan_mode(custom_fan_mode)) {
|
||||||
ESP_LOGW(TAG, " Fan Mode %s is not supported by this device!", custom_fan_mode.c_str());
|
ESP_LOGW(TAG, " Fan Mode %s not supported", custom_fan_mode.c_str());
|
||||||
this->custom_fan_mode_.reset();
|
this->custom_fan_mode_.reset();
|
||||||
}
|
}
|
||||||
} else if (this->fan_mode_.has_value()) {
|
} else if (this->fan_mode_.has_value()) {
|
||||||
auto fan_mode = *this->fan_mode_;
|
auto fan_mode = *this->fan_mode_;
|
||||||
if (!traits.supports_fan_mode(fan_mode)) {
|
if (!traits.supports_fan_mode(fan_mode)) {
|
||||||
ESP_LOGW(TAG, " Fan Mode %s is not supported by this device!",
|
ESP_LOGW(TAG, " Fan Mode %s not supported", LOG_STR_ARG(climate_fan_mode_to_string(fan_mode)));
|
||||||
LOG_STR_ARG(climate_fan_mode_to_string(fan_mode)));
|
|
||||||
this->fan_mode_.reset();
|
this->fan_mode_.reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this->custom_preset_.has_value()) {
|
if (this->custom_preset_.has_value()) {
|
||||||
auto custom_preset = *this->custom_preset_;
|
auto custom_preset = *this->custom_preset_;
|
||||||
if (!traits.supports_custom_preset(custom_preset)) {
|
if (!traits.supports_custom_preset(custom_preset)) {
|
||||||
ESP_LOGW(TAG, " Preset %s is not supported by this device!", custom_preset.c_str());
|
ESP_LOGW(TAG, " Preset %s not supported", custom_preset.c_str());
|
||||||
this->custom_preset_.reset();
|
this->custom_preset_.reset();
|
||||||
}
|
}
|
||||||
} else if (this->preset_.has_value()) {
|
} else if (this->preset_.has_value()) {
|
||||||
auto preset = *this->preset_;
|
auto preset = *this->preset_;
|
||||||
if (!traits.supports_preset(preset)) {
|
if (!traits.supports_preset(preset)) {
|
||||||
ESP_LOGW(TAG, " Preset %s is not supported by this device!", LOG_STR_ARG(climate_preset_to_string(preset)));
|
ESP_LOGW(TAG, " Preset %s not supported", LOG_STR_ARG(climate_preset_to_string(preset)));
|
||||||
this->preset_.reset();
|
this->preset_.reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this->swing_mode_.has_value()) {
|
if (this->swing_mode_.has_value()) {
|
||||||
auto swing_mode = *this->swing_mode_;
|
auto swing_mode = *this->swing_mode_;
|
||||||
if (!traits.supports_swing_mode(swing_mode)) {
|
if (!traits.supports_swing_mode(swing_mode)) {
|
||||||
ESP_LOGW(TAG, " Swing Mode %s is not supported by this device!",
|
ESP_LOGW(TAG, " Swing Mode %s not supported", LOG_STR_ARG(climate_swing_mode_to_string(swing_mode)));
|
||||||
LOG_STR_ARG(climate_swing_mode_to_string(swing_mode)));
|
|
||||||
this->swing_mode_.reset();
|
this->swing_mode_.reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,159 +134,127 @@ void ClimateCall::validate_() {
|
|||||||
if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
|
if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
|
||||||
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
|
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
|
||||||
ESP_LOGW(TAG, " Cannot set target temperature for climate device "
|
ESP_LOGW(TAG, " Cannot set target temperature for climate device "
|
||||||
"with two-point target temperature!");
|
"with two-point target temperature");
|
||||||
this->target_temperature_.reset();
|
this->target_temperature_.reset();
|
||||||
} else if (std::isnan(target)) {
|
} 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();
|
this->target_temperature_.reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this->target_temperature_low_.has_value() || this->target_temperature_high_.has_value()) {
|
if (this->target_temperature_low_.has_value() || this->target_temperature_high_.has_value()) {
|
||||||
if (!traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
|
if (!traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
|
||||||
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
|
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
|
||||||
ESP_LOGW(TAG, " Cannot set low/high target temperature for this device!");
|
ESP_LOGW(TAG, " Cannot set low/high target temperature");
|
||||||
this->target_temperature_low_.reset();
|
this->target_temperature_low_.reset();
|
||||||
this->target_temperature_high_.reset();
|
this->target_temperature_high_.reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this->target_temperature_low_.has_value() && std::isnan(*this->target_temperature_low_)) {
|
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();
|
this->target_temperature_low_.reset();
|
||||||
}
|
}
|
||||||
if (this->target_temperature_high_.has_value() && std::isnan(*this->target_temperature_high_)) {
|
if (this->target_temperature_high_.has_value() && std::isnan(*this->target_temperature_high_)) {
|
||||||
ESP_LOGW(TAG, " Target temperature low must not be NAN!");
|
ESP_LOGW(TAG, " Target temperature high must not be NAN");
|
||||||
this->target_temperature_high_.reset();
|
this->target_temperature_high_.reset();
|
||||||
}
|
}
|
||||||
if (this->target_temperature_low_.has_value() && this->target_temperature_high_.has_value()) {
|
if (this->target_temperature_low_.has_value() && this->target_temperature_high_.has_value()) {
|
||||||
float low = *this->target_temperature_low_;
|
float low = *this->target_temperature_low_;
|
||||||
float high = *this->target_temperature_high_;
|
float high = *this->target_temperature_high_;
|
||||||
if (low > high) {
|
if (low > high) {
|
||||||
ESP_LOGW(TAG, " Target temperature low %.2f must be smaller than target temperature high %.2f!", low, high);
|
ESP_LOGW(TAG, " Target temperature low %.2f must be less than target temperature high %.2f", low, high);
|
||||||
this->target_temperature_low_.reset();
|
this->target_temperature_low_.reset();
|
||||||
this->target_temperature_high_.reset();
|
this->target_temperature_high_.reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ClimateCall &ClimateCall::set_mode(ClimateMode mode) {
|
ClimateCall &ClimateCall::set_mode(ClimateMode mode) {
|
||||||
this->mode_ = mode;
|
this->mode_ = mode;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
ClimateCall &ClimateCall::set_mode(const std::string &mode) {
|
ClimateCall &ClimateCall::set_mode(const std::string &mode) {
|
||||||
if (str_equals_case_insensitive(mode, "OFF")) {
|
for (const auto &mode_entry : CLIMATE_MODES_BY_STR) {
|
||||||
this->set_mode(CLIMATE_MODE_OFF);
|
if (str_equals_case_insensitive(mode, mode_entry.str)) {
|
||||||
} else if (str_equals_case_insensitive(mode, "AUTO")) {
|
this->set_mode(static_cast<ClimateMode>(mode_entry.value));
|
||||||
this->set_mode(CLIMATE_MODE_AUTO);
|
return *this;
|
||||||
} 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;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
ClimateCall &ClimateCall::set_fan_mode(ClimateFanMode fan_mode) {
|
ClimateCall &ClimateCall::set_fan_mode(ClimateFanMode fan_mode) {
|
||||||
this->fan_mode_ = fan_mode;
|
this->fan_mode_ = fan_mode;
|
||||||
this->custom_fan_mode_.reset();
|
this->custom_fan_mode_.reset();
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) {
|
ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) {
|
||||||
if (str_equals_case_insensitive(fan_mode, "ON")) {
|
for (const auto &mode_entry : CLIMATE_FAN_MODES_BY_STR) {
|
||||||
this->set_fan_mode(CLIMATE_FAN_ON);
|
if (str_equals_case_insensitive(fan_mode, mode_entry.str)) {
|
||||||
} else if (str_equals_case_insensitive(fan_mode, "OFF")) {
|
this->set_fan_mode(static_cast<ClimateFanMode>(mode_entry.value));
|
||||||
this->set_fan_mode(CLIMATE_FAN_OFF);
|
return *this;
|
||||||
} else if (str_equals_case_insensitive(fan_mode, "AUTO")) {
|
}
|
||||||
this->set_fan_mode(CLIMATE_FAN_AUTO);
|
}
|
||||||
} else if (str_equals_case_insensitive(fan_mode, "LOW")) {
|
|
||||||
this->set_fan_mode(CLIMATE_FAN_LOW);
|
|
||||||
} else if (str_equals_case_insensitive(fan_mode, "MEDIUM")) {
|
|
||||||
this->set_fan_mode(CLIMATE_FAN_MEDIUM);
|
|
||||||
} else if (str_equals_case_insensitive(fan_mode, "HIGH")) {
|
|
||||||
this->set_fan_mode(CLIMATE_FAN_HIGH);
|
|
||||||
} else if (str_equals_case_insensitive(fan_mode, "MIDDLE")) {
|
|
||||||
this->set_fan_mode(CLIMATE_FAN_MIDDLE);
|
|
||||||
} else if (str_equals_case_insensitive(fan_mode, "FOCUS")) {
|
|
||||||
this->set_fan_mode(CLIMATE_FAN_FOCUS);
|
|
||||||
} else if (str_equals_case_insensitive(fan_mode, "DIFFUSE")) {
|
|
||||||
this->set_fan_mode(CLIMATE_FAN_DIFFUSE);
|
|
||||||
} else if (str_equals_case_insensitive(fan_mode, "QUIET")) {
|
|
||||||
this->set_fan_mode(CLIMATE_FAN_QUIET);
|
|
||||||
} else {
|
|
||||||
if (this->parent_->get_traits().supports_custom_fan_mode(fan_mode)) {
|
if (this->parent_->get_traits().supports_custom_fan_mode(fan_mode)) {
|
||||||
this->custom_fan_mode_ = fan_mode;
|
this->custom_fan_mode_ = fan_mode;
|
||||||
this->fan_mode_.reset();
|
this->fan_mode_.reset();
|
||||||
} else {
|
} else {
|
||||||
ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), fan_mode.c_str());
|
ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), fan_mode.c_str());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
ClimateCall &ClimateCall::set_fan_mode(optional<std::string> fan_mode) {
|
ClimateCall &ClimateCall::set_fan_mode(optional<std::string> fan_mode) {
|
||||||
if (fan_mode.has_value()) {
|
if (fan_mode.has_value()) {
|
||||||
this->set_fan_mode(fan_mode.value());
|
this->set_fan_mode(fan_mode.value());
|
||||||
}
|
}
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
ClimateCall &ClimateCall::set_preset(ClimatePreset preset) {
|
ClimateCall &ClimateCall::set_preset(ClimatePreset preset) {
|
||||||
this->preset_ = preset;
|
this->preset_ = preset;
|
||||||
this->custom_preset_.reset();
|
this->custom_preset_.reset();
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
ClimateCall &ClimateCall::set_preset(const std::string &preset) {
|
ClimateCall &ClimateCall::set_preset(const std::string &preset) {
|
||||||
if (str_equals_case_insensitive(preset, "ECO")) {
|
for (const auto &preset_entry : CLIMATE_PRESETS_BY_STR) {
|
||||||
this->set_preset(CLIMATE_PRESET_ECO);
|
if (str_equals_case_insensitive(preset, preset_entry.str)) {
|
||||||
} else if (str_equals_case_insensitive(preset, "AWAY")) {
|
this->set_preset(static_cast<ClimatePreset>(preset_entry.value));
|
||||||
this->set_preset(CLIMATE_PRESET_AWAY);
|
return *this;
|
||||||
} else if (str_equals_case_insensitive(preset, "BOOST")) {
|
}
|
||||||
this->set_preset(CLIMATE_PRESET_BOOST);
|
}
|
||||||
} else if (str_equals_case_insensitive(preset, "COMFORT")) {
|
|
||||||
this->set_preset(CLIMATE_PRESET_COMFORT);
|
|
||||||
} else if (str_equals_case_insensitive(preset, "HOME")) {
|
|
||||||
this->set_preset(CLIMATE_PRESET_HOME);
|
|
||||||
} else if (str_equals_case_insensitive(preset, "SLEEP")) {
|
|
||||||
this->set_preset(CLIMATE_PRESET_SLEEP);
|
|
||||||
} else if (str_equals_case_insensitive(preset, "ACTIVITY")) {
|
|
||||||
this->set_preset(CLIMATE_PRESET_ACTIVITY);
|
|
||||||
} else if (str_equals_case_insensitive(preset, "NONE")) {
|
|
||||||
this->set_preset(CLIMATE_PRESET_NONE);
|
|
||||||
} else {
|
|
||||||
if (this->parent_->get_traits().supports_custom_preset(preset)) {
|
if (this->parent_->get_traits().supports_custom_preset(preset)) {
|
||||||
this->custom_preset_ = preset;
|
this->custom_preset_ = preset;
|
||||||
this->preset_.reset();
|
this->preset_.reset();
|
||||||
} else {
|
} else {
|
||||||
ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), preset.c_str());
|
ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), preset.c_str());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
ClimateCall &ClimateCall::set_preset(optional<std::string> preset) {
|
ClimateCall &ClimateCall::set_preset(optional<std::string> preset) {
|
||||||
if (preset.has_value()) {
|
if (preset.has_value()) {
|
||||||
this->set_preset(preset.value());
|
this->set_preset(preset.value());
|
||||||
}
|
}
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
ClimateCall &ClimateCall::set_swing_mode(ClimateSwingMode swing_mode) {
|
ClimateCall &ClimateCall::set_swing_mode(ClimateSwingMode swing_mode) {
|
||||||
this->swing_mode_ = swing_mode;
|
this->swing_mode_ = swing_mode;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
ClimateCall &ClimateCall::set_swing_mode(const std::string &swing_mode) {
|
ClimateCall &ClimateCall::set_swing_mode(const std::string &swing_mode) {
|
||||||
if (str_equals_case_insensitive(swing_mode, "OFF")) {
|
for (const auto &mode_entry : CLIMATE_SWING_MODES_BY_STR) {
|
||||||
this->set_swing_mode(CLIMATE_SWING_OFF);
|
if (str_equals_case_insensitive(swing_mode, mode_entry.str)) {
|
||||||
} else if (str_equals_case_insensitive(swing_mode, "BOTH")) {
|
this->set_swing_mode(static_cast<ClimateSwingMode>(mode_entry.value));
|
||||||
this->set_swing_mode(CLIMATE_SWING_BOTH);
|
return *this;
|
||||||
} 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;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,59 +262,71 @@ ClimateCall &ClimateCall::set_target_temperature(float target_temperature) {
|
|||||||
this->target_temperature_ = target_temperature;
|
this->target_temperature_ = target_temperature;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
ClimateCall &ClimateCall::set_target_temperature_low(float target_temperature_low) {
|
ClimateCall &ClimateCall::set_target_temperature_low(float target_temperature_low) {
|
||||||
this->target_temperature_low_ = target_temperature_low;
|
this->target_temperature_low_ = target_temperature_low;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
ClimateCall &ClimateCall::set_target_temperature_high(float target_temperature_high) {
|
ClimateCall &ClimateCall::set_target_temperature_high(float target_temperature_high) {
|
||||||
this->target_temperature_high_ = target_temperature_high;
|
this->target_temperature_high_ = target_temperature_high;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
ClimateCall &ClimateCall::set_target_humidity(float target_humidity) {
|
ClimateCall &ClimateCall::set_target_humidity(float target_humidity) {
|
||||||
this->target_humidity_ = target_humidity;
|
this->target_humidity_ = target_humidity;
|
||||||
return *this;
|
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() 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_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_temperature_high() const { return this->target_temperature_high_; }
|
||||||
const optional<float> &ClimateCall::get_target_humidity() const { return this->target_humidity_; }
|
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<ClimateFanMode> &ClimateCall::get_fan_mode() const { return this->fan_mode_; }
|
||||||
const optional<std::string> &ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; }
|
|
||||||
const optional<ClimatePreset> &ClimateCall::get_preset() const { return this->preset_; }
|
|
||||||
const optional<std::string> &ClimateCall::get_custom_preset() const { return this->custom_preset_; }
|
|
||||||
const optional<ClimateSwingMode> &ClimateCall::get_swing_mode() const { return this->swing_mode_; }
|
const optional<ClimateSwingMode> &ClimateCall::get_swing_mode() const { return this->swing_mode_; }
|
||||||
|
const optional<ClimatePreset> &ClimateCall::get_preset() const { return this->preset_; }
|
||||||
|
const optional<std::string> &ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; }
|
||||||
|
const optional<std::string> &ClimateCall::get_custom_preset() const { return this->custom_preset_; }
|
||||||
|
|
||||||
ClimateCall &ClimateCall::set_target_temperature_high(optional<float> target_temperature_high) {
|
ClimateCall &ClimateCall::set_target_temperature_high(optional<float> target_temperature_high) {
|
||||||
this->target_temperature_high_ = target_temperature_high;
|
this->target_temperature_high_ = target_temperature_high;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
ClimateCall &ClimateCall::set_target_temperature_low(optional<float> target_temperature_low) {
|
ClimateCall &ClimateCall::set_target_temperature_low(optional<float> target_temperature_low) {
|
||||||
this->target_temperature_low_ = target_temperature_low;
|
this->target_temperature_low_ = target_temperature_low;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
ClimateCall &ClimateCall::set_target_temperature(optional<float> target_temperature) {
|
ClimateCall &ClimateCall::set_target_temperature(optional<float> target_temperature) {
|
||||||
this->target_temperature_ = target_temperature;
|
this->target_temperature_ = target_temperature;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
ClimateCall &ClimateCall::set_target_humidity(optional<float> target_humidity) {
|
ClimateCall &ClimateCall::set_target_humidity(optional<float> target_humidity) {
|
||||||
this->target_humidity_ = target_humidity;
|
this->target_humidity_ = target_humidity;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
ClimateCall &ClimateCall::set_mode(optional<ClimateMode> mode) {
|
ClimateCall &ClimateCall::set_mode(optional<ClimateMode> mode) {
|
||||||
this->mode_ = mode;
|
this->mode_ = mode;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
ClimateCall &ClimateCall::set_fan_mode(optional<ClimateFanMode> fan_mode) {
|
ClimateCall &ClimateCall::set_fan_mode(optional<ClimateFanMode> fan_mode) {
|
||||||
this->fan_mode_ = fan_mode;
|
this->fan_mode_ = fan_mode;
|
||||||
this->custom_fan_mode_.reset();
|
this->custom_fan_mode_.reset();
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
ClimateCall &ClimateCall::set_preset(optional<ClimatePreset> preset) {
|
ClimateCall &ClimateCall::set_preset(optional<ClimatePreset> preset) {
|
||||||
this->preset_ = preset;
|
this->preset_ = preset;
|
||||||
this->custom_preset_.reset();
|
this->custom_preset_.reset();
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
ClimateCall &ClimateCall::set_swing_mode(optional<ClimateSwingMode> swing_mode) {
|
ClimateCall &ClimateCall::set_swing_mode(optional<ClimateSwingMode> swing_mode) {
|
||||||
this->swing_mode_ = swing_mode;
|
this->swing_mode_ = swing_mode;
|
||||||
return *this;
|
return *this;
|
||||||
@@ -336,6 +351,7 @@ optional<ClimateDeviceRestoreState> Climate::restore_state_() {
|
|||||||
return {};
|
return {};
|
||||||
return recovered;
|
return recovered;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Climate::save_state_() {
|
void Climate::save_state_() {
|
||||||
#if (defined(USE_ESP_IDF) || (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0))) && \
|
#if (defined(USE_ESP_IDF) || (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0))) && \
|
||||||
!defined(CLANG_TIDY)
|
!defined(CLANG_TIDY)
|
||||||
@@ -398,6 +414,7 @@ void Climate::save_state_() {
|
|||||||
|
|
||||||
this->rtc_.save(&state);
|
this->rtc_.save(&state);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Climate::publish_state() {
|
void Climate::publish_state() {
|
||||||
ESP_LOGD(TAG, "'%s' - Sending state:", this->name_.c_str());
|
ESP_LOGD(TAG, "'%s' - Sending state:", this->name_.c_str());
|
||||||
auto traits = this->get_traits();
|
auto traits = this->get_traits();
|
||||||
@@ -469,16 +486,20 @@ ClimateTraits Climate::get_traits() {
|
|||||||
void Climate::set_visual_min_temperature_override(float visual_min_temperature_override) {
|
void Climate::set_visual_min_temperature_override(float visual_min_temperature_override) {
|
||||||
this->visual_min_temperature_override_ = 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) {
|
void Climate::set_visual_max_temperature_override(float visual_max_temperature_override) {
|
||||||
this->visual_max_temperature_override_ = visual_max_temperature_override;
|
this->visual_max_temperature_override_ = visual_max_temperature_override;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Climate::set_visual_temperature_step_override(float target, float current) {
|
void Climate::set_visual_temperature_step_override(float target, float current) {
|
||||||
this->visual_target_temperature_step_override_ = target;
|
this->visual_target_temperature_step_override_ = target;
|
||||||
this->visual_current_temperature_step_override_ = current;
|
this->visual_current_temperature_step_override_ = current;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Climate::set_visual_min_humidity_override(float visual_min_humidity_override) {
|
void Climate::set_visual_min_humidity_override(float visual_min_humidity_override) {
|
||||||
this->visual_min_humidity_override_ = 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) {
|
void Climate::set_visual_max_humidity_override(float visual_max_humidity_override) {
|
||||||
this->visual_max_humidity_override_ = visual_max_humidity_override;
|
this->visual_max_humidity_override_ = visual_max_humidity_override;
|
||||||
}
|
}
|
||||||
@@ -510,6 +531,7 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) {
|
|||||||
}
|
}
|
||||||
return call;
|
return call;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClimateDeviceRestoreState::apply(Climate *climate) {
|
void ClimateDeviceRestoreState::apply(Climate *climate) {
|
||||||
auto traits = climate->get_traits();
|
auto traits = climate->get_traits();
|
||||||
climate->mode = this->mode;
|
climate->mode = this->mode;
|
||||||
@@ -579,7 +601,7 @@ void Climate::dump_traits_(const char *tag) {
|
|||||||
auto traits = this->get_traits();
|
auto traits = this->get_traits();
|
||||||
ESP_LOGCONFIG(tag, "ClimateTraits:");
|
ESP_LOGCONFIG(tag, "ClimateTraits:");
|
||||||
ESP_LOGCONFIG(tag,
|
ESP_LOGCONFIG(tag,
|
||||||
" [x] Visual settings:\n"
|
" Visual settings:\n"
|
||||||
" - Min temperature: %.1f\n"
|
" - Min temperature: %.1f\n"
|
||||||
" - Max temperature: %.1f\n"
|
" - Max temperature: %.1f\n"
|
||||||
" - Temperature step:\n"
|
" - Temperature step:\n"
|
||||||
@@ -598,47 +620,47 @@ void Climate::dump_traits_(const char *tag) {
|
|||||||
}
|
}
|
||||||
if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
|
if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
|
||||||
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
|
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
|
||||||
ESP_LOGCONFIG(tag, " [x] Supports two-point target temperature");
|
ESP_LOGCONFIG(tag, " Supports two-point target temperature");
|
||||||
}
|
}
|
||||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) {
|
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) {
|
||||||
ESP_LOGCONFIG(tag, " [x] Supports current temperature");
|
ESP_LOGCONFIG(tag, " Supports current temperature");
|
||||||
}
|
}
|
||||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
|
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
|
||||||
ESP_LOGCONFIG(tag, " [x] Supports target humidity");
|
ESP_LOGCONFIG(tag, " Supports target humidity");
|
||||||
}
|
}
|
||||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) {
|
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) {
|
||||||
ESP_LOGCONFIG(tag, " [x] Supports current humidity");
|
ESP_LOGCONFIG(tag, " Supports current humidity");
|
||||||
}
|
}
|
||||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) {
|
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) {
|
||||||
ESP_LOGCONFIG(tag, " [x] Supports action");
|
ESP_LOGCONFIG(tag, " Supports action");
|
||||||
}
|
}
|
||||||
if (!traits.get_supported_modes().empty()) {
|
if (!traits.get_supported_modes().empty()) {
|
||||||
ESP_LOGCONFIG(tag, " [x] Supported modes:");
|
ESP_LOGCONFIG(tag, " Supported modes:");
|
||||||
for (ClimateMode m : traits.get_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()) {
|
if (!traits.get_supported_fan_modes().empty()) {
|
||||||
ESP_LOGCONFIG(tag, " [x] Supported fan modes:");
|
ESP_LOGCONFIG(tag, " Supported fan modes:");
|
||||||
for (ClimateFanMode m : traits.get_supported_fan_modes())
|
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()) {
|
if (!traits.get_supported_custom_fan_modes().empty()) {
|
||||||
ESP_LOGCONFIG(tag, " [x] Supported custom fan modes:");
|
ESP_LOGCONFIG(tag, " Supported custom fan modes:");
|
||||||
for (const std::string &s : traits.get_supported_custom_fan_modes())
|
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()) {
|
if (!traits.get_supported_presets().empty()) {
|
||||||
ESP_LOGCONFIG(tag, " [x] Supported presets:");
|
ESP_LOGCONFIG(tag, " Supported presets:");
|
||||||
for (ClimatePreset p : traits.get_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()) {
|
if (!traits.get_supported_custom_presets().empty()) {
|
||||||
ESP_LOGCONFIG(tag, " [x] Supported custom presets:");
|
ESP_LOGCONFIG(tag, " Supported custom presets:");
|
||||||
for (const std::string &s : traits.get_supported_custom_presets())
|
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()) {
|
if (!traits.get_supported_swing_modes().empty()) {
|
||||||
ESP_LOGCONFIG(tag, " [x] Supported swing modes:");
|
ESP_LOGCONFIG(tag, " Supported swing modes:");
|
||||||
for (ClimateSwingMode m : traits.get_supported_swing_modes())
|
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,30 +93,31 @@ class ClimateCall {
|
|||||||
|
|
||||||
void perform();
|
void perform();
|
||||||
|
|
||||||
const optional<ClimateMode> &get_mode() const;
|
|
||||||
const optional<float> &get_target_temperature() const;
|
const optional<float> &get_target_temperature() const;
|
||||||
const optional<float> &get_target_temperature_low() const;
|
const optional<float> &get_target_temperature_low() const;
|
||||||
const optional<float> &get_target_temperature_high() const;
|
const optional<float> &get_target_temperature_high() const;
|
||||||
const optional<float> &get_target_humidity() const;
|
const optional<float> &get_target_humidity() const;
|
||||||
|
|
||||||
|
const optional<ClimateMode> &get_mode() const;
|
||||||
const optional<ClimateFanMode> &get_fan_mode() const;
|
const optional<ClimateFanMode> &get_fan_mode() const;
|
||||||
const optional<ClimateSwingMode> &get_swing_mode() const;
|
const optional<ClimateSwingMode> &get_swing_mode() const;
|
||||||
const optional<std::string> &get_custom_fan_mode() const;
|
|
||||||
const optional<ClimatePreset> &get_preset() const;
|
const optional<ClimatePreset> &get_preset() const;
|
||||||
|
const optional<std::string> &get_custom_fan_mode() const;
|
||||||
const optional<std::string> &get_custom_preset() const;
|
const optional<std::string> &get_custom_preset() const;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void validate_();
|
void validate_();
|
||||||
|
|
||||||
Climate *const parent_;
|
Climate *const parent_;
|
||||||
optional<ClimateMode> mode_;
|
|
||||||
optional<float> target_temperature_;
|
optional<float> target_temperature_;
|
||||||
optional<float> target_temperature_low_;
|
optional<float> target_temperature_low_;
|
||||||
optional<float> target_temperature_high_;
|
optional<float> target_temperature_high_;
|
||||||
optional<float> target_humidity_;
|
optional<float> target_humidity_;
|
||||||
|
optional<ClimateMode> mode_;
|
||||||
optional<ClimateFanMode> fan_mode_;
|
optional<ClimateFanMode> fan_mode_;
|
||||||
optional<ClimateSwingMode> swing_mode_;
|
optional<ClimateSwingMode> swing_mode_;
|
||||||
optional<std::string> custom_fan_mode_;
|
|
||||||
optional<ClimatePreset> preset_;
|
optional<ClimatePreset> preset_;
|
||||||
|
optional<std::string> custom_fan_mode_;
|
||||||
optional<std::string> custom_preset_;
|
optional<std::string> custom_preset_;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -169,47 +170,6 @@ class Climate : public EntityBase {
|
|||||||
public:
|
public:
|
||||||
Climate() {}
|
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
|
/** 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.
|
* (using publish_state), this callback will be called.
|
||||||
*
|
*
|
||||||
@@ -251,6 +211,47 @@ class Climate : public EntityBase {
|
|||||||
void set_visual_min_humidity_override(float visual_min_humidity_override);
|
void set_visual_min_humidity_override(float visual_min_humidity_override);
|
||||||
void set_visual_max_humidity_override(float visual_max_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:
|
protected:
|
||||||
friend ClimateCall;
|
friend ClimateCall;
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ static const char *const TAG = "climate_ir";
|
|||||||
|
|
||||||
climate::ClimateTraits ClimateIR::traits() {
|
climate::ClimateTraits ClimateIR::traits() {
|
||||||
auto traits = climate::ClimateTraits();
|
auto traits = climate::ClimateTraits();
|
||||||
traits.set_supports_current_temperature(this->sensor_ != nullptr);
|
if (this->sensor_ != nullptr) {
|
||||||
|
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
|
||||||
|
}
|
||||||
|
|
||||||
traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_HEAT_COOL});
|
traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_HEAT_COOL});
|
||||||
if (this->supports_cool_)
|
if (this->supports_cool_)
|
||||||
traits.add_supported_mode(climate::CLIMATE_MODE_COOL);
|
traits.add_supported_mode(climate::CLIMATE_MODE_COOL);
|
||||||
@@ -19,7 +22,6 @@ climate::ClimateTraits ClimateIR::traits() {
|
|||||||
if (this->supports_fan_only_)
|
if (this->supports_fan_only_)
|
||||||
traits.add_supported_mode(climate::CLIMATE_MODE_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_min_temperature(this->minimum_temperature_);
|
||||||
traits.set_visual_max_temperature(this->maximum_temperature_);
|
traits.set_visual_max_temperature(this->maximum_temperature_);
|
||||||
traits.set_visual_temperature_step(this->temperature_step_);
|
traits.set_visual_temperature_step(this->temperature_step_);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#include "cover.h"
|
#include "cover.h"
|
||||||
#include "esphome/core/log.h"
|
|
||||||
#include <strings.h>
|
#include <strings.h>
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace cover {
|
namespace cover {
|
||||||
@@ -144,21 +144,7 @@ CoverCall &CoverCall::set_stop(bool stop) {
|
|||||||
bool CoverCall::get_stop() const { return this->stop_; }
|
bool CoverCall::get_stop() const { return this->stop_; }
|
||||||
|
|
||||||
CoverCall Cover::make_call() { return {this}; }
|
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::add_on_state_callback(std::function<void()> &&f) { this->state_callback_.add(std::move(f)); }
|
||||||
void Cover::publish_state(bool save) {
|
void Cover::publish_state(bool save) {
|
||||||
this->position = clamp(this->position, 0.0f, 1.0f);
|
this->position = clamp(this->position, 0.0f, 1.0f);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "esphome/core/entity_base.h"
|
#include "esphome/core/entity_base.h"
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/core/preferences.h"
|
#include "esphome/core/preferences.h"
|
||||||
|
|
||||||
#include "cover_traits.h"
|
#include "cover_traits.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
@@ -125,25 +126,6 @@ class Cover : public EntityBase, public EntityBase_DeviceClass {
|
|||||||
|
|
||||||
/// Construct a new cover call used to control the cover.
|
/// Construct a new cover call used to control the cover.
|
||||||
CoverCall make_call();
|
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);
|
void add_on_state_callback(std::function<void()> &&f);
|
||||||
|
|
||||||
|
|||||||
@@ -241,9 +241,7 @@ uint8_t DaikinArcClimate::humidity_() {
|
|||||||
|
|
||||||
climate::ClimateTraits DaikinArcClimate::traits() {
|
climate::ClimateTraits DaikinArcClimate::traits() {
|
||||||
climate::ClimateTraits traits = climate_ir::ClimateIR::traits();
|
climate::ClimateTraits traits = climate_ir::ClimateIR::traits();
|
||||||
traits.set_supports_current_temperature(true);
|
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE | climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY);
|
||||||
traits.set_supports_current_humidity(false);
|
|
||||||
traits.set_supports_target_humidity(true);
|
|
||||||
traits.set_visual_min_humidity(38);
|
traits.set_visual_min_humidity(38);
|
||||||
traits.set_visual_max_humidity(52);
|
traits.set_visual_max_humidity(52);
|
||||||
return traits;
|
return traits;
|
||||||
|
|||||||
@@ -82,16 +82,14 @@ class DemoClimate : public climate::Climate, public Component {
|
|||||||
climate::ClimateTraits traits{};
|
climate::ClimateTraits traits{};
|
||||||
switch (type_) {
|
switch (type_) {
|
||||||
case DemoClimateType::TYPE_1:
|
case DemoClimateType::TYPE_1:
|
||||||
traits.set_supports_current_temperature(true);
|
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE | climate::CLIMATE_SUPPORTS_ACTION);
|
||||||
traits.set_supported_modes({
|
traits.set_supported_modes({
|
||||||
climate::CLIMATE_MODE_OFF,
|
climate::CLIMATE_MODE_OFF,
|
||||||
climate::CLIMATE_MODE_HEAT,
|
climate::CLIMATE_MODE_HEAT,
|
||||||
});
|
});
|
||||||
traits.set_supports_action(true);
|
|
||||||
traits.set_visual_temperature_step(0.5);
|
traits.set_visual_temperature_step(0.5);
|
||||||
break;
|
break;
|
||||||
case DemoClimateType::TYPE_2:
|
case DemoClimateType::TYPE_2:
|
||||||
traits.set_supports_current_temperature(false);
|
|
||||||
traits.set_supported_modes({
|
traits.set_supported_modes({
|
||||||
climate::CLIMATE_MODE_OFF,
|
climate::CLIMATE_MODE_OFF,
|
||||||
climate::CLIMATE_MODE_HEAT,
|
climate::CLIMATE_MODE_HEAT,
|
||||||
@@ -100,7 +98,7 @@ class DemoClimate : public climate::Climate, public Component {
|
|||||||
climate::CLIMATE_MODE_DRY,
|
climate::CLIMATE_MODE_DRY,
|
||||||
climate::CLIMATE_MODE_FAN_ONLY,
|
climate::CLIMATE_MODE_FAN_ONLY,
|
||||||
});
|
});
|
||||||
traits.set_supports_action(true);
|
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_ACTION);
|
||||||
traits.set_supported_fan_modes({
|
traits.set_supported_fan_modes({
|
||||||
climate::CLIMATE_FAN_ON,
|
climate::CLIMATE_FAN_ON,
|
||||||
climate::CLIMATE_FAN_OFF,
|
climate::CLIMATE_FAN_OFF,
|
||||||
@@ -123,8 +121,8 @@ class DemoClimate : public climate::Climate, public Component {
|
|||||||
traits.set_supported_custom_presets({"My Preset"});
|
traits.set_supported_custom_presets({"My Preset"});
|
||||||
break;
|
break;
|
||||||
case DemoClimateType::TYPE_3:
|
case DemoClimateType::TYPE_3:
|
||||||
traits.set_supports_current_temperature(true);
|
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE |
|
||||||
traits.set_supports_two_point_target_temperature(true);
|
climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE);
|
||||||
traits.set_supported_modes({
|
traits.set_supported_modes({
|
||||||
climate::CLIMATE_MODE_OFF,
|
climate::CLIMATE_MODE_OFF,
|
||||||
climate::CLIMATE_MODE_COOL,
|
climate::CLIMATE_MODE_COOL,
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ bool EPaperBase::is_idle_() {
|
|||||||
if (this->busy_pin_ == nullptr) {
|
if (this->busy_pin_ == nullptr) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return !this->busy_pin_->digital_read();
|
return this->busy_pin_->digital_read();
|
||||||
}
|
}
|
||||||
|
|
||||||
void EPaperBase::reset() {
|
void EPaperBase::reset() {
|
||||||
|
|||||||
@@ -850,6 +850,16 @@ async def to_code(config):
|
|||||||
Path(__file__).parent / "post_build.py.script",
|
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:
|
if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
|
||||||
cg.add_platformio_option("framework", "espidf")
|
cg.add_platformio_option("framework", "espidf")
|
||||||
cg.add_build_flag("-DUSE_ESP_IDF")
|
cg.add_build_flag("-DUSE_ESP_IDF")
|
||||||
@@ -876,6 +886,7 @@ async def to_code(config):
|
|||||||
add_idf_sdkconfig_option("CONFIG_AUTOSTART_ARDUINO", True)
|
add_idf_sdkconfig_option("CONFIG_AUTOSTART_ARDUINO", True)
|
||||||
add_idf_sdkconfig_option("CONFIG_MBEDTLS_PSK_MODES", True)
|
add_idf_sdkconfig_option("CONFIG_MBEDTLS_PSK_MODES", True)
|
||||||
add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", 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")
|
cg.add_build_flag("-Wno-nonnull-compare")
|
||||||
|
|
||||||
|
|||||||
71
esphome/components/esp32/iram_fix.py.script
Normal file
71
esphome/components/esp32/iram_fix.py.script
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
# pylint: disable=E0602
|
||||||
|
Import("env") # noqa
|
||||||
|
|
||||||
|
# IRAM size for testing mode (2MB - large enough to accommodate grouped tests)
|
||||||
|
TESTING_IRAM_SIZE = 0x200000
|
||||||
|
|
||||||
|
|
||||||
|
def patch_idf_linker_script(source, target, env):
|
||||||
|
"""Patch ESP-IDF linker script to increase IRAM size for testing mode."""
|
||||||
|
# Check if we're in testing mode by looking for the define
|
||||||
|
build_flags = env.get("BUILD_FLAGS", [])
|
||||||
|
testing_mode = any("-DESPHOME_TESTING_MODE" in flag for flag in build_flags)
|
||||||
|
|
||||||
|
if not testing_mode:
|
||||||
|
return
|
||||||
|
|
||||||
|
# For ESP-IDF, the linker scripts are generated in the build directory
|
||||||
|
build_dir = env.subst("$BUILD_DIR")
|
||||||
|
|
||||||
|
# The memory.ld file is directly in the build directory
|
||||||
|
memory_ld = os.path.join(build_dir, "memory.ld")
|
||||||
|
|
||||||
|
if not os.path.exists(memory_ld):
|
||||||
|
print(f"ESPHome: Warning - could not find linker script at {memory_ld}")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(memory_ld, "r") as f:
|
||||||
|
content = f.read()
|
||||||
|
except OSError as e:
|
||||||
|
print(f"ESPHome: Error reading linker script: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if this file contains iram0_0_seg
|
||||||
|
if 'iram0_0_seg' not in content:
|
||||||
|
print(f"ESPHome: Warning - iram0_0_seg not found in {memory_ld}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Look for iram0_0_seg definition and increase its length
|
||||||
|
# ESP-IDF format can be:
|
||||||
|
# iram0_0_seg (RX) : org = 0x40080000, len = 0x20000 + 0x0
|
||||||
|
# or more complex with nested parentheses:
|
||||||
|
# iram0_0_seg (RX) : org = (0x40370000 + 0x4000), len = (((0x403CB700 - (0x40378000 - 0x3FC88000)) - 0x3FC88000) + 0x8000 - 0x4000)
|
||||||
|
# We want to change len to TESTING_IRAM_SIZE for testing
|
||||||
|
|
||||||
|
# Use a more robust approach: find the line and manually parse it
|
||||||
|
lines = content.split('\n')
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if 'iram0_0_seg' in line and 'len' in line:
|
||||||
|
# Find the position of "len = " and replace everything after it until the end of the statement
|
||||||
|
match = re.search(r'(iram0_0_seg\s*\([^)]*\)\s*:\s*org\s*=\s*(?:\([^)]+\)|0x[0-9a-fA-F]+)\s*,\s*len\s*=\s*)(.+?)(\s*)$', line)
|
||||||
|
if match:
|
||||||
|
lines[i] = f"{match.group(1)}{TESTING_IRAM_SIZE:#x}{match.group(3)}"
|
||||||
|
break
|
||||||
|
|
||||||
|
updated = '\n'.join(lines)
|
||||||
|
|
||||||
|
if updated != content:
|
||||||
|
with open(memory_ld, "w") as f:
|
||||||
|
f.write(updated)
|
||||||
|
print(f"ESPHome: Patched IRAM size to {TESTING_IRAM_SIZE:#x} in {memory_ld} for testing mode")
|
||||||
|
else:
|
||||||
|
print(f"ESPHome: Warning - could not patch iram0_0_seg in {memory_ld}")
|
||||||
|
|
||||||
|
|
||||||
|
# Hook into the build process before linking
|
||||||
|
# For ESP-IDF, we need to run this after the linker scripts are generated
|
||||||
|
env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", patch_idf_linker_script)
|
||||||
@@ -95,7 +95,7 @@ async def to_code(config):
|
|||||||
if framework_ver >= cv.Version(5, 5, 0):
|
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/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/eppp_link", ref="1.1.3")
|
||||||
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.5.11")
|
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.6.1")
|
||||||
else:
|
else:
|
||||||
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.13.0")
|
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")
|
esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0")
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
from esphome import automation
|
from esphome import automation
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components import binary_sensor, esp32_ble, output
|
from esphome.components import binary_sensor, esp32_ble, improv_base, output
|
||||||
from esphome.components.esp32_ble import BTLoggers
|
from esphome.components.esp32_ble import BTLoggers
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_ID, CONF_ON_STATE, CONF_TRIGGER_ID
|
from esphome.const import CONF_ID, CONF_ON_STATE, CONF_TRIGGER_ID
|
||||||
|
|
||||||
AUTO_LOAD = ["esp32_ble_server"]
|
AUTO_LOAD = ["esp32_ble_server", "improv_base"]
|
||||||
CODEOWNERS = ["@jesserockz"]
|
CODEOWNERS = ["@jesserockz"]
|
||||||
DEPENDENCIES = ["wifi", "esp32"]
|
DEPENDENCIES = ["wifi", "esp32"]
|
||||||
|
|
||||||
@@ -20,6 +20,7 @@ CONF_ON_STOP = "on_stop"
|
|||||||
CONF_STATUS_INDICATOR = "status_indicator"
|
CONF_STATUS_INDICATOR = "status_indicator"
|
||||||
CONF_WIFI_TIMEOUT = "wifi_timeout"
|
CONF_WIFI_TIMEOUT = "wifi_timeout"
|
||||||
|
|
||||||
|
|
||||||
improv_ns = cg.esphome_ns.namespace("improv")
|
improv_ns = cg.esphome_ns.namespace("improv")
|
||||||
Error = improv_ns.enum("Error")
|
Error = improv_ns.enum("Error")
|
||||||
State = improv_ns.enum("State")
|
State = improv_ns.enum("State")
|
||||||
@@ -43,7 +44,8 @@ ESP32ImprovStoppedTrigger = esp32_improv_ns.class_(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.Schema(
|
CONFIG_SCHEMA = (
|
||||||
|
cv.Schema(
|
||||||
{
|
{
|
||||||
cv.GenerateID(): cv.declare_id(ESP32ImprovComponent),
|
cv.GenerateID(): cv.declare_id(ESP32ImprovComponent),
|
||||||
cv.Required(CONF_AUTHORIZER): cv.Any(
|
cv.Required(CONF_AUTHORIZER): cv.Any(
|
||||||
@@ -75,12 +77,16 @@ CONFIG_SCHEMA = cv.Schema(
|
|||||||
),
|
),
|
||||||
cv.Optional(CONF_ON_START): automation.validate_automation(
|
cv.Optional(CONF_ON_START): automation.validate_automation(
|
||||||
{
|
{
|
||||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESP32ImprovStartTrigger),
|
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||||
|
ESP32ImprovStartTrigger
|
||||||
|
),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
cv.Optional(CONF_ON_STATE): automation.validate_automation(
|
cv.Optional(CONF_ON_STATE): automation.validate_automation(
|
||||||
{
|
{
|
||||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESP32ImprovStateTrigger),
|
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||||
|
ESP32ImprovStateTrigger
|
||||||
|
),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
cv.Optional(CONF_ON_STOP): automation.validate_automation(
|
cv.Optional(CONF_ON_STOP): automation.validate_automation(
|
||||||
@@ -91,7 +97,10 @@ CONFIG_SCHEMA = cv.Schema(
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
).extend(cv.COMPONENT_SCHEMA)
|
)
|
||||||
|
.extend(improv_base.IMPROV_SCHEMA)
|
||||||
|
.extend(cv.COMPONENT_SCHEMA)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
@@ -102,7 +111,8 @@ async def to_code(config):
|
|||||||
await cg.register_component(var, config)
|
await cg.register_component(var, config)
|
||||||
|
|
||||||
cg.add_define("USE_IMPROV")
|
cg.add_define("USE_IMPROV")
|
||||||
cg.add_library("improv/Improv", "1.2.4")
|
|
||||||
|
await improv_base.setup_improv_core(var, config)
|
||||||
|
|
||||||
cg.add(var.set_identify_duration(config[CONF_IDENTIFY_DURATION]))
|
cg.add(var.set_identify_duration(config[CONF_IDENTIFY_DURATION]))
|
||||||
cg.add(var.set_authorized_duration(config[CONF_AUTHORIZED_DURATION]))
|
cg.add(var.set_authorized_duration(config[CONF_AUTHORIZED_DURATION]))
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
#include "esp32_improv_component.h"
|
#include "esp32_improv_component.h"
|
||||||
|
|
||||||
|
#include "esphome/components/bytebuffer/bytebuffer.h"
|
||||||
#include "esphome/components/esp32_ble/ble.h"
|
#include "esphome/components/esp32_ble/ble.h"
|
||||||
#include "esphome/components/esp32_ble_server/ble_2902.h"
|
#include "esphome/components/esp32_ble_server/ble_2902.h"
|
||||||
#include "esphome/core/application.h"
|
#include "esphome/core/application.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
#include "esphome/components/bytebuffer/bytebuffer.h"
|
|
||||||
|
|
||||||
#ifdef USE_ESP32
|
#ifdef USE_ESP32
|
||||||
|
|
||||||
@@ -384,17 +384,32 @@ void ESP32ImprovComponent::check_wifi_connection_() {
|
|||||||
this->connecting_sta_ = {};
|
this->connecting_sta_ = {};
|
||||||
this->cancel_timeout("wifi-connect-timeout");
|
this->cancel_timeout("wifi-connect-timeout");
|
||||||
|
|
||||||
std::vector<std::string> urls = {ESPHOME_MY_LINK};
|
// Build URL list with minimal allocations
|
||||||
|
// Maximum 3 URLs: custom next_url + ESPHOME_MY_LINK + webserver URL
|
||||||
|
std::string url_strings[3];
|
||||||
|
size_t url_count = 0;
|
||||||
|
|
||||||
|
// 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;
|
||||||
#ifdef USE_WEBSERVER
|
#ifdef USE_WEBSERVER
|
||||||
for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) {
|
for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) {
|
||||||
if (ip.is_ip4()) {
|
if (ip.is_ip4()) {
|
||||||
std::string webserver_url = "http://" + ip.str() + ":" + to_string(USE_WEBSERVER_PORT);
|
char url_buffer[64];
|
||||||
urls.push_back(webserver_url);
|
snprintf(url_buffer, sizeof(url_buffer), "http://%s:%d", ip.str().c_str(), USE_WEBSERVER_PORT);
|
||||||
|
url_strings[url_count++] = url_buffer;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
std::vector<uint8_t> data = improv::build_rpc_response(improv::WIFI_SETTINGS, urls);
|
// Pass to build_rpc_response using vector constructor from iterators to avoid extra copies
|
||||||
|
std::vector<uint8_t> data = improv::build_rpc_response(
|
||||||
|
improv::WIFI_SETTINGS, std::vector<std::string>(url_strings, url_strings + url_count));
|
||||||
this->send_response_(data);
|
this->send_response_(data);
|
||||||
} else if (this->is_active() && this->state_ != improv::STATE_PROVISIONED) {
|
} else if (this->is_active() && this->state_ != improv::STATE_PROVISIONED) {
|
||||||
ESP_LOGD(TAG, "WiFi provisioned externally");
|
ESP_LOGD(TAG, "WiFi provisioned externally");
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
#include "esphome/components/esp32_ble_server/ble_characteristic.h"
|
#include "esphome/components/esp32_ble_server/ble_characteristic.h"
|
||||||
#include "esphome/components/esp32_ble_server/ble_server.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"
|
#include "esphome/components/wifi/wifi_component.h"
|
||||||
|
|
||||||
#ifdef USE_ESP32_IMPROV_STATE_CALLBACK
|
#ifdef USE_ESP32_IMPROV_STATE_CALLBACK
|
||||||
@@ -32,7 +33,7 @@ namespace esp32_improv {
|
|||||||
|
|
||||||
using namespace esp32_ble_server;
|
using namespace esp32_ble_server;
|
||||||
|
|
||||||
class ESP32ImprovComponent : public Component {
|
class ESP32ImprovComponent : public Component, public improv_base::ImprovBase {
|
||||||
public:
|
public:
|
||||||
ESP32ImprovComponent();
|
ESP32ImprovComponent();
|
||||||
void dump_config() override;
|
void dump_config() override;
|
||||||
|
|||||||
39
esphome/components/espnow/packet_transport/__init__.py
Normal file
39
esphome/components/espnow/packet_transport/__init__.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
"""ESP-NOW transport platform for packet_transport component."""
|
||||||
|
|
||||||
|
import esphome.codegen as cg
|
||||||
|
from esphome.components.packet_transport import (
|
||||||
|
PacketTransport,
|
||||||
|
new_packet_transport,
|
||||||
|
transport_schema,
|
||||||
|
)
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.core import HexInt
|
||||||
|
from esphome.cpp_types import PollingComponent
|
||||||
|
|
||||||
|
from .. import ESPNowComponent, espnow_ns
|
||||||
|
|
||||||
|
CODEOWNERS = ["@EasilyBoredEngineer"]
|
||||||
|
DEPENDENCIES = ["espnow"]
|
||||||
|
|
||||||
|
ESPNowTransport = espnow_ns.class_("ESPNowTransport", PacketTransport, PollingComponent)
|
||||||
|
|
||||||
|
CONF_ESPNOW_ID = "espnow_id"
|
||||||
|
CONF_PEER_ADDRESS = "peer_address"
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = transport_schema(ESPNowTransport).extend(
|
||||||
|
{
|
||||||
|
cv.GenerateID(CONF_ESPNOW_ID): cv.use_id(ESPNowComponent),
|
||||||
|
cv.Optional(CONF_PEER_ADDRESS, default="FF:FF:FF:FF:FF:FF"): cv.mac_address,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
"""Set up the ESP-NOW transport component."""
|
||||||
|
var, _ = await new_packet_transport(config)
|
||||||
|
|
||||||
|
await cg.register_parented(var, config[CONF_ESPNOW_ID])
|
||||||
|
|
||||||
|
# Set peer address - convert MAC to parts array like ESP-NOW does
|
||||||
|
mac = config[CONF_PEER_ADDRESS]
|
||||||
|
cg.add(var.set_peer_address([HexInt(x) for x in mac.parts]))
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
#include "espnow_transport.h"
|
||||||
|
|
||||||
|
#ifdef USE_ESP32
|
||||||
|
|
||||||
|
#include "esphome/core/application.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace espnow {
|
||||||
|
|
||||||
|
static const char *const TAG = "espnow.transport";
|
||||||
|
|
||||||
|
bool ESPNowTransport::should_send() { return this->parent_ != nullptr && !this->parent_->is_failed(); }
|
||||||
|
|
||||||
|
void ESPNowTransport::setup() {
|
||||||
|
packet_transport::PacketTransport::setup();
|
||||||
|
|
||||||
|
if (this->parent_ == nullptr) {
|
||||||
|
ESP_LOGE(TAG, "ESPNow component not set");
|
||||||
|
this->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Registering ESP-NOW handlers");
|
||||||
|
ESP_LOGI(TAG, "Peer address: %02X:%02X:%02X:%02X:%02X:%02X", this->peer_address_[0], this->peer_address_[1],
|
||||||
|
this->peer_address_[2], this->peer_address_[3], this->peer_address_[4], this->peer_address_[5]);
|
||||||
|
|
||||||
|
// Register received handler
|
||||||
|
this->parent_->register_received_handler(static_cast<ESPNowReceivedPacketHandler *>(this));
|
||||||
|
|
||||||
|
// Register broadcasted handler
|
||||||
|
this->parent_->register_broadcasted_handler(static_cast<ESPNowBroadcastedHandler *>(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ESPNowTransport::update() {
|
||||||
|
packet_transport::PacketTransport::update();
|
||||||
|
this->updated_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ESPNowTransport::send_packet(const std::vector<uint8_t> &buf) const {
|
||||||
|
if (this->parent_ == nullptr) {
|
||||||
|
ESP_LOGE(TAG, "ESPNow component not set");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buf.empty()) {
|
||||||
|
ESP_LOGW(TAG, "Attempted to send empty packet");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buf.size() > ESP_NOW_MAX_DATA_LEN) {
|
||||||
|
ESP_LOGE(TAG, "Packet too large: %zu bytes (max %d)", buf.size(), ESP_NOW_MAX_DATA_LEN);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send to configured peer address
|
||||||
|
this->parent_->send(this->peer_address_.data(), buf.data(), buf.size(), [](esp_err_t err) {
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "Send failed: %d", err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ESPNowTransport::on_received(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) {
|
||||||
|
ESP_LOGV(TAG, "Received packet of size %u from %02X:%02X:%02X:%02X:%02X:%02X", size, info.src_addr[0],
|
||||||
|
info.src_addr[1], info.src_addr[2], info.src_addr[3], info.src_addr[4], info.src_addr[5]);
|
||||||
|
|
||||||
|
if (data == nullptr || size == 0) {
|
||||||
|
ESP_LOGW(TAG, "Received empty or null packet");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->packet_buffer_.resize(size);
|
||||||
|
memcpy(this->packet_buffer_.data(), data, size);
|
||||||
|
this->process_(this->packet_buffer_);
|
||||||
|
return false; // Allow other handlers to run
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ESPNowTransport::on_broadcasted(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) {
|
||||||
|
ESP_LOGV(TAG, "Received broadcast packet of size %u from %02X:%02X:%02X:%02X:%02X:%02X", size, info.src_addr[0],
|
||||||
|
info.src_addr[1], info.src_addr[2], info.src_addr[3], info.src_addr[4], info.src_addr[5]);
|
||||||
|
|
||||||
|
if (data == nullptr || size == 0) {
|
||||||
|
ESP_LOGW(TAG, "Received empty or null broadcast packet");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->packet_buffer_.resize(size);
|
||||||
|
memcpy(this->packet_buffer_.data(), data, size);
|
||||||
|
this->process_(this->packet_buffer_);
|
||||||
|
return false; // Allow other handlers to run
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace espnow
|
||||||
|
} // namespace esphome
|
||||||
|
|
||||||
|
#endif // USE_ESP32
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "../espnow_component.h"
|
||||||
|
|
||||||
|
#ifdef USE_ESP32
|
||||||
|
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/components/packet_transport/packet_transport.h"
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace espnow {
|
||||||
|
|
||||||
|
class ESPNowTransport : public packet_transport::PacketTransport,
|
||||||
|
public Parented<ESPNowComponent>,
|
||||||
|
public ESPNowReceivedPacketHandler,
|
||||||
|
public ESPNowBroadcastedHandler {
|
||||||
|
public:
|
||||||
|
void setup() override;
|
||||||
|
void update() override;
|
||||||
|
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
|
||||||
|
|
||||||
|
void set_peer_address(peer_address_t address) {
|
||||||
|
memcpy(this->peer_address_.data(), address.data(), ESP_NOW_ETH_ALEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ESPNow handler interface
|
||||||
|
bool on_received(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override;
|
||||||
|
bool on_broadcasted(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void send_packet(const std::vector<uint8_t> &buf) const override;
|
||||||
|
size_t get_max_packet_size() override { return ESP_NOW_MAX_DATA_LEN; }
|
||||||
|
bool should_send() override;
|
||||||
|
|
||||||
|
peer_address_t peer_address_{{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}};
|
||||||
|
std::vector<uint8_t> packet_buffer_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace espnow
|
||||||
|
} // namespace esphome
|
||||||
|
|
||||||
|
#endif // USE_ESP32
|
||||||
@@ -38,7 +38,6 @@ IS_PLATFORM_COMPONENT = True
|
|||||||
|
|
||||||
fan_ns = cg.esphome_ns.namespace("fan")
|
fan_ns = cg.esphome_ns.namespace("fan")
|
||||||
Fan = fan_ns.class_("Fan", cg.EntityBase)
|
Fan = fan_ns.class_("Fan", cg.EntityBase)
|
||||||
FanState = fan_ns.class_("Fan", Fan, cg.Component)
|
|
||||||
|
|
||||||
FanDirection = fan_ns.enum("FanDirection", is_class=True)
|
FanDirection = fan_ns.enum("FanDirection", is_class=True)
|
||||||
FAN_DIRECTION_ENUM = {
|
FAN_DIRECTION_ENUM = {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "esphome/core/component.h"
|
|
||||||
#include "esphome/core/automation.h"
|
#include "esphome/core/automation.h"
|
||||||
#include "fan_state.h"
|
#include "esphome/core/component.h"
|
||||||
|
#include "fan.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace fan {
|
namespace fan {
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
#include "fan_state.h"
|
|
||||||
|
|
||||||
namespace esphome {
|
|
||||||
namespace fan {
|
|
||||||
|
|
||||||
static const char *const TAG = "fan";
|
|
||||||
|
|
||||||
void FanState::setup() {
|
|
||||||
auto restore = this->restore_state_();
|
|
||||||
if (restore)
|
|
||||||
restore->to_call(*this).perform();
|
|
||||||
}
|
|
||||||
float FanState::get_setup_priority() const { return setup_priority::DATA - 1.0f; }
|
|
||||||
|
|
||||||
} // namespace fan
|
|
||||||
} // namespace esphome
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "esphome/core/component.h"
|
|
||||||
#include "fan.h"
|
|
||||||
|
|
||||||
namespace esphome {
|
|
||||||
namespace fan {
|
|
||||||
|
|
||||||
enum ESPDEPRECATED("LegacyFanDirection members are deprecated, use FanDirection instead.",
|
|
||||||
"2022.2") LegacyFanDirection {
|
|
||||||
FAN_DIRECTION_FORWARD = 0,
|
|
||||||
FAN_DIRECTION_REVERSE = 1
|
|
||||||
};
|
|
||||||
|
|
||||||
class ESPDEPRECATED("FanState is deprecated, use Fan instead.", "2022.2") FanState : public Fan, public Component {
|
|
||||||
public:
|
|
||||||
FanState() = default;
|
|
||||||
|
|
||||||
/// Get the traits of this fan.
|
|
||||||
FanTraits get_traits() override { return this->traits_; }
|
|
||||||
/// Set the traits of this fan (i.e. what features it supports).
|
|
||||||
void set_traits(const FanTraits &traits) { this->traits_ = traits; }
|
|
||||||
|
|
||||||
void setup() override;
|
|
||||||
float get_setup_priority() const override;
|
|
||||||
|
|
||||||
protected:
|
|
||||||
void control(const FanCall &call) override { this->publish_state(); }
|
|
||||||
|
|
||||||
FanTraits traits_{};
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace fan
|
|
||||||
} // namespace esphome
|
|
||||||
@@ -65,7 +65,7 @@ HaierClimateBase::HaierClimateBase()
|
|||||||
{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH});
|
{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,
|
this->traits_.set_supported_swing_modes({climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH,
|
||||||
climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL});
|
climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL});
|
||||||
this->traits_.set_supports_current_temperature(true);
|
this->traits_.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
|
||||||
}
|
}
|
||||||
|
|
||||||
HaierClimateBase::~HaierClimateBase() {}
|
HaierClimateBase::~HaierClimateBase() {}
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ void HDC1080Component::setup() {
|
|||||||
|
|
||||||
// if configuration fails - there is a problem
|
// if configuration fails - there is a problem
|
||||||
if (this->write_register(HDC1080_CMD_CONFIGURATION, config, 2) != i2c::ERROR_OK) {
|
if (this->write_register(HDC1080_CMD_CONFIGURATION, config, 2) != i2c::ERROR_OK) {
|
||||||
this->mark_failed();
|
ESP_LOGW(TAG, "Failed to configure HDC1080");
|
||||||
|
this->status_set_warning();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,31 +6,42 @@
|
|||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace improv_base {
|
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_() {
|
std::string ImprovBase::get_formatted_next_url_() {
|
||||||
if (this->next_url_.empty()) {
|
if (this->next_url_.empty()) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
std::string copy = this->next_url_;
|
|
||||||
// Device name
|
|
||||||
std::size_t pos = this->next_url_.find("{{device_name}}");
|
|
||||||
if (pos != std::string::npos) {
|
|
||||||
const std::string &device_name = App.get_name();
|
|
||||||
copy.replace(pos, 15, device_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ip address
|
std::string formatted_url = this->next_url_;
|
||||||
pos = this->next_url_.find("{{ip_address}}");
|
|
||||||
if (pos != std::string::npos) {
|
// 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()) {
|
for (auto &ip : network::get_ip_addresses()) {
|
||||||
if (ip.is_ip4()) {
|
if (ip.is_ip4()) {
|
||||||
std::string ipa = ip.str();
|
replace_all_in_place(formatted_url, IP_ADDRESS_PLACEHOLDER, IP_ADDRESS_PLACEHOLDER_LEN, ip.str());
|
||||||
copy.replace(pos, 14, ipa);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return copy;
|
// Note: {{esphome_version}} is replaced at code generation time in Python
|
||||||
|
|
||||||
|
return formatted_url;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace improv_base
|
} // namespace improv_base
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ void AirConditioner::control(const ClimateCall &call) {
|
|||||||
|
|
||||||
ClimateTraits AirConditioner::traits() {
|
ClimateTraits AirConditioner::traits() {
|
||||||
auto traits = ClimateTraits();
|
auto traits = ClimateTraits();
|
||||||
traits.set_supports_current_temperature(true);
|
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
|
||||||
traits.set_visual_min_temperature(17);
|
traits.set_visual_min_temperature(17);
|
||||||
traits.set_visual_max_temperature(30);
|
traits.set_visual_max_temperature(30);
|
||||||
traits.set_visual_temperature_step(0.5);
|
traits.set_visual_temperature_step(0.5);
|
||||||
|
|||||||
@@ -30,6 +30,19 @@ wave_4_3 = DriverChip(
|
|||||||
"blue": [14, 38, 18, 17, 10],
|
"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(
|
wave_4_3.extend(
|
||||||
"ESP32-S3-TOUCH-LCD-7-800X480",
|
"ESP32-S3-TOUCH-LCD-7-800X480",
|
||||||
enable_pin=[{"ch422g": None, "number": 2}, {"ch422g": None, "number": 6}],
|
enable_pin=[{"ch422g": None, "number": 2}, {"ch422g": None, "number": 6}],
|
||||||
|
|||||||
@@ -52,8 +52,9 @@ const uint8_t MITSUBISHI_BYTE16 = 0x00;
|
|||||||
|
|
||||||
climate::ClimateTraits MitsubishiClimate::traits() {
|
climate::ClimateTraits MitsubishiClimate::traits() {
|
||||||
auto traits = climate::ClimateTraits();
|
auto traits = climate::ClimateTraits();
|
||||||
traits.set_supports_current_temperature(this->sensor_ != nullptr);
|
if (this->sensor_ != nullptr) {
|
||||||
traits.set_supports_action(false);
|
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
|
||||||
|
}
|
||||||
traits.set_visual_min_temperature(MITSUBISHI_TEMP_MIN);
|
traits.set_visual_min_temperature(MITSUBISHI_TEMP_MIN);
|
||||||
traits.set_visual_max_temperature(MITSUBISHI_TEMP_MAX);
|
traits.set_visual_max_temperature(MITSUBISHI_TEMP_MAX);
|
||||||
traits.set_visual_temperature_step(1.0f);
|
traits.set_visual_temperature_step(1.0f);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
#ifdef USE_MQTT
|
#ifdef USE_MQTT
|
||||||
#ifdef USE_FAN
|
#ifdef USE_FAN
|
||||||
|
|
||||||
#include "esphome/components/fan/fan_state.h"
|
#include "esphome/components/fan/fan.h"
|
||||||
#include "mqtt_component.h"
|
#include "mqtt_component.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
|
|||||||
@@ -69,6 +69,12 @@ void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscovery
|
|||||||
if (traits.supports_color_capability(ColorCapability::BRIGHTNESS))
|
if (traits.supports_color_capability(ColorCapability::BRIGHTNESS))
|
||||||
root["brightness"] = true;
|
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()) {
|
if (this->state_->supports_effects()) {
|
||||||
root["effect"] = true;
|
root["effect"] = true;
|
||||||
JsonArray effect_list = root[MQTT_EFFECT_LIST].to<JsonArray>();
|
JsonArray effect_list = root[MQTT_EFFECT_LIST].to<JsonArray>();
|
||||||
|
|||||||
@@ -54,11 +54,10 @@ void PIDClimate::control(const climate::ClimateCall &call) {
|
|||||||
}
|
}
|
||||||
climate::ClimateTraits PIDClimate::traits() {
|
climate::ClimateTraits PIDClimate::traits() {
|
||||||
auto traits = climate::ClimateTraits();
|
auto traits = climate::ClimateTraits();
|
||||||
traits.set_supports_current_temperature(true);
|
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE | climate::CLIMATE_SUPPORTS_ACTION);
|
||||||
traits.set_supports_two_point_target_temperature(false);
|
|
||||||
|
|
||||||
if (this->humidity_sensor_ != nullptr)
|
if (this->humidity_sensor_ != nullptr)
|
||||||
traits.set_supports_current_humidity(true);
|
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY);
|
||||||
|
|
||||||
traits.set_supported_modes({climate::CLIMATE_MODE_OFF});
|
traits.set_supported_modes({climate::CLIMATE_MODE_OFF});
|
||||||
if (supports_cool_())
|
if (supports_cool_())
|
||||||
@@ -68,7 +67,6 @@ climate::ClimateTraits PIDClimate::traits() {
|
|||||||
if (supports_heat_() && supports_cool_())
|
if (supports_heat_() && supports_cool_())
|
||||||
traits.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL);
|
traits.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL);
|
||||||
|
|
||||||
traits.set_supports_action(true);
|
|
||||||
return traits;
|
return traits;
|
||||||
}
|
}
|
||||||
void PIDClimate::dump_config() {
|
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_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_DC_DC_OVER_CURRENT = "fault_dc_dc_over_current"
|
||||||
CONF_FAULT_CODE = "fault_code"
|
CONF_FAULT_CODE = "fault_code"
|
||||||
CONF_WARNUNG_LOW_PV_ENERGY = "warnung_low_pv_energy"
|
CONF_WARNING_LOW_PV_ENERGY = "warning_low_pv_energy"
|
||||||
CONF_WARNING_HIGH_AC_INPUT_DURING_BUS_SOFT_START = (
|
CONF_WARNING_HIGH_AC_INPUT_DURING_BUS_SOFT_START = (
|
||||||
"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_WARNING_BATTERY_TOO_LOW_TO_CHARGE,
|
||||||
CONF_FAULT_DC_DC_OVER_CURRENT,
|
CONF_FAULT_DC_DC_OVER_CURRENT,
|
||||||
CONF_FAULT_CODE,
|
CONF_FAULT_CODE,
|
||||||
CONF_WARNUNG_LOW_PV_ENERGY,
|
CONF_WARNING_LOW_PV_ENERGY,
|
||||||
CONF_WARNING_HIGH_AC_INPUT_DURING_BUS_SOFT_START,
|
CONF_WARNING_HIGH_AC_INPUT_DURING_BUS_SOFT_START,
|
||||||
CONF_WARNING_BATTERY_EQUALIZATION,
|
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()) {
|
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);
|
ESP_LOGD(TAG, "Will write: %s out of value %f / %02.0f", tmp, state, state);
|
||||||
this->parent_->switch_command(std::string(tmp));
|
this->parent_->queue_command(std::string(tmp));
|
||||||
} else {
|
} else {
|
||||||
ESP_LOGD(TAG, "Will not write: %s as it is not in list of allowed values", tmp);
|
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,6 +7,7 @@
|
|||||||
#include "esphome/components/uart/uart.h"
|
#include "esphome/components/uart/uart.h"
|
||||||
#include "esphome/core/automation.h"
|
#include "esphome/core/automation.h"
|
||||||
#include "esphome/core/component.h"
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace pipsolar {
|
namespace pipsolar {
|
||||||
@@ -28,10 +29,17 @@ struct PollingCommand {
|
|||||||
bool needs_update;
|
bool needs_update;
|
||||||
};
|
};
|
||||||
|
|
||||||
#define PIPSOLAR_VALUED_ENTITY_(type, name, polling_command, value_type) \
|
struct QFLAGValues {
|
||||||
protected: \
|
esphome::optional<bool> silence_buzzer_open_buzzer;
|
||||||
value_type value_##name##_; \
|
esphome::optional<bool> overload_bypass_function;
|
||||||
PIPSOLAR_ENTITY_(type, name, polling_command)
|
esphome::optional<bool> lcd_escape_to_default;
|
||||||
|
esphome::optional<bool> overload_restart_function;
|
||||||
|
esphome::optional<bool> over_temperature_restart_function;
|
||||||
|
esphome::optional<bool> backlight_on;
|
||||||
|
esphome::optional<bool> alarm_on_when_primary_source_interrupt;
|
||||||
|
esphome::optional<bool> fault_code_record;
|
||||||
|
esphome::optional<bool> power_saving;
|
||||||
|
};
|
||||||
|
|
||||||
#define PIPSOLAR_ENTITY_(type, name, polling_command) \
|
#define PIPSOLAR_ENTITY_(type, name, polling_command) \
|
||||||
protected: \
|
protected: \
|
||||||
@@ -43,126 +51,123 @@ struct PollingCommand {
|
|||||||
this->add_polling_command_(#polling_command, POLLING_##polling_command); \
|
this->add_polling_command_(#polling_command, POLLING_##polling_command); \
|
||||||
}
|
}
|
||||||
|
|
||||||
#define PIPSOLAR_SENSOR(name, polling_command, value_type) \
|
#define PIPSOLAR_SENSOR(name, polling_command) PIPSOLAR_ENTITY_(sensor::Sensor, name, polling_command)
|
||||||
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_SWITCH(name, polling_command) PIPSOLAR_ENTITY_(switch_::Switch, name, polling_command)
|
||||||
#define PIPSOLAR_BINARY_SENSOR(name, polling_command, value_type) \
|
#define PIPSOLAR_BINARY_SENSOR(name, polling_command) \
|
||||||
PIPSOLAR_VALUED_ENTITY_(binary_sensor::BinarySensor, name, polling_command, value_type)
|
PIPSOLAR_ENTITY_(binary_sensor::BinarySensor, name, polling_command)
|
||||||
#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)
|
#define PIPSOLAR_TEXT_SENSOR(name, polling_command) PIPSOLAR_ENTITY_(text_sensor::TextSensor, name, polling_command)
|
||||||
|
|
||||||
class Pipsolar : public uart::UARTDevice, public PollingComponent {
|
class Pipsolar : public uart::UARTDevice, public PollingComponent {
|
||||||
// QPIGS values
|
// QPIGS values
|
||||||
PIPSOLAR_SENSOR(grid_voltage, QPIGS, float)
|
PIPSOLAR_SENSOR(grid_voltage, QPIGS)
|
||||||
PIPSOLAR_SENSOR(grid_frequency, QPIGS, float)
|
PIPSOLAR_SENSOR(grid_frequency, QPIGS)
|
||||||
PIPSOLAR_SENSOR(ac_output_voltage, QPIGS, float)
|
PIPSOLAR_SENSOR(ac_output_voltage, QPIGS)
|
||||||
PIPSOLAR_SENSOR(ac_output_frequency, QPIGS, float)
|
PIPSOLAR_SENSOR(ac_output_frequency, QPIGS)
|
||||||
PIPSOLAR_SENSOR(ac_output_apparent_power, QPIGS, int)
|
PIPSOLAR_SENSOR(ac_output_apparent_power, QPIGS)
|
||||||
PIPSOLAR_SENSOR(ac_output_active_power, QPIGS, int)
|
PIPSOLAR_SENSOR(ac_output_active_power, QPIGS)
|
||||||
PIPSOLAR_SENSOR(output_load_percent, QPIGS, int)
|
PIPSOLAR_SENSOR(output_load_percent, QPIGS)
|
||||||
PIPSOLAR_SENSOR(bus_voltage, QPIGS, int)
|
PIPSOLAR_SENSOR(bus_voltage, QPIGS)
|
||||||
PIPSOLAR_SENSOR(battery_voltage, QPIGS, float)
|
PIPSOLAR_SENSOR(battery_voltage, QPIGS)
|
||||||
PIPSOLAR_SENSOR(battery_charging_current, QPIGS, int)
|
PIPSOLAR_SENSOR(battery_charging_current, QPIGS)
|
||||||
PIPSOLAR_SENSOR(battery_capacity_percent, QPIGS, int)
|
PIPSOLAR_SENSOR(battery_capacity_percent, QPIGS)
|
||||||
PIPSOLAR_SENSOR(inverter_heat_sink_temperature, QPIGS, int)
|
PIPSOLAR_SENSOR(inverter_heat_sink_temperature, QPIGS)
|
||||||
PIPSOLAR_SENSOR(pv_input_current_for_battery, QPIGS, float)
|
PIPSOLAR_SENSOR(pv_input_current_for_battery, QPIGS)
|
||||||
PIPSOLAR_SENSOR(pv_input_voltage, QPIGS, float)
|
PIPSOLAR_SENSOR(pv_input_voltage, QPIGS)
|
||||||
PIPSOLAR_SENSOR(battery_voltage_scc, QPIGS, float)
|
PIPSOLAR_SENSOR(battery_voltage_scc, QPIGS)
|
||||||
PIPSOLAR_SENSOR(battery_discharge_current, QPIGS, int)
|
PIPSOLAR_SENSOR(battery_discharge_current, QPIGS)
|
||||||
PIPSOLAR_BINARY_SENSOR(add_sbu_priority_version, QPIGS, int)
|
PIPSOLAR_BINARY_SENSOR(add_sbu_priority_version, QPIGS)
|
||||||
PIPSOLAR_BINARY_SENSOR(configuration_status, QPIGS, int)
|
PIPSOLAR_BINARY_SENSOR(configuration_status, QPIGS)
|
||||||
PIPSOLAR_BINARY_SENSOR(scc_firmware_version, QPIGS, int)
|
PIPSOLAR_BINARY_SENSOR(scc_firmware_version, QPIGS)
|
||||||
PIPSOLAR_BINARY_SENSOR(load_status, QPIGS, int)
|
PIPSOLAR_BINARY_SENSOR(load_status, QPIGS)
|
||||||
PIPSOLAR_BINARY_SENSOR(battery_voltage_to_steady_while_charging, QPIGS, int)
|
PIPSOLAR_BINARY_SENSOR(battery_voltage_to_steady_while_charging, QPIGS)
|
||||||
PIPSOLAR_BINARY_SENSOR(charging_status, QPIGS, int)
|
PIPSOLAR_BINARY_SENSOR(charging_status, QPIGS)
|
||||||
PIPSOLAR_BINARY_SENSOR(scc_charging_status, QPIGS, int)
|
PIPSOLAR_BINARY_SENSOR(scc_charging_status, QPIGS)
|
||||||
PIPSOLAR_BINARY_SENSOR(ac_charging_status, QPIGS, int)
|
PIPSOLAR_BINARY_SENSOR(ac_charging_status, QPIGS)
|
||||||
PIPSOLAR_SENSOR(battery_voltage_offset_for_fans_on, QPIGS, int) //.1 scale
|
PIPSOLAR_SENSOR(battery_voltage_offset_for_fans_on, QPIGS) //.1 scale
|
||||||
PIPSOLAR_SENSOR(eeprom_version, QPIGS, int)
|
PIPSOLAR_SENSOR(eeprom_version, QPIGS)
|
||||||
PIPSOLAR_SENSOR(pv_charging_power, QPIGS, int)
|
PIPSOLAR_SENSOR(pv_charging_power, QPIGS)
|
||||||
PIPSOLAR_BINARY_SENSOR(charging_to_floating_mode, QPIGS, int)
|
PIPSOLAR_BINARY_SENSOR(charging_to_floating_mode, QPIGS)
|
||||||
PIPSOLAR_BINARY_SENSOR(switch_on, QPIGS, int)
|
PIPSOLAR_BINARY_SENSOR(switch_on, QPIGS)
|
||||||
PIPSOLAR_BINARY_SENSOR(dustproof_installed, QPIGS, int)
|
PIPSOLAR_BINARY_SENSOR(dustproof_installed, QPIGS)
|
||||||
|
|
||||||
// QPIRI values
|
// QPIRI values
|
||||||
PIPSOLAR_SENSOR(grid_rating_voltage, QPIRI, float)
|
PIPSOLAR_SENSOR(grid_rating_voltage, QPIRI)
|
||||||
PIPSOLAR_SENSOR(grid_rating_current, QPIRI, float)
|
PIPSOLAR_SENSOR(grid_rating_current, QPIRI)
|
||||||
PIPSOLAR_SENSOR(ac_output_rating_voltage, QPIRI, float)
|
PIPSOLAR_SENSOR(ac_output_rating_voltage, QPIRI)
|
||||||
PIPSOLAR_SENSOR(ac_output_rating_frequency, QPIRI, float)
|
PIPSOLAR_SENSOR(ac_output_rating_frequency, QPIRI)
|
||||||
PIPSOLAR_SENSOR(ac_output_rating_current, QPIRI, float)
|
PIPSOLAR_SENSOR(ac_output_rating_current, QPIRI)
|
||||||
PIPSOLAR_SENSOR(ac_output_rating_apparent_power, QPIRI, int)
|
PIPSOLAR_SENSOR(ac_output_rating_apparent_power, QPIRI)
|
||||||
PIPSOLAR_SENSOR(ac_output_rating_active_power, QPIRI, int)
|
PIPSOLAR_SENSOR(ac_output_rating_active_power, QPIRI)
|
||||||
PIPSOLAR_SENSOR(battery_rating_voltage, QPIRI, float)
|
PIPSOLAR_SENSOR(battery_rating_voltage, QPIRI)
|
||||||
PIPSOLAR_SENSOR(battery_recharge_voltage, QPIRI, float)
|
PIPSOLAR_SENSOR(battery_recharge_voltage, QPIRI)
|
||||||
PIPSOLAR_SENSOR(battery_under_voltage, QPIRI, float)
|
PIPSOLAR_SENSOR(battery_under_voltage, QPIRI)
|
||||||
PIPSOLAR_SENSOR(battery_bulk_voltage, QPIRI, float)
|
PIPSOLAR_SENSOR(battery_bulk_voltage, QPIRI)
|
||||||
PIPSOLAR_SENSOR(battery_float_voltage, QPIRI, float)
|
PIPSOLAR_SENSOR(battery_float_voltage, QPIRI)
|
||||||
PIPSOLAR_SENSOR(battery_type, QPIRI, int)
|
PIPSOLAR_SENSOR(battery_type, QPIRI)
|
||||||
PIPSOLAR_SENSOR(current_max_ac_charging_current, QPIRI, int)
|
PIPSOLAR_SENSOR(current_max_ac_charging_current, QPIRI)
|
||||||
PIPSOLAR_SENSOR(current_max_charging_current, QPIRI, int)
|
PIPSOLAR_SENSOR(current_max_charging_current, QPIRI)
|
||||||
PIPSOLAR_SENSOR(input_voltage_range, QPIRI, int)
|
PIPSOLAR_SENSOR(input_voltage_range, QPIRI)
|
||||||
PIPSOLAR_SENSOR(output_source_priority, QPIRI, int)
|
PIPSOLAR_SENSOR(output_source_priority, QPIRI)
|
||||||
PIPSOLAR_SENSOR(charger_source_priority, QPIRI, int)
|
PIPSOLAR_SENSOR(charger_source_priority, QPIRI)
|
||||||
PIPSOLAR_SENSOR(parallel_max_num, QPIRI, int)
|
PIPSOLAR_SENSOR(parallel_max_num, QPIRI)
|
||||||
PIPSOLAR_SENSOR(machine_type, QPIRI, int)
|
PIPSOLAR_SENSOR(machine_type, QPIRI)
|
||||||
PIPSOLAR_SENSOR(topology, QPIRI, int)
|
PIPSOLAR_SENSOR(topology, QPIRI)
|
||||||
PIPSOLAR_SENSOR(output_mode, QPIRI, int)
|
PIPSOLAR_SENSOR(output_mode, QPIRI)
|
||||||
PIPSOLAR_SENSOR(battery_redischarge_voltage, QPIRI, float)
|
PIPSOLAR_SENSOR(battery_redischarge_voltage, QPIRI)
|
||||||
PIPSOLAR_SENSOR(pv_ok_condition_for_parallel, QPIRI, int)
|
PIPSOLAR_SENSOR(pv_ok_condition_for_parallel, QPIRI)
|
||||||
PIPSOLAR_SENSOR(pv_power_balance, QPIRI, int)
|
PIPSOLAR_SENSOR(pv_power_balance, QPIRI)
|
||||||
|
|
||||||
// QMOD values
|
// QMOD values
|
||||||
PIPSOLAR_VALUED_TEXT_SENSOR(device_mode, QMOD, char)
|
PIPSOLAR_TEXT_SENSOR(device_mode, QMOD)
|
||||||
|
|
||||||
// QFLAG values
|
// QFLAG values
|
||||||
PIPSOLAR_BINARY_SENSOR(silence_buzzer_open_buzzer, QFLAG, int)
|
PIPSOLAR_BINARY_SENSOR(silence_buzzer_open_buzzer, QFLAG)
|
||||||
PIPSOLAR_BINARY_SENSOR(overload_bypass_function, QFLAG, int)
|
PIPSOLAR_BINARY_SENSOR(overload_bypass_function, QFLAG)
|
||||||
PIPSOLAR_BINARY_SENSOR(lcd_escape_to_default, QFLAG, int)
|
PIPSOLAR_BINARY_SENSOR(lcd_escape_to_default, QFLAG)
|
||||||
PIPSOLAR_BINARY_SENSOR(overload_restart_function, QFLAG, int)
|
PIPSOLAR_BINARY_SENSOR(overload_restart_function, QFLAG)
|
||||||
PIPSOLAR_BINARY_SENSOR(over_temperature_restart_function, QFLAG, int)
|
PIPSOLAR_BINARY_SENSOR(over_temperature_restart_function, QFLAG)
|
||||||
PIPSOLAR_BINARY_SENSOR(backlight_on, QFLAG, int)
|
PIPSOLAR_BINARY_SENSOR(backlight_on, QFLAG)
|
||||||
PIPSOLAR_BINARY_SENSOR(alarm_on_when_primary_source_interrupt, QFLAG, int)
|
PIPSOLAR_BINARY_SENSOR(alarm_on_when_primary_source_interrupt, QFLAG)
|
||||||
PIPSOLAR_BINARY_SENSOR(fault_code_record, QFLAG, int)
|
PIPSOLAR_BINARY_SENSOR(fault_code_record, QFLAG)
|
||||||
PIPSOLAR_BINARY_SENSOR(power_saving, QFLAG, int)
|
PIPSOLAR_BINARY_SENSOR(power_saving, QFLAG)
|
||||||
|
|
||||||
// QPIWS values
|
// QPIWS values
|
||||||
PIPSOLAR_BINARY_SENSOR(warnings_present, QPIWS, bool)
|
PIPSOLAR_BINARY_SENSOR(warnings_present, QPIWS)
|
||||||
PIPSOLAR_BINARY_SENSOR(faults_present, QPIWS, bool)
|
PIPSOLAR_BINARY_SENSOR(faults_present, QPIWS)
|
||||||
PIPSOLAR_BINARY_SENSOR(warning_power_loss, QPIWS, bool)
|
PIPSOLAR_BINARY_SENSOR(warning_power_loss, QPIWS)
|
||||||
PIPSOLAR_BINARY_SENSOR(fault_inverter_fault, QPIWS, bool)
|
PIPSOLAR_BINARY_SENSOR(fault_inverter_fault, QPIWS)
|
||||||
PIPSOLAR_BINARY_SENSOR(fault_bus_over, QPIWS, bool)
|
PIPSOLAR_BINARY_SENSOR(fault_bus_over, QPIWS)
|
||||||
PIPSOLAR_BINARY_SENSOR(fault_bus_under, QPIWS, bool)
|
PIPSOLAR_BINARY_SENSOR(fault_bus_under, QPIWS)
|
||||||
PIPSOLAR_BINARY_SENSOR(fault_bus_soft_fail, QPIWS, bool)
|
PIPSOLAR_BINARY_SENSOR(fault_bus_soft_fail, QPIWS)
|
||||||
PIPSOLAR_BINARY_SENSOR(warning_line_fail, QPIWS, bool)
|
PIPSOLAR_BINARY_SENSOR(warning_line_fail, QPIWS)
|
||||||
PIPSOLAR_BINARY_SENSOR(fault_opvshort, QPIWS, bool)
|
PIPSOLAR_BINARY_SENSOR(fault_opvshort, QPIWS)
|
||||||
PIPSOLAR_BINARY_SENSOR(fault_inverter_voltage_too_low, QPIWS, bool)
|
PIPSOLAR_BINARY_SENSOR(fault_inverter_voltage_too_low, QPIWS)
|
||||||
PIPSOLAR_BINARY_SENSOR(fault_inverter_voltage_too_high, QPIWS, bool)
|
PIPSOLAR_BINARY_SENSOR(fault_inverter_voltage_too_high, QPIWS)
|
||||||
PIPSOLAR_BINARY_SENSOR(warning_over_temperature, QPIWS, bool)
|
PIPSOLAR_BINARY_SENSOR(warning_over_temperature, QPIWS)
|
||||||
PIPSOLAR_BINARY_SENSOR(warning_fan_lock, QPIWS, bool)
|
PIPSOLAR_BINARY_SENSOR(warning_fan_lock, QPIWS)
|
||||||
PIPSOLAR_BINARY_SENSOR(warning_battery_voltage_high, QPIWS, bool)
|
PIPSOLAR_BINARY_SENSOR(warning_battery_voltage_high, QPIWS)
|
||||||
PIPSOLAR_BINARY_SENSOR(warning_battery_low_alarm, QPIWS, bool)
|
PIPSOLAR_BINARY_SENSOR(warning_battery_low_alarm, QPIWS)
|
||||||
PIPSOLAR_BINARY_SENSOR(warning_battery_under_shutdown, QPIWS, bool)
|
PIPSOLAR_BINARY_SENSOR(warning_battery_under_shutdown, QPIWS)
|
||||||
PIPSOLAR_BINARY_SENSOR(warning_battery_derating, QPIWS, bool)
|
PIPSOLAR_BINARY_SENSOR(warning_battery_derating, QPIWS)
|
||||||
PIPSOLAR_BINARY_SENSOR(warning_over_load, QPIWS, bool)
|
PIPSOLAR_BINARY_SENSOR(warning_over_load, QPIWS)
|
||||||
PIPSOLAR_BINARY_SENSOR(warning_eeprom_failed, QPIWS, bool)
|
PIPSOLAR_BINARY_SENSOR(warning_eeprom_failed, QPIWS)
|
||||||
PIPSOLAR_BINARY_SENSOR(fault_inverter_over_current, QPIWS, bool)
|
PIPSOLAR_BINARY_SENSOR(fault_inverter_over_current, QPIWS)
|
||||||
PIPSOLAR_BINARY_SENSOR(fault_inverter_soft_failed, QPIWS, bool)
|
PIPSOLAR_BINARY_SENSOR(fault_inverter_soft_failed, QPIWS)
|
||||||
PIPSOLAR_BINARY_SENSOR(fault_self_test_failed, QPIWS, bool)
|
PIPSOLAR_BINARY_SENSOR(fault_self_test_failed, QPIWS)
|
||||||
PIPSOLAR_BINARY_SENSOR(fault_op_dc_voltage_over, QPIWS, bool)
|
PIPSOLAR_BINARY_SENSOR(fault_op_dc_voltage_over, QPIWS)
|
||||||
PIPSOLAR_BINARY_SENSOR(fault_battery_open, QPIWS, bool)
|
PIPSOLAR_BINARY_SENSOR(fault_battery_open, QPIWS)
|
||||||
PIPSOLAR_BINARY_SENSOR(fault_current_sensor_failed, QPIWS, bool)
|
PIPSOLAR_BINARY_SENSOR(fault_current_sensor_failed, QPIWS)
|
||||||
PIPSOLAR_BINARY_SENSOR(fault_battery_short, QPIWS, bool)
|
PIPSOLAR_BINARY_SENSOR(fault_battery_short, QPIWS)
|
||||||
PIPSOLAR_BINARY_SENSOR(warning_power_limit, QPIWS, bool)
|
PIPSOLAR_BINARY_SENSOR(warning_power_limit, QPIWS)
|
||||||
PIPSOLAR_BINARY_SENSOR(warning_pv_voltage_high, QPIWS, bool)
|
PIPSOLAR_BINARY_SENSOR(warning_pv_voltage_high, QPIWS)
|
||||||
PIPSOLAR_BINARY_SENSOR(fault_mppt_overload, QPIWS, bool)
|
PIPSOLAR_BINARY_SENSOR(fault_mppt_overload, QPIWS)
|
||||||
PIPSOLAR_BINARY_SENSOR(warning_mppt_overload, QPIWS, bool)
|
PIPSOLAR_BINARY_SENSOR(warning_mppt_overload, QPIWS)
|
||||||
PIPSOLAR_BINARY_SENSOR(warning_battery_too_low_to_charge, QPIWS, bool)
|
PIPSOLAR_BINARY_SENSOR(warning_battery_too_low_to_charge, QPIWS)
|
||||||
PIPSOLAR_BINARY_SENSOR(fault_dc_dc_over_current, QPIWS, bool)
|
PIPSOLAR_BINARY_SENSOR(fault_dc_dc_over_current, QPIWS)
|
||||||
PIPSOLAR_BINARY_SENSOR(fault_code, QPIWS, int)
|
PIPSOLAR_BINARY_SENSOR(fault_code, QPIWS)
|
||||||
PIPSOLAR_BINARY_SENSOR(warnung_low_pv_energy, QPIWS, bool)
|
PIPSOLAR_BINARY_SENSOR(warning_low_pv_energy, QPIWS)
|
||||||
PIPSOLAR_BINARY_SENSOR(warning_high_ac_input_during_bus_soft_start, QPIWS, bool)
|
PIPSOLAR_BINARY_SENSOR(warning_high_ac_input_during_bus_soft_start, QPIWS)
|
||||||
PIPSOLAR_BINARY_SENSOR(warning_battery_equalization, QPIWS, bool)
|
PIPSOLAR_BINARY_SENSOR(warning_battery_equalization, QPIWS)
|
||||||
|
|
||||||
PIPSOLAR_TEXT_SENSOR(last_qpigs, QPIGS)
|
PIPSOLAR_TEXT_SENSOR(last_qpigs, QPIGS)
|
||||||
PIPSOLAR_TEXT_SENSOR(last_qpiri, QPIRI)
|
PIPSOLAR_TEXT_SENSOR(last_qpiri, QPIRI)
|
||||||
@@ -180,14 +185,14 @@ class Pipsolar : public uart::UARTDevice, public PollingComponent {
|
|||||||
PIPSOLAR_SWITCH(pv_ok_condition_for_parallel_switch, QPIRI)
|
PIPSOLAR_SWITCH(pv_ok_condition_for_parallel_switch, QPIRI)
|
||||||
PIPSOLAR_SWITCH(pv_power_balance_switch, QPIRI)
|
PIPSOLAR_SWITCH(pv_power_balance_switch, QPIRI)
|
||||||
|
|
||||||
void switch_command(const std::string &command);
|
void queue_command(const std::string &command);
|
||||||
void setup() override;
|
void setup() override;
|
||||||
void loop() override;
|
void loop() override;
|
||||||
void dump_config() override;
|
void dump_config() override;
|
||||||
void update() override;
|
void update() override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
static const size_t PIPSOLAR_READ_BUFFER_LENGTH = 110; // maximum supported answer length
|
static const size_t PIPSOLAR_READ_BUFFER_LENGTH = 128; // maximum supported answer length
|
||||||
static const size_t COMMAND_QUEUE_LENGTH = 10;
|
static const size_t COMMAND_QUEUE_LENGTH = 10;
|
||||||
static const size_t COMMAND_TIMEOUT = 5000;
|
static const size_t COMMAND_TIMEOUT = 5000;
|
||||||
static const size_t POLLING_COMMANDS_MAX = 15;
|
static const size_t POLLING_COMMANDS_MAX = 15;
|
||||||
@@ -198,7 +203,26 @@ class Pipsolar : public uart::UARTDevice, public PollingComponent {
|
|||||||
uint16_t pipsolar_crc_(uint8_t *msg, uint8_t len);
|
uint16_t pipsolar_crc_(uint8_t *msg, uint8_t len);
|
||||||
bool send_next_command_();
|
bool send_next_command_();
|
||||||
bool send_next_poll_();
|
bool send_next_poll_();
|
||||||
void queue_command_(const char *command, uint8_t length);
|
|
||||||
|
void handle_qpiri_(const char *message);
|
||||||
|
void handle_qpigs_(const char *message);
|
||||||
|
void handle_qmod_(const char *message);
|
||||||
|
void handle_qflag_(const char *message);
|
||||||
|
void handle_qpiws_(const char *message);
|
||||||
|
void handle_qt_(const char *message);
|
||||||
|
void handle_qmn_(const char *message);
|
||||||
|
|
||||||
|
void skip_start_(const char *message, size_t *pos);
|
||||||
|
void skip_field_(const char *message, size_t *pos);
|
||||||
|
std::string read_field_(const char *message, size_t *pos);
|
||||||
|
|
||||||
|
void read_float_sensor_(const char *message, size_t *pos, sensor::Sensor *sensor);
|
||||||
|
void read_int_sensor_(const char *message, size_t *pos, sensor::Sensor *sensor);
|
||||||
|
|
||||||
|
void publish_binary_sensor_(esphome::optional<bool> b, binary_sensor::BinarySensor *sensor);
|
||||||
|
|
||||||
|
esphome::optional<bool> get_bit_(std::string bits, uint8_t bit_pos);
|
||||||
|
|
||||||
std::string command_queue_[COMMAND_QUEUE_LENGTH];
|
std::string command_queue_[COMMAND_QUEUE_LENGTH];
|
||||||
uint8_t command_queue_position_ = 0;
|
uint8_t command_queue_position_ = 0;
|
||||||
uint8_t read_buffer_[PIPSOLAR_READ_BUFFER_LENGTH];
|
uint8_t read_buffer_[PIPSOLAR_READ_BUFFER_LENGTH];
|
||||||
@@ -213,11 +237,10 @@ class Pipsolar : public uart::UARTDevice, public PollingComponent {
|
|||||||
STATE_POLL_COMPLETE = 3,
|
STATE_POLL_COMPLETE = 3,
|
||||||
STATE_COMMAND_COMPLETE = 4,
|
STATE_COMMAND_COMPLETE = 4,
|
||||||
STATE_POLL_CHECKED = 5,
|
STATE_POLL_CHECKED = 5,
|
||||||
STATE_POLL_DECODED = 6,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
uint8_t last_polling_command_ = 0;
|
uint8_t last_polling_command_ = 0;
|
||||||
PollingCommand used_polling_commands_[POLLING_COMMANDS_MAX];
|
PollingCommand enabled_polling_commands_[POLLING_COMMANDS_MAX];
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace pipsolar
|
} // namespace pipsolar
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ void PipsolarSwitch::dump_config() { LOG_SWITCH("", "Pipsolar Switch", this); }
|
|||||||
void PipsolarSwitch::write_state(bool state) {
|
void PipsolarSwitch::write_state(bool state) {
|
||||||
if (state) {
|
if (state) {
|
||||||
if (!this->on_command_.empty()) {
|
if (!this->on_command_.empty()) {
|
||||||
this->parent_->switch_command(this->on_command_);
|
this->parent_->queue_command(this->on_command_);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!this->off_command_.empty()) {
|
if (!this->off_command_.empty()) {
|
||||||
this->parent_->switch_command(this->off_command_);
|
this->parent_->queue_command(this->off_command_);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -261,9 +261,12 @@ ThrottleAverageFilter = sensor_ns.class_("ThrottleAverageFilter", Filter, cg.Com
|
|||||||
LambdaFilter = sensor_ns.class_("LambdaFilter", Filter)
|
LambdaFilter = sensor_ns.class_("LambdaFilter", Filter)
|
||||||
OffsetFilter = sensor_ns.class_("OffsetFilter", Filter)
|
OffsetFilter = sensor_ns.class_("OffsetFilter", Filter)
|
||||||
MultiplyFilter = sensor_ns.class_("MultiplyFilter", Filter)
|
MultiplyFilter = sensor_ns.class_("MultiplyFilter", Filter)
|
||||||
FilterOutValueFilter = sensor_ns.class_("FilterOutValueFilter", Filter)
|
ValueListFilter = sensor_ns.class_("ValueListFilter", Filter)
|
||||||
|
FilterOutValueFilter = sensor_ns.class_("FilterOutValueFilter", ValueListFilter)
|
||||||
ThrottleFilter = sensor_ns.class_("ThrottleFilter", Filter)
|
ThrottleFilter = sensor_ns.class_("ThrottleFilter", Filter)
|
||||||
ThrottleWithPriorityFilter = sensor_ns.class_("ThrottleWithPriorityFilter", Filter)
|
ThrottleWithPriorityFilter = sensor_ns.class_(
|
||||||
|
"ThrottleWithPriorityFilter", ValueListFilter
|
||||||
|
)
|
||||||
TimeoutFilter = sensor_ns.class_("TimeoutFilter", Filter, cg.Component)
|
TimeoutFilter = sensor_ns.class_("TimeoutFilter", Filter, cg.Component)
|
||||||
DebounceFilter = sensor_ns.class_("DebounceFilter", Filter, cg.Component)
|
DebounceFilter = sensor_ns.class_("DebounceFilter", Filter, cg.Component)
|
||||||
HeartbeatFilter = sensor_ns.class_("HeartbeatFilter", Filter, cg.Component)
|
HeartbeatFilter = sensor_ns.class_("HeartbeatFilter", Filter, cg.Component)
|
||||||
|
|||||||
@@ -228,27 +228,40 @@ MultiplyFilter::MultiplyFilter(TemplatableValue<float> multiplier) : multiplier_
|
|||||||
|
|
||||||
optional<float> MultiplyFilter::new_value(float value) { return value * this->multiplier_.value(); }
|
optional<float> MultiplyFilter::new_value(float value) { return value * this->multiplier_.value(); }
|
||||||
|
|
||||||
// FilterOutValueFilter
|
// ValueListFilter (base class)
|
||||||
FilterOutValueFilter::FilterOutValueFilter(std::vector<TemplatableValue<float>> values_to_filter_out)
|
ValueListFilter::ValueListFilter(std::initializer_list<TemplatableValue<float>> values) : values_(values) {}
|
||||||
: values_to_filter_out_(std::move(values_to_filter_out)) {}
|
|
||||||
|
|
||||||
optional<float> FilterOutValueFilter::new_value(float value) {
|
bool ValueListFilter::value_matches_any_(float sensor_value) {
|
||||||
int8_t accuracy = this->parent_->get_accuracy_decimals();
|
int8_t accuracy = this->parent_->get_accuracy_decimals();
|
||||||
float accuracy_mult = powf(10.0f, accuracy);
|
float accuracy_mult = powf(10.0f, accuracy);
|
||||||
for (auto filter_value : this->values_to_filter_out_) {
|
float rounded_sensor = roundf(accuracy_mult * sensor_value);
|
||||||
if (std::isnan(filter_value.value())) {
|
|
||||||
if (std::isnan(value)) {
|
for (auto &filter_value : this->values_) {
|
||||||
return {};
|
float fv = filter_value.value();
|
||||||
}
|
|
||||||
|
// Handle NaN comparison
|
||||||
|
if (std::isnan(fv)) {
|
||||||
|
if (std::isnan(sensor_value))
|
||||||
|
return true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
float rounded_filter_out = roundf(accuracy_mult * filter_value.value());
|
|
||||||
float rounded_value = roundf(accuracy_mult * value);
|
// Compare rounded values
|
||||||
if (rounded_filter_out == rounded_value) {
|
if (roundf(accuracy_mult * fv) == rounded_sensor)
|
||||||
return {};
|
return true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return value;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterOutValueFilter
|
||||||
|
FilterOutValueFilter::FilterOutValueFilter(std::initializer_list<TemplatableValue<float>> values_to_filter_out)
|
||||||
|
: ValueListFilter(values_to_filter_out) {}
|
||||||
|
|
||||||
|
optional<float> FilterOutValueFilter::new_value(float value) {
|
||||||
|
if (this->value_matches_any_(value))
|
||||||
|
return {}; // Filter out
|
||||||
|
return value; // Pass through
|
||||||
}
|
}
|
||||||
|
|
||||||
// ThrottleFilter
|
// ThrottleFilter
|
||||||
@@ -263,33 +276,15 @@ optional<float> ThrottleFilter::new_value(float value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ThrottleWithPriorityFilter
|
// ThrottleWithPriorityFilter
|
||||||
ThrottleWithPriorityFilter::ThrottleWithPriorityFilter(uint32_t min_time_between_inputs,
|
ThrottleWithPriorityFilter::ThrottleWithPriorityFilter(
|
||||||
std::vector<TemplatableValue<float>> prioritized_values)
|
uint32_t min_time_between_inputs, std::initializer_list<TemplatableValue<float>> prioritized_values)
|
||||||
: min_time_between_inputs_(min_time_between_inputs), prioritized_values_(std::move(prioritized_values)) {}
|
: ValueListFilter(prioritized_values), min_time_between_inputs_(min_time_between_inputs) {}
|
||||||
|
|
||||||
optional<float> ThrottleWithPriorityFilter::new_value(float value) {
|
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();
|
const uint32_t now = App.get_loop_component_start_time();
|
||||||
// First, determine if the new value is one of the prioritized values
|
// Allow value through if: no previous input, time expired, or is prioritized
|
||||||
for (auto prioritized_value : this->prioritized_values_) {
|
if (this->last_input_ == 0 || now - this->last_input_ >= min_time_between_inputs_ ||
|
||||||
if (std::isnan(prioritized_value.value())) {
|
this->value_matches_any_(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;
|
this->last_input_ = now;
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -317,15 +317,28 @@ class MultiplyFilter : public Filter {
|
|||||||
TemplatableValue<float> multiplier_;
|
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`.
|
/// A simple filter that only forwards the filter chain if it doesn't receive `value_to_filter_out`.
|
||||||
class FilterOutValueFilter : public Filter {
|
class FilterOutValueFilter : public ValueListFilter {
|
||||||
public:
|
public:
|
||||||
explicit FilterOutValueFilter(std::vector<TemplatableValue<float>> values_to_filter_out);
|
explicit FilterOutValueFilter(std::initializer_list<TemplatableValue<float>> values_to_filter_out);
|
||||||
|
|
||||||
optional<float> new_value(float value) override;
|
optional<float> new_value(float value) override;
|
||||||
|
|
||||||
protected:
|
|
||||||
std::vector<TemplatableValue<float>> values_to_filter_out_;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class ThrottleFilter : public Filter {
|
class ThrottleFilter : public Filter {
|
||||||
@@ -340,17 +353,16 @@ class ThrottleFilter : public Filter {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/// Same as 'throttle' but will immediately publish values contained in `value_to_prioritize`.
|
/// Same as 'throttle' but will immediately publish values contained in `value_to_prioritize`.
|
||||||
class ThrottleWithPriorityFilter : public Filter {
|
class ThrottleWithPriorityFilter : public ValueListFilter {
|
||||||
public:
|
public:
|
||||||
explicit ThrottleWithPriorityFilter(uint32_t min_time_between_inputs,
|
explicit ThrottleWithPriorityFilter(uint32_t min_time_between_inputs,
|
||||||
std::vector<TemplatableValue<float>> prioritized_values);
|
std::initializer_list<TemplatableValue<float>> prioritized_values);
|
||||||
|
|
||||||
optional<float> new_value(float value) override;
|
optional<float> new_value(float value) override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
uint32_t last_input_{0};
|
uint32_t last_input_{0};
|
||||||
uint32_t min_time_between_inputs_;
|
uint32_t min_time_between_inputs_;
|
||||||
std::vector<TemplatableValue<float>> prioritized_values_;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class TimeoutFilter : public Filter, public Component {
|
class TimeoutFilter : public Filter, public Component {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import esphome.config_validation as cv
|
|||||||
from esphome.const import CONF_SUBSTITUTIONS, VALID_SUBSTITUTIONS_CHARACTERS
|
from esphome.const import CONF_SUBSTITUTIONS, VALID_SUBSTITUTIONS_CHARACTERS
|
||||||
from esphome.yaml_util import ESPHomeDataBase, ESPLiteralValue, make_data_base
|
from esphome.yaml_util import ESPHomeDataBase, ESPLiteralValue, make_data_base
|
||||||
|
|
||||||
from .jinja import Jinja, JinjaStr, TemplateError, TemplateRuntimeError, has_jinja
|
from .jinja import Jinja, JinjaError, JinjaStr, has_jinja
|
||||||
|
|
||||||
CODEOWNERS = ["@esphome/core"]
|
CODEOWNERS = ["@esphome/core"]
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@@ -57,17 +57,12 @@ def _expand_jinja(value, orig_value, path, jinja, ignore_missing):
|
|||||||
"->".join(str(x) for x in path),
|
"->".join(str(x) for x in path),
|
||||||
err.message,
|
err.message,
|
||||||
)
|
)
|
||||||
except (
|
except JinjaError as err:
|
||||||
TemplateError,
|
|
||||||
TemplateRuntimeError,
|
|
||||||
RuntimeError,
|
|
||||||
ArithmeticError,
|
|
||||||
AttributeError,
|
|
||||||
TypeError,
|
|
||||||
) as err:
|
|
||||||
raise cv.Invalid(
|
raise cv.Invalid(
|
||||||
f"{type(err).__name__} Error evaluating jinja expression '{value}': {str(err)}."
|
f"{err.error_name()} Error evaluating jinja expression '{value}': {str(err.parent())}."
|
||||||
f" See {'->'.join(str(x) for x in path)}",
|
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)}",
|
||||||
path,
|
path,
|
||||||
)
|
)
|
||||||
return value
|
return value
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import re
|
|||||||
import jinja2 as jinja
|
import jinja2 as jinja
|
||||||
from jinja2.sandbox import SandboxedEnvironment
|
from jinja2.sandbox import SandboxedEnvironment
|
||||||
|
|
||||||
|
from esphome.yaml_util import ESPLiteralValue
|
||||||
|
|
||||||
TemplateError = jinja.TemplateError
|
TemplateError = jinja.TemplateError
|
||||||
TemplateSyntaxError = jinja.TemplateSyntaxError
|
TemplateSyntaxError = jinja.TemplateSyntaxError
|
||||||
TemplateRuntimeError = jinja.TemplateRuntimeError
|
TemplateRuntimeError = jinja.TemplateRuntimeError
|
||||||
@@ -26,18 +28,20 @@ def has_jinja(st):
|
|||||||
return detect_jinja_re.search(st) is not None
|
return detect_jinja_re.search(st) is not None
|
||||||
|
|
||||||
|
|
||||||
# SAFE_GLOBAL_FUNCTIONS defines a allowlist of built-in functions that are considered safe to expose
|
# SAFE_GLOBALS defines a allowlist of built-in functions or modules that are considered safe to expose
|
||||||
# in Jinja templates or other sandboxed evaluation contexts. Only functions that do not allow
|
# 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.
|
# arbitrary code execution, file access, or other security risks are included.
|
||||||
#
|
#
|
||||||
# The following functions are considered safe:
|
# 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.
|
# - ord: Converts a character to its Unicode code point integer.
|
||||||
# - chr: Converts an integer to its corresponding Unicode character.
|
# - chr: Converts an integer to its corresponding Unicode character.
|
||||||
# - len: Returns the length of a sequence or collection.
|
# - 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
|
# 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.
|
# to the file system, environment, or other potentially sensitive resources.
|
||||||
SAFE_GLOBAL_FUNCTIONS = {
|
SAFE_GLOBALS = {
|
||||||
|
"math": math, # Inject entire math module
|
||||||
"ord": ord,
|
"ord": ord,
|
||||||
"chr": chr,
|
"chr": chr,
|
||||||
"len": len,
|
"len": len,
|
||||||
@@ -56,22 +60,62 @@ class JinjaStr(str):
|
|||||||
later in the main substitutions pass.
|
later in the main substitutions pass.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
Undefined = object()
|
||||||
|
|
||||||
def __new__(cls, value: str, upvalues=None):
|
def __new__(cls, value: str, upvalues=None):
|
||||||
obj = super().__new__(cls, value)
|
if isinstance(value, JinjaStr):
|
||||||
obj.upvalues = upvalues or {}
|
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
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
def __init__(self, value: str, upvalues=None):
|
|
||||||
self.upvalues = upvalues or {}
|
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)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Jinja:
|
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):
|
||||||
"""
|
"""
|
||||||
Wraps a Jinja environment
|
Wraps a Jinja environment
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, context_vars):
|
def __init__(self, context_vars):
|
||||||
self.env = SandboxedEnvironment(
|
super().__init__(
|
||||||
trim_blocks=True,
|
trim_blocks=True,
|
||||||
lstrip_blocks=True,
|
lstrip_blocks=True,
|
||||||
block_start_string="<%",
|
block_start_string="<%",
|
||||||
@@ -82,13 +126,20 @@ class Jinja:
|
|||||||
variable_end_string="}",
|
variable_end_string="}",
|
||||||
undefined=jinja.StrictUndefined,
|
undefined=jinja.StrictUndefined,
|
||||||
)
|
)
|
||||||
self.env.add_extension("jinja2.ext.do")
|
self.context_class = TrackerContext
|
||||||
self.env.globals["math"] = math # Inject entire math module
|
self.add_extension("jinja2.ext.do")
|
||||||
|
self.context_trace = {}
|
||||||
self.context_vars = {**context_vars}
|
self.context_vars = {**context_vars}
|
||||||
self.env.globals = {
|
for k, v in self.context_vars.items():
|
||||||
**self.env.globals,
|
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.context_vars,
|
**self.context_vars,
|
||||||
**SAFE_GLOBAL_FUNCTIONS,
|
**SAFE_GLOBALS,
|
||||||
}
|
}
|
||||||
|
|
||||||
def safe_eval(self, expr):
|
def safe_eval(self, expr):
|
||||||
@@ -110,23 +161,43 @@ class Jinja:
|
|||||||
result = None
|
result = None
|
||||||
override_vars = {}
|
override_vars = {}
|
||||||
if isinstance(content_str, JinjaStr):
|
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
|
# If `value` is already a JinjaStr, it means we are trying to evaluate it again
|
||||||
# in a parent pass.
|
# in a parent pass.
|
||||||
# Hopefully, all required variables are visible now.
|
# Hopefully, all required variables are visible now.
|
||||||
override_vars = content_str.upvalues
|
override_vars = content_str.upvalues
|
||||||
|
|
||||||
|
old_trace = self.context_trace
|
||||||
|
self.context_trace = {}
|
||||||
try:
|
try:
|
||||||
template = self.env.from_string(content_str)
|
template = self.from_string(content_str)
|
||||||
result = self.safe_eval(template.render(override_vars))
|
result = self.safe_eval(template.render(override_vars))
|
||||||
if isinstance(result, Undefined):
|
if isinstance(result, Undefined):
|
||||||
# This happens when the expression is simply an undefined variable. Jinja does not
|
print("" + result) # force a UndefinedError exception
|
||||||
# raise an exception, instead we get "Undefined".
|
|
||||||
# Trigger an UndefinedError exception so we skip to below.
|
|
||||||
print("" + result)
|
|
||||||
except (TemplateSyntaxError, UndefinedError) as err:
|
except (TemplateSyntaxError, UndefinedError) as err:
|
||||||
# `content_str` contains a Jinja expression that refers to a variable that is undefined
|
# `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.
|
# in this scope. Perhaps it refers to a root substitution that is not visible yet.
|
||||||
# Therefore, return the original `content_str` as a JinjaStr, which contains the variables
|
# Therefore, return `content_str` as a JinjaStr, which contains the variables
|
||||||
# that are actually visible to it at this point to postpone evaluation.
|
# that are actually visible to it at this point to postpone evaluation.
|
||||||
return JinjaStr(content_str, {**self.context_vars, **override_vars}), err
|
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
|
return result, None
|
||||||
|
|||||||
@@ -110,17 +110,28 @@ def validate_mapping(value):
|
|||||||
"substitute", SubstituteFilter, cv.ensure_list(validate_mapping)
|
"substitute", SubstituteFilter, cv.ensure_list(validate_mapping)
|
||||||
)
|
)
|
||||||
async def substitute_filter_to_code(config, filter_id):
|
async def substitute_filter_to_code(config, filter_id):
|
||||||
from_strings = [conf[CONF_FROM] for conf in config]
|
substitutions = [
|
||||||
to_strings = [conf[CONF_TO] for conf in config]
|
cg.StructInitializer(
|
||||||
return cg.new_Pvariable(filter_id, from_strings, to_strings)
|
cg.MockObj("Substitution", "esphome::text_sensor::"),
|
||||||
|
("from", conf[CONF_FROM]),
|
||||||
|
("to", conf[CONF_TO]),
|
||||||
|
)
|
||||||
|
for conf in config
|
||||||
|
]
|
||||||
|
return cg.new_Pvariable(filter_id, substitutions)
|
||||||
|
|
||||||
|
|
||||||
@FILTER_REGISTRY.register("map", MapFilter, cv.ensure_list(validate_mapping))
|
@FILTER_REGISTRY.register("map", MapFilter, cv.ensure_list(validate_mapping))
|
||||||
async def map_filter_to_code(config, filter_id):
|
async def map_filter_to_code(config, filter_id):
|
||||||
map_ = cg.std_ns.class_("map").template(cg.std_string, cg.std_string)
|
mappings = [
|
||||||
return cg.new_Pvariable(
|
cg.StructInitializer(
|
||||||
filter_id, map_([(item[CONF_FROM], item[CONF_TO]) for item in config])
|
cg.MockObj("Substitution", "esphome::text_sensor::"),
|
||||||
|
("from", conf[CONF_FROM]),
|
||||||
|
("to", conf[CONF_TO]),
|
||||||
)
|
)
|
||||||
|
for conf in config
|
||||||
|
]
|
||||||
|
return cg.new_Pvariable(filter_id, mappings)
|
||||||
|
|
||||||
|
|
||||||
validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_")
|
validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_")
|
||||||
|
|||||||
@@ -62,19 +62,26 @@ optional<std::string> AppendFilter::new_value(std::string value) { return value
|
|||||||
optional<std::string> PrependFilter::new_value(std::string value) { return this->prefix_ + value; }
|
optional<std::string> PrependFilter::new_value(std::string value) { return this->prefix_ + value; }
|
||||||
|
|
||||||
// Substitute
|
// Substitute
|
||||||
|
SubstituteFilter::SubstituteFilter(std::initializer_list<Substitution> substitutions) : substitutions_(substitutions) {}
|
||||||
|
|
||||||
optional<std::string> SubstituteFilter::new_value(std::string value) {
|
optional<std::string> SubstituteFilter::new_value(std::string value) {
|
||||||
std::size_t pos;
|
std::size_t pos;
|
||||||
for (size_t i = 0; i < this->from_strings_.size(); i++) {
|
for (const auto &sub : this->substitutions_) {
|
||||||
while ((pos = value.find(this->from_strings_[i])) != std::string::npos)
|
while ((pos = value.find(sub.from)) != std::string::npos)
|
||||||
value.replace(pos, this->from_strings_[i].size(), this->to_strings_[i]);
|
value.replace(pos, sub.from.size(), sub.to);
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map
|
// Map
|
||||||
|
MapFilter::MapFilter(std::initializer_list<Substitution> mappings) : mappings_(mappings) {}
|
||||||
|
|
||||||
optional<std::string> MapFilter::new_value(std::string value) {
|
optional<std::string> MapFilter::new_value(std::string value) {
|
||||||
auto item = mappings_.find(value);
|
for (const auto &mapping : this->mappings_) {
|
||||||
return item == mappings_.end() ? value : item->second;
|
if (mapping.from == value)
|
||||||
|
return mapping.to;
|
||||||
|
}
|
||||||
|
return value; // Pass through if no match
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace text_sensor
|
} // namespace text_sensor
|
||||||
|
|||||||
@@ -2,10 +2,6 @@
|
|||||||
|
|
||||||
#include "esphome/core/component.h"
|
#include "esphome/core/component.h"
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include <queue>
|
|
||||||
#include <utility>
|
|
||||||
#include <map>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace text_sensor {
|
namespace text_sensor {
|
||||||
@@ -98,26 +94,52 @@ class PrependFilter : public Filter {
|
|||||||
std::string prefix_;
|
std::string prefix_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct Substitution {
|
||||||
|
std::string from;
|
||||||
|
std::string to;
|
||||||
|
};
|
||||||
|
|
||||||
/// A simple filter that replaces a substring with another substring
|
/// A simple filter that replaces a substring with another substring
|
||||||
class SubstituteFilter : public Filter {
|
class SubstituteFilter : public Filter {
|
||||||
public:
|
public:
|
||||||
SubstituteFilter(std::vector<std::string> from_strings, std::vector<std::string> to_strings)
|
explicit SubstituteFilter(std::initializer_list<Substitution> substitutions);
|
||||||
: from_strings_(std::move(from_strings)), to_strings_(std::move(to_strings)) {}
|
|
||||||
optional<std::string> new_value(std::string value) override;
|
optional<std::string> new_value(std::string value) override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
std::vector<std::string> from_strings_;
|
FixedVector<Substitution> substitutions_;
|
||||||
std::vector<std::string> to_strings_;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A filter that maps values from one set to another
|
/** A filter that maps values from one set to another
|
||||||
|
*
|
||||||
|
* Uses linear search instead of std::map for typical small datasets (2-20 mappings).
|
||||||
|
* Linear search on contiguous memory is faster than red-black tree lookups when:
|
||||||
|
* - Dataset is small (< ~30 items)
|
||||||
|
* - Memory is contiguous (cache-friendly, better CPU cache utilization)
|
||||||
|
* - No pointer chasing overhead (tree node traversal)
|
||||||
|
* - String comparison cost dominates lookup time
|
||||||
|
*
|
||||||
|
* Benchmark results (see benchmark_map_filter.cpp):
|
||||||
|
* - 2 mappings: Linear 1.26x faster than std::map
|
||||||
|
* - 5 mappings: Linear 2.25x faster than std::map
|
||||||
|
* - 10 mappings: Linear 1.83x faster than std::map
|
||||||
|
* - 20 mappings: Linear 1.59x faster than std::map
|
||||||
|
* - 30 mappings: Linear 1.09x faster than std::map
|
||||||
|
* - 40 mappings: std::map 1.27x faster than Linear (break-even)
|
||||||
|
*
|
||||||
|
* Benefits over std::map:
|
||||||
|
* - ~2KB smaller flash (no red-black tree code)
|
||||||
|
* - ~24-32 bytes less RAM per mapping (no tree node overhead)
|
||||||
|
* - Faster for typical ESPHome usage (2-10 mappings common, 20+ rare)
|
||||||
|
*
|
||||||
|
* Break-even point: ~35-40 mappings, but ESPHome configs rarely exceed 20
|
||||||
|
*/
|
||||||
class MapFilter : public Filter {
|
class MapFilter : public Filter {
|
||||||
public:
|
public:
|
||||||
MapFilter(std::map<std::string, std::string> mappings) : mappings_(std::move(mappings)) {}
|
explicit MapFilter(std::initializer_list<Substitution> mappings);
|
||||||
optional<std::string> new_value(std::string value) override;
|
optional<std::string> new_value(std::string value) override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
std::map<std::string, std::string> mappings_;
|
FixedVector<Substitution> mappings_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace text_sensor
|
} // namespace text_sensor
|
||||||
|
|||||||
@@ -283,8 +283,11 @@ void TuyaClimate::control_fan_mode_(const climate::ClimateCall &call) {
|
|||||||
|
|
||||||
climate::ClimateTraits TuyaClimate::traits() {
|
climate::ClimateTraits TuyaClimate::traits() {
|
||||||
auto traits = climate::ClimateTraits();
|
auto traits = climate::ClimateTraits();
|
||||||
traits.set_supports_action(true);
|
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_ACTION);
|
||||||
traits.set_supports_current_temperature(this->current_temperature_id_.has_value());
|
if (this->current_temperature_id_.has_value()) {
|
||||||
|
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
|
||||||
|
}
|
||||||
|
|
||||||
if (supports_heat_)
|
if (supports_heat_)
|
||||||
traits.add_supported_mode(climate::CLIMATE_MODE_HEAT);
|
traits.add_supported_mode(climate::CLIMATE_MODE_HEAT);
|
||||||
if (supports_cool_)
|
if (supports_cool_)
|
||||||
|
|||||||
@@ -17,6 +17,12 @@ UponorSmatrixDevice = uponor_smatrix_ns.class_(
|
|||||||
"UponorSmatrixDevice", cg.Parented.template(UponorSmatrixComponent)
|
"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_UPONOR_SMATRIX_ID = "uponor_smatrix_id"
|
||||||
CONF_TIME_DEVICE_ADDRESS = "time_device_address"
|
CONF_TIME_DEVICE_ADDRESS = "time_device_address"
|
||||||
|
|
||||||
@@ -24,9 +30,12 @@ CONFIG_SCHEMA = (
|
|||||||
cv.Schema(
|
cv.Schema(
|
||||||
{
|
{
|
||||||
cv.GenerateID(): cv.declare_id(UponorSmatrixComponent),
|
cv.GenerateID(): cv.declare_id(UponorSmatrixComponent),
|
||||||
cv.Optional(CONF_ADDRESS): cv.hex_uint16_t,
|
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_TIME_ID): cv.use_id(time.RealTimeClock),
|
cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
|
||||||
cv.Optional(CONF_TIME_DEVICE_ADDRESS): cv.hex_uint16_t,
|
cv.Optional(CONF_TIME_DEVICE_ADDRESS): device_address,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.extend(cv.COMPONENT_SCHEMA)
|
.extend(cv.COMPONENT_SCHEMA)
|
||||||
@@ -47,7 +56,7 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
|||||||
UPONOR_SMATRIX_DEVICE_SCHEMA = cv.Schema(
|
UPONOR_SMATRIX_DEVICE_SCHEMA = cv.Schema(
|
||||||
{
|
{
|
||||||
cv.GenerateID(CONF_UPONOR_SMATRIX_ID): cv.use_id(UponorSmatrixComponent),
|
cv.GenerateID(CONF_UPONOR_SMATRIX_ID): cv.use_id(UponorSmatrixComponent),
|
||||||
cv.Required(CONF_ADDRESS): cv.hex_uint16_t,
|
cv.Required(CONF_ADDRESS): device_address,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -58,8 +67,6 @@ async def to_code(config):
|
|||||||
await cg.register_component(var, config)
|
await cg.register_component(var, config)
|
||||||
await uart.register_uart_device(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):
|
if time_id := config.get(CONF_TIME_ID):
|
||||||
time_ = await cg.get_variable(time_id)
|
time_ = await cg.get_variable(time_id)
|
||||||
cg.add(var.set_time_id(time_))
|
cg.add(var.set_time_id(time_))
|
||||||
@@ -70,5 +77,5 @@ async def to_code(config):
|
|||||||
async def register_uponor_smatrix_device(var, config):
|
async def register_uponor_smatrix_device(var, config):
|
||||||
parent = await cg.get_variable(config[CONF_UPONOR_SMATRIX_ID])
|
parent = await cg.get_variable(config[CONF_UPONOR_SMATRIX_ID])
|
||||||
cg.add(var.set_parent(parent))
|
cg.add(var.set_parent(parent))
|
||||||
cg.add(var.set_device_address(config[CONF_ADDRESS]))
|
cg.add(var.set_address(config[CONF_ADDRESS]))
|
||||||
cg.add(parent.register_device(var))
|
cg.add(parent.register_device(var))
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ static const char *const TAG = "uponor_smatrix.climate";
|
|||||||
|
|
||||||
void UponorSmatrixClimate::dump_config() {
|
void UponorSmatrixClimate::dump_config() {
|
||||||
LOG_CLIMATE("", "Uponor Smatrix Climate", this);
|
LOG_CLIMATE("", "Uponor Smatrix Climate", this);
|
||||||
ESP_LOGCONFIG(TAG, " Device address: 0x%04X", this->address_);
|
ESP_LOGCONFIG(TAG, " Device address: 0x%08X", this->address_);
|
||||||
}
|
}
|
||||||
|
|
||||||
void UponorSmatrixClimate::loop() {
|
void UponorSmatrixClimate::loop() {
|
||||||
@@ -30,10 +30,9 @@ void UponorSmatrixClimate::loop() {
|
|||||||
|
|
||||||
climate::ClimateTraits UponorSmatrixClimate::traits() {
|
climate::ClimateTraits UponorSmatrixClimate::traits() {
|
||||||
auto traits = climate::ClimateTraits();
|
auto traits = climate::ClimateTraits();
|
||||||
traits.set_supports_current_temperature(true);
|
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE | climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY |
|
||||||
traits.set_supports_current_humidity(true);
|
climate::CLIMATE_SUPPORTS_ACTION);
|
||||||
traits.set_supported_modes({climate::CLIMATE_MODE_HEAT});
|
traits.set_supported_modes({climate::CLIMATE_MODE_HEAT});
|
||||||
traits.set_supports_action(true);
|
|
||||||
traits.set_supported_presets({climate::CLIMATE_PRESET_ECO});
|
traits.set_supported_presets({climate::CLIMATE_PRESET_ECO});
|
||||||
traits.set_visual_min_temperature(this->min_temperature_);
|
traits.set_visual_min_temperature(this->min_temperature_);
|
||||||
traits.set_visual_max_temperature(this->max_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() {
|
void UponorSmatrixSensor::dump_config() {
|
||||||
ESP_LOGCONFIG(TAG,
|
ESP_LOGCONFIG(TAG,
|
||||||
"Uponor Smatrix Sensor\n"
|
"Uponor Smatrix Sensor\n"
|
||||||
" Device address: 0x%04X",
|
" Device address: 0x%08X",
|
||||||
this->address_);
|
this->address_);
|
||||||
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||||
LOG_SENSOR(" ", "External Temperature", this->external_temperature_sensor_);
|
LOG_SENSOR(" ", "External Temperature", this->external_temperature_sensor_);
|
||||||
|
|||||||
@@ -18,11 +18,10 @@ void UponorSmatrixComponent::setup() {
|
|||||||
|
|
||||||
void UponorSmatrixComponent::dump_config() {
|
void UponorSmatrixComponent::dump_config() {
|
||||||
ESP_LOGCONFIG(TAG, "Uponor Smatrix");
|
ESP_LOGCONFIG(TAG, "Uponor Smatrix");
|
||||||
ESP_LOGCONFIG(TAG, " System address: 0x%04X", this->address_);
|
|
||||||
#ifdef USE_TIME
|
#ifdef USE_TIME
|
||||||
if (this->time_id_ != nullptr) {
|
if (this->time_id_ != nullptr) {
|
||||||
ESP_LOGCONFIG(TAG, " Time synchronization: YES");
|
ESP_LOGCONFIG(TAG, " Time synchronization: YES");
|
||||||
ESP_LOGCONFIG(TAG, " Time master device address: 0x%04X", this->time_device_address_);
|
ESP_LOGCONFIG(TAG, " Time master device address: 0x%08X", this->time_device_address_);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -31,7 +30,7 @@ void UponorSmatrixComponent::dump_config() {
|
|||||||
if (!this->unknown_devices_.empty()) {
|
if (!this->unknown_devices_.empty()) {
|
||||||
ESP_LOGCONFIG(TAG, " Detected unknown device addresses:");
|
ESP_LOGCONFIG(TAG, " Detected unknown device addresses:");
|
||||||
for (auto device_address : this->unknown_devices_) {
|
for (auto device_address : this->unknown_devices_) {
|
||||||
ESP_LOGCONFIG(TAG, " 0x%04X", device_address);
|
ESP_LOGCONFIG(TAG, " 0x%08X", device_address);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,8 +88,7 @@ bool UponorSmatrixComponent::parse_byte_(uint8_t byte) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t system_address = encode_uint16(packet[0], packet[1]);
|
uint32_t device_address = encode_uint32(packet[0], packet[1], packet[2], packet[3]);
|
||||||
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 crc = encode_uint16(packet[packet_len - 1], packet[packet_len - 2]);
|
||||||
|
|
||||||
uint16_t computed_crc = crc16(packet, packet_len - 2);
|
uint16_t computed_crc = crc16(packet, packet_len - 2);
|
||||||
@@ -99,24 +97,14 @@ bool UponorSmatrixComponent::parse_byte_(uint8_t byte) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ESP_LOGV(TAG, "Received packet: sys=%04X, dev=%04X, data=%s, crc=%04X", system_address, device_address,
|
ESP_LOGV(TAG, "Received packet: addr=%08X, data=%s, crc=%04X", device_address,
|
||||||
format_hex(&packet[4], packet_len - 6).c_str(), crc);
|
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
|
// Handle packet
|
||||||
size_t data_len = (packet_len - 6) / 3;
|
size_t data_len = (packet_len - 6) / 3;
|
||||||
if (data_len == 0) {
|
if (data_len == 0) {
|
||||||
if (packet[4] == UPONOR_ID_REQUEST)
|
if (packet[4] == UPONOR_ID_REQUEST)
|
||||||
ESP_LOGVV(TAG, "Ignoring request packet for device 0x%04X", device_address);
|
ESP_LOGVV(TAG, "Ignoring request packet for device 0x%08X", device_address);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,7 +129,7 @@ bool UponorSmatrixComponent::parse_byte_(uint8_t byte) {
|
|||||||
if (data[i].id == UPONOR_ID_DATETIME1)
|
if (data[i].id == UPONOR_ID_DATETIME1)
|
||||||
found_time = true;
|
found_time = true;
|
||||||
if (found_temperature && found_time) {
|
if (found_temperature && found_time) {
|
||||||
ESP_LOGI(TAG, "Using detected time device address 0x%04X", device_address);
|
ESP_LOGI(TAG, "Using detected time device address 0x%08X", device_address);
|
||||||
this->time_device_address_ = device_address;
|
this->time_device_address_ = device_address;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -160,7 +148,7 @@ bool UponorSmatrixComponent::parse_byte_(uint8_t byte) {
|
|||||||
|
|
||||||
// Log unknown device addresses
|
// Log unknown device addresses
|
||||||
if (!found && !this->unknown_devices_.count(device_address)) {
|
if (!found && !this->unknown_devices_.count(device_address)) {
|
||||||
ESP_LOGI(TAG, "Received packet for unknown device address 0x%04X ", device_address);
|
ESP_LOGI(TAG, "Received packet for unknown device address 0x%08X ", device_address);
|
||||||
this->unknown_devices_.insert(device_address);
|
this->unknown_devices_.insert(device_address);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,16 +156,16 @@ bool UponorSmatrixComponent::parse_byte_(uint8_t byte) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool UponorSmatrixComponent::send(uint16_t device_address, const UponorSmatrixData *data, size_t data_len) {
|
bool UponorSmatrixComponent::send(uint32_t device_address, const UponorSmatrixData *data, size_t data_len) {
|
||||||
if (this->address_ == 0 || device_address == 0 || data == nullptr || data_len == 0)
|
if (device_address == 0 || data == nullptr || data_len == 0)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// Assemble packet for send queue. All fields are big-endian except for the little-endian checksum.
|
// Assemble packet for send queue. All fields are big-endian except for the little-endian checksum.
|
||||||
std::vector<uint8_t> packet;
|
std::vector<uint8_t> packet;
|
||||||
packet.reserve(6 + 3 * data_len);
|
packet.reserve(6 + 3 * data_len);
|
||||||
|
|
||||||
packet.push_back(this->address_ >> 8);
|
packet.push_back(device_address >> 24);
|
||||||
packet.push_back(this->address_ >> 0);
|
packet.push_back(device_address >> 16);
|
||||||
packet.push_back(device_address >> 8);
|
packet.push_back(device_address >> 8);
|
||||||
packet.push_back(device_address >> 0);
|
packet.push_back(device_address >> 0);
|
||||||
|
|
||||||
|
|||||||
@@ -71,23 +71,21 @@ class UponorSmatrixComponent : public uart::UARTDevice, public Component {
|
|||||||
void dump_config() override;
|
void dump_config() override;
|
||||||
void loop() override;
|
void loop() override;
|
||||||
|
|
||||||
void set_system_address(uint16_t address) { this->address_ = address; }
|
|
||||||
void register_device(UponorSmatrixDevice *device) { this->devices_.push_back(device); }
|
void register_device(UponorSmatrixDevice *device) { this->devices_.push_back(device); }
|
||||||
|
|
||||||
bool send(uint16_t device_address, const UponorSmatrixData *data, size_t data_len);
|
bool send(uint32_t device_address, const UponorSmatrixData *data, size_t data_len);
|
||||||
|
|
||||||
#ifdef USE_TIME
|
#ifdef USE_TIME
|
||||||
void set_time_id(time::RealTimeClock *time_id) { this->time_id_ = time_id; }
|
void set_time_id(time::RealTimeClock *time_id) { this->time_id_ = time_id; }
|
||||||
void set_time_device_address(uint16_t address) { this->time_device_address_ = address; }
|
void set_time_device_address(uint32_t address) { this->time_device_address_ = address; }
|
||||||
void send_time() { this->send_time_requested_ = true; }
|
void send_time() { this->send_time_requested_ = true; }
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
bool parse_byte_(uint8_t byte);
|
bool parse_byte_(uint8_t byte);
|
||||||
|
|
||||||
uint16_t address_;
|
|
||||||
std::vector<UponorSmatrixDevice *> devices_;
|
std::vector<UponorSmatrixDevice *> devices_;
|
||||||
std::set<uint16_t> unknown_devices_;
|
std::set<uint32_t> unknown_devices_;
|
||||||
|
|
||||||
std::vector<uint8_t> rx_buffer_;
|
std::vector<uint8_t> rx_buffer_;
|
||||||
std::queue<std::vector<uint8_t>> tx_queue_;
|
std::queue<std::vector<uint8_t>> tx_queue_;
|
||||||
@@ -96,7 +94,7 @@ class UponorSmatrixComponent : public uart::UARTDevice, public Component {
|
|||||||
|
|
||||||
#ifdef USE_TIME
|
#ifdef USE_TIME
|
||||||
time::RealTimeClock *time_id_{nullptr};
|
time::RealTimeClock *time_id_{nullptr};
|
||||||
uint16_t time_device_address_;
|
uint32_t time_device_address_;
|
||||||
bool send_time_requested_;
|
bool send_time_requested_;
|
||||||
bool do_send_time_();
|
bool do_send_time_();
|
||||||
#endif
|
#endif
|
||||||
@@ -104,7 +102,7 @@ class UponorSmatrixComponent : public uart::UARTDevice, public Component {
|
|||||||
|
|
||||||
class UponorSmatrixDevice : public Parented<UponorSmatrixComponent> {
|
class UponorSmatrixDevice : public Parented<UponorSmatrixComponent> {
|
||||||
public:
|
public:
|
||||||
void set_device_address(uint16_t address) { this->address_ = address; }
|
void set_address(uint32_t address) { this->address_ = address; }
|
||||||
|
|
||||||
virtual void on_device_data(const UponorSmatrixData *data, size_t data_len) = 0;
|
virtual void on_device_data(const UponorSmatrixData *data, size_t data_len) = 0;
|
||||||
bool send(const UponorSmatrixData *data, size_t data_len) {
|
bool send(const UponorSmatrixData *data, size_t data_len) {
|
||||||
@@ -113,7 +111,7 @@ class UponorSmatrixDevice : public Parented<UponorSmatrixComponent> {
|
|||||||
|
|
||||||
protected:
|
protected:
|
||||||
friend UponorSmatrixComponent;
|
friend UponorSmatrixComponent;
|
||||||
uint16_t address_;
|
uint32_t address_;
|
||||||
};
|
};
|
||||||
|
|
||||||
inline float raw_to_celsius(uint16_t raw) {
|
inline float raw_to_celsius(uint16_t raw) {
|
||||||
|
|||||||
@@ -81,7 +81,9 @@ const uint32_t YASHIMA_CARRIER_FREQUENCY = 38000;
|
|||||||
|
|
||||||
climate::ClimateTraits YashimaClimate::traits() {
|
climate::ClimateTraits YashimaClimate::traits() {
|
||||||
auto traits = climate::ClimateTraits();
|
auto traits = climate::ClimateTraits();
|
||||||
traits.set_supports_current_temperature(this->sensor_ != nullptr);
|
if (this->sensor_ != nullptr) {
|
||||||
|
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
|
||||||
|
}
|
||||||
|
|
||||||
traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_HEAT_COOL});
|
traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_HEAT_COOL});
|
||||||
if (supports_cool_)
|
if (supports_cool_)
|
||||||
@@ -89,7 +91,6 @@ climate::ClimateTraits YashimaClimate::traits() {
|
|||||||
if (supports_heat_)
|
if (supports_heat_)
|
||||||
traits.add_supported_mode(climate::CLIMATE_MODE_HEAT);
|
traits.add_supported_mode(climate::CLIMATE_MODE_HEAT);
|
||||||
|
|
||||||
traits.set_supports_two_point_target_temperature(false);
|
|
||||||
traits.set_visual_min_temperature(YASHIMA_TEMP_MIN);
|
traits.set_visual_min_temperature(YASHIMA_TEMP_MIN);
|
||||||
traits.set_visual_max_temperature(YASHIMA_TEMP_MAX);
|
traits.set_visual_max_temperature(YASHIMA_TEMP_MAX);
|
||||||
traits.set_visual_temperature_step(1);
|
traits.set_visual_temperature_step(1);
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
#include "esphome/components/text_sensor/text_sensor.h"
|
#include "esphome/components/text_sensor/text_sensor.h"
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_FAN
|
#ifdef USE_FAN
|
||||||
#include "esphome/components/fan/fan_state.h"
|
#include "esphome/components/fan/fan.h"
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_CLIMATE
|
#ifdef USE_CLIMATE
|
||||||
#include "esphome/components/climate/climate.h"
|
#include "esphome/components/climate/climate.h"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_FAN
|
#ifdef USE_FAN
|
||||||
#include "esphome/components/fan/fan_state.h"
|
#include "esphome/components/fan/fan.h"
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_LIGHT
|
#ifdef USE_LIGHT
|
||||||
#include "esphome/components/light/light_state.h"
|
#include "esphome/components/light/light_state.h"
|
||||||
|
|||||||
@@ -273,6 +273,8 @@
|
|||||||
|
|
||||||
#ifdef USE_NRF52
|
#ifdef USE_NRF52
|
||||||
#define USE_NRF52_DFU
|
#define USE_NRF52_DFU
|
||||||
|
#define USE_SOFTDEVICE_ID 7
|
||||||
|
#define USE_SOFTDEVICE_VERSION 1
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Disabled feature flags
|
// Disabled feature flags
|
||||||
|
|||||||
@@ -1161,18 +1161,4 @@ template<typename T, enable_if_t<std::is_pointer<T *>::value, int> = 0> T &id(T
|
|||||||
|
|
||||||
///@}
|
///@}
|
||||||
|
|
||||||
/// @name Deprecated functions
|
|
||||||
///@{
|
|
||||||
|
|
||||||
ESPDEPRECATED("hexencode() is deprecated, use format_hex_pretty() instead.", "2022.1")
|
|
||||||
inline std::string hexencode(const uint8_t *data, uint32_t len) { return format_hex_pretty(data, len); }
|
|
||||||
|
|
||||||
template<typename T>
|
|
||||||
ESPDEPRECATED("hexencode() is deprecated, use format_hex_pretty() instead.", "2022.1")
|
|
||||||
std::string hexencode(const T &data) {
|
|
||||||
return hexencode(data.data(), data.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
///@}
|
|
||||||
|
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
pylint==4.0.1
|
pylint==4.0.2
|
||||||
flake8==7.3.0 # also change in .pre-commit-config.yaml when updating
|
flake8==7.3.0 # also change in .pre-commit-config.yaml when updating
|
||||||
ruff==0.14.1 # also change in .pre-commit-config.yaml when updating
|
ruff==0.14.1 # also change in .pre-commit-config.yaml when updating
|
||||||
pyupgrade==3.21.0 # also change in .pre-commit-config.yaml when updating
|
pyupgrade==3.21.0 # also change in .pre-commit-config.yaml when updating
|
||||||
|
|||||||
88
script/ci_add_metadata_to_json.py
Executable file
88
script/ci_add_metadata_to_json.py
Executable file
@@ -0,0 +1,88 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Add metadata to memory analysis JSON file.
|
||||||
|
|
||||||
|
This script adds components and platform metadata to an existing
|
||||||
|
memory analysis JSON file. Used by CI to ensure all required fields are present
|
||||||
|
for the comment script.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
"""Main entry point."""
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Add metadata to memory analysis JSON file"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--json-file",
|
||||||
|
required=True,
|
||||||
|
help="Path to JSON file to update",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--components",
|
||||||
|
required=True,
|
||||||
|
help='JSON array of component names (e.g., \'["api", "wifi"]\')',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--platform",
|
||||||
|
required=True,
|
||||||
|
help="Platform name",
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Load existing JSON
|
||||||
|
json_path = Path(args.json_file)
|
||||||
|
if not json_path.exists():
|
||||||
|
print(f"Error: JSON file not found: {args.json_file}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(json_path, encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
except (json.JSONDecodeError, OSError) as e:
|
||||||
|
print(f"Error loading JSON: {e}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# Parse components
|
||||||
|
try:
|
||||||
|
components = json.loads(args.components)
|
||||||
|
if not isinstance(components, list):
|
||||||
|
print("Error: --components must be a JSON array", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
# Element-level validation: ensure each component is a non-empty string
|
||||||
|
for idx, comp in enumerate(components):
|
||||||
|
if not isinstance(comp, str) or not comp.strip():
|
||||||
|
print(
|
||||||
|
f"Error: component at index {idx} is not a non-empty string: {comp!r}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
return 1
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
print(f"Error parsing components: {e}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# Add metadata
|
||||||
|
data["components"] = components
|
||||||
|
data["platform"] = args.platform
|
||||||
|
|
||||||
|
# Write back
|
||||||
|
try:
|
||||||
|
with open(json_path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f, indent=2)
|
||||||
|
print(f"Added metadata to {args.json_file}", file=sys.stderr)
|
||||||
|
except OSError as e:
|
||||||
|
print(f"Error writing JSON: {e}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -24,6 +24,37 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
|
|||||||
# Comment marker to identify our memory impact comments
|
# Comment marker to identify our memory impact comments
|
||||||
COMMENT_MARKER = "<!-- esphome-memory-impact-analysis -->"
|
COMMENT_MARKER = "<!-- esphome-memory-impact-analysis -->"
|
||||||
|
|
||||||
|
|
||||||
|
def run_gh_command(args: list[str], operation: str) -> subprocess.CompletedProcess:
|
||||||
|
"""Run a gh CLI command with error handling.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args: Command arguments (including 'gh')
|
||||||
|
operation: Description of the operation for error messages
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
CompletedProcess result
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
subprocess.CalledProcessError: If command fails (with detailed error output)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return subprocess.run(
|
||||||
|
args,
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(
|
||||||
|
f"ERROR: {operation} failed with exit code {e.returncode}", file=sys.stderr
|
||||||
|
)
|
||||||
|
print(f"ERROR: Command: {' '.join(args)}", file=sys.stderr)
|
||||||
|
print(f"ERROR: stdout: {e.stdout}", file=sys.stderr)
|
||||||
|
print(f"ERROR: stderr: {e.stderr}", file=sys.stderr)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
# Thresholds for emoji significance indicators (percentage)
|
# Thresholds for emoji significance indicators (percentage)
|
||||||
OVERALL_CHANGE_THRESHOLD = 1.0 # Overall RAM/Flash changes
|
OVERALL_CHANGE_THRESHOLD = 1.0 # Overall RAM/Flash changes
|
||||||
COMPONENT_CHANGE_THRESHOLD = 3.0 # Component breakdown changes
|
COMPONENT_CHANGE_THRESHOLD = 3.0 # Component breakdown changes
|
||||||
@@ -238,7 +269,6 @@ def create_comment_body(
|
|||||||
pr_analysis: dict | None = None,
|
pr_analysis: dict | None = None,
|
||||||
target_symbols: dict | None = None,
|
target_symbols: dict | None = None,
|
||||||
pr_symbols: dict | None = None,
|
pr_symbols: dict | None = None,
|
||||||
target_cache_hit: bool = False,
|
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Create the comment body with memory impact analysis using Jinja2 templates.
|
"""Create the comment body with memory impact analysis using Jinja2 templates.
|
||||||
|
|
||||||
@@ -253,7 +283,6 @@ def create_comment_body(
|
|||||||
pr_analysis: Optional component breakdown for PR branch
|
pr_analysis: Optional component breakdown for PR branch
|
||||||
target_symbols: Optional symbol map for target branch
|
target_symbols: Optional symbol map for target branch
|
||||||
pr_symbols: Optional symbol map for PR branch
|
pr_symbols: Optional symbol map for PR branch
|
||||||
target_cache_hit: Whether target branch analysis was loaded from cache
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Formatted comment body
|
Formatted comment body
|
||||||
@@ -283,7 +312,6 @@ def create_comment_body(
|
|||||||
"flash_change": format_change(
|
"flash_change": format_change(
|
||||||
target_flash, pr_flash, threshold=OVERALL_CHANGE_THRESHOLD
|
target_flash, pr_flash, threshold=OVERALL_CHANGE_THRESHOLD
|
||||||
),
|
),
|
||||||
"target_cache_hit": target_cache_hit,
|
|
||||||
"component_change_threshold": COMPONENT_CHANGE_THRESHOLD,
|
"component_change_threshold": COMPONENT_CHANGE_THRESHOLD,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,7 +384,7 @@ def find_existing_comment(pr_number: str) -> str | None:
|
|||||||
print(f"DEBUG: Looking for existing comment on PR #{pr_number}", file=sys.stderr)
|
print(f"DEBUG: Looking for existing comment on PR #{pr_number}", file=sys.stderr)
|
||||||
|
|
||||||
# Use gh api to get comments directly - this returns the numeric id field
|
# Use gh api to get comments directly - this returns the numeric id field
|
||||||
result = subprocess.run(
|
result = run_gh_command(
|
||||||
[
|
[
|
||||||
"gh",
|
"gh",
|
||||||
"api",
|
"api",
|
||||||
@@ -364,9 +392,7 @@ def find_existing_comment(pr_number: str) -> str | None:
|
|||||||
"--jq",
|
"--jq",
|
||||||
".[] | {id, body}",
|
".[] | {id, body}",
|
||||||
],
|
],
|
||||||
capture_output=True,
|
operation="Get PR comments",
|
||||||
text=True,
|
|
||||||
check=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
print(
|
print(
|
||||||
@@ -420,7 +446,8 @@ def update_existing_comment(comment_id: str, comment_body: str) -> None:
|
|||||||
subprocess.CalledProcessError: If gh command fails
|
subprocess.CalledProcessError: If gh command fails
|
||||||
"""
|
"""
|
||||||
print(f"DEBUG: Updating existing comment {comment_id}", file=sys.stderr)
|
print(f"DEBUG: Updating existing comment {comment_id}", file=sys.stderr)
|
||||||
result = subprocess.run(
|
print(f"DEBUG: Comment body length: {len(comment_body)} bytes", file=sys.stderr)
|
||||||
|
result = run_gh_command(
|
||||||
[
|
[
|
||||||
"gh",
|
"gh",
|
||||||
"api",
|
"api",
|
||||||
@@ -430,9 +457,7 @@ def update_existing_comment(comment_id: str, comment_body: str) -> None:
|
|||||||
"-f",
|
"-f",
|
||||||
f"body={comment_body}",
|
f"body={comment_body}",
|
||||||
],
|
],
|
||||||
check=True,
|
operation="Update PR comment",
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
)
|
||||||
print(f"DEBUG: Update response: {result.stdout}", file=sys.stderr)
|
print(f"DEBUG: Update response: {result.stdout}", file=sys.stderr)
|
||||||
|
|
||||||
@@ -448,11 +473,10 @@ def create_new_comment(pr_number: str, comment_body: str) -> None:
|
|||||||
subprocess.CalledProcessError: If gh command fails
|
subprocess.CalledProcessError: If gh command fails
|
||||||
"""
|
"""
|
||||||
print(f"DEBUG: Posting new comment on PR #{pr_number}", file=sys.stderr)
|
print(f"DEBUG: Posting new comment on PR #{pr_number}", file=sys.stderr)
|
||||||
result = subprocess.run(
|
print(f"DEBUG: Comment body length: {len(comment_body)} bytes", file=sys.stderr)
|
||||||
|
result = run_gh_command(
|
||||||
["gh", "pr", "comment", pr_number, "--body", comment_body],
|
["gh", "pr", "comment", pr_number, "--body", comment_body],
|
||||||
check=True,
|
operation="Create PR comment",
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
)
|
||||||
print(f"DEBUG: Post response: {result.stdout}", file=sys.stderr)
|
print(f"DEBUG: Post response: {result.stdout}", file=sys.stderr)
|
||||||
|
|
||||||
@@ -484,80 +508,129 @@ def main() -> int:
|
|||||||
description="Post or update PR comment with memory impact analysis"
|
description="Post or update PR comment with memory impact analysis"
|
||||||
)
|
)
|
||||||
parser.add_argument("--pr-number", required=True, help="PR number")
|
parser.add_argument("--pr-number", required=True, help="PR number")
|
||||||
parser.add_argument(
|
|
||||||
"--components",
|
|
||||||
required=True,
|
|
||||||
help='JSON array of component names (e.g., \'["api", "wifi"]\')',
|
|
||||||
)
|
|
||||||
parser.add_argument("--platform", required=True, help="Platform name")
|
|
||||||
parser.add_argument(
|
|
||||||
"--target-ram", type=int, required=True, help="Target branch RAM usage"
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--target-flash", type=int, required=True, help="Target branch flash usage"
|
|
||||||
)
|
|
||||||
parser.add_argument("--pr-ram", type=int, required=True, help="PR branch RAM usage")
|
|
||||||
parser.add_argument(
|
|
||||||
"--pr-flash", type=int, required=True, help="PR branch flash usage"
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--target-json",
|
"--target-json",
|
||||||
help="Optional path to target branch analysis JSON (for detailed analysis)",
|
required=True,
|
||||||
|
help="Path to target branch analysis JSON file",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--pr-json",
|
"--pr-json",
|
||||||
help="Optional path to PR branch analysis JSON (for detailed analysis)",
|
required=True,
|
||||||
)
|
help="Path to PR branch analysis JSON file",
|
||||||
parser.add_argument(
|
|
||||||
"--target-cache-hit",
|
|
||||||
action="store_true",
|
|
||||||
help="Indicates that target branch analysis was loaded from cache",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
# Parse components from JSON
|
# Load analysis JSON files (all data comes from JSON for security)
|
||||||
try:
|
target_data: dict | None = load_analysis_json(args.target_json)
|
||||||
components = json.loads(args.components)
|
if not target_data:
|
||||||
if not isinstance(components, list):
|
print("Error: Failed to load target analysis JSON", file=sys.stderr)
|
||||||
print("Error: --components must be a JSON array", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
print(f"Error parsing --components JSON: {e}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Load analysis JSON files
|
pr_data: dict | None = load_analysis_json(args.pr_json)
|
||||||
target_analysis = None
|
if not pr_data:
|
||||||
pr_analysis = None
|
print("Error: Failed to load PR analysis JSON", file=sys.stderr)
|
||||||
target_symbols = None
|
sys.exit(1)
|
||||||
pr_symbols = None
|
|
||||||
|
|
||||||
if args.target_json:
|
# Extract detailed analysis if available
|
||||||
target_data = load_analysis_json(args.target_json)
|
target_analysis: dict | None = None
|
||||||
if target_data and target_data.get("detailed_analysis"):
|
pr_analysis: dict | None = None
|
||||||
|
target_symbols: dict | None = None
|
||||||
|
pr_symbols: dict | None = None
|
||||||
|
|
||||||
|
if target_data.get("detailed_analysis"):
|
||||||
target_analysis = target_data["detailed_analysis"].get("components")
|
target_analysis = target_data["detailed_analysis"].get("components")
|
||||||
target_symbols = target_data["detailed_analysis"].get("symbols")
|
target_symbols = target_data["detailed_analysis"].get("symbols")
|
||||||
|
|
||||||
if args.pr_json:
|
if pr_data.get("detailed_analysis"):
|
||||||
pr_data = load_analysis_json(args.pr_json)
|
|
||||||
if pr_data and pr_data.get("detailed_analysis"):
|
|
||||||
pr_analysis = pr_data["detailed_analysis"].get("components")
|
pr_analysis = pr_data["detailed_analysis"].get("components")
|
||||||
pr_symbols = pr_data["detailed_analysis"].get("symbols")
|
pr_symbols = pr_data["detailed_analysis"].get("symbols")
|
||||||
|
|
||||||
|
# Extract all values from JSON files (prevents shell injection from PR code)
|
||||||
|
components = target_data.get("components")
|
||||||
|
platform = target_data.get("platform")
|
||||||
|
target_ram = target_data.get("ram_bytes")
|
||||||
|
target_flash = target_data.get("flash_bytes")
|
||||||
|
pr_ram = pr_data.get("ram_bytes")
|
||||||
|
pr_flash = pr_data.get("flash_bytes")
|
||||||
|
|
||||||
|
# Validate required fields and types
|
||||||
|
missing_fields: list[str] = []
|
||||||
|
type_errors: list[str] = []
|
||||||
|
|
||||||
|
if components is None:
|
||||||
|
missing_fields.append("components")
|
||||||
|
elif not isinstance(components, list):
|
||||||
|
type_errors.append(
|
||||||
|
f"components must be a list, got {type(components).__name__}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
for idx, comp in enumerate(components):
|
||||||
|
if not isinstance(comp, str):
|
||||||
|
type_errors.append(
|
||||||
|
f"components[{idx}] must be a string, got {type(comp).__name__}"
|
||||||
|
)
|
||||||
|
if platform is None:
|
||||||
|
missing_fields.append("platform")
|
||||||
|
elif not isinstance(platform, str):
|
||||||
|
type_errors.append(f"platform must be a string, got {type(platform).__name__}")
|
||||||
|
|
||||||
|
if target_ram is None:
|
||||||
|
missing_fields.append("target.ram_bytes")
|
||||||
|
elif not isinstance(target_ram, int):
|
||||||
|
type_errors.append(
|
||||||
|
f"target.ram_bytes must be an integer, got {type(target_ram).__name__}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if target_flash is None:
|
||||||
|
missing_fields.append("target.flash_bytes")
|
||||||
|
elif not isinstance(target_flash, int):
|
||||||
|
type_errors.append(
|
||||||
|
f"target.flash_bytes must be an integer, got {type(target_flash).__name__}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if pr_ram is None:
|
||||||
|
missing_fields.append("pr.ram_bytes")
|
||||||
|
elif not isinstance(pr_ram, int):
|
||||||
|
type_errors.append(
|
||||||
|
f"pr.ram_bytes must be an integer, got {type(pr_ram).__name__}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if pr_flash is None:
|
||||||
|
missing_fields.append("pr.flash_bytes")
|
||||||
|
elif not isinstance(pr_flash, int):
|
||||||
|
type_errors.append(
|
||||||
|
f"pr.flash_bytes must be an integer, got {type(pr_flash).__name__}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if missing_fields or type_errors:
|
||||||
|
if missing_fields:
|
||||||
|
print(
|
||||||
|
f"Error: JSON files missing required fields: {', '.join(missing_fields)}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
if type_errors:
|
||||||
|
print(
|
||||||
|
f"Error: Type validation failed: {'; '.join(type_errors)}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
print(f"Target JSON keys: {list(target_data.keys())}", file=sys.stderr)
|
||||||
|
print(f"PR JSON keys: {list(pr_data.keys())}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
# Create comment body
|
# Create comment body
|
||||||
# Note: Memory totals (RAM/Flash) are summed across all builds if multiple were run.
|
# Note: Memory totals (RAM/Flash) are summed across all builds if multiple were run.
|
||||||
comment_body = create_comment_body(
|
comment_body = create_comment_body(
|
||||||
components=components,
|
components=components,
|
||||||
platform=args.platform,
|
platform=platform,
|
||||||
target_ram=args.target_ram,
|
target_ram=target_ram,
|
||||||
target_flash=args.target_flash,
|
target_flash=target_flash,
|
||||||
pr_ram=args.pr_ram,
|
pr_ram=pr_ram,
|
||||||
pr_flash=args.pr_flash,
|
pr_flash=pr_flash,
|
||||||
target_analysis=target_analysis,
|
target_analysis=target_analysis,
|
||||||
pr_analysis=pr_analysis,
|
pr_analysis=pr_analysis,
|
||||||
target_symbols=target_symbols,
|
target_symbols=target_symbols,
|
||||||
pr_symbols=pr_symbols,
|
pr_symbols=pr_symbols,
|
||||||
target_cache_hit=args.target_cache_hit,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Post or update comment
|
# Post or update comment
|
||||||
|
|||||||
@@ -61,6 +61,11 @@ from helpers import (
|
|||||||
root_path,
|
root_path,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Threshold for splitting clang-tidy jobs
|
||||||
|
# For small PRs (< 65 files), use nosplit for faster CI
|
||||||
|
# For large PRs (>= 65 files), use split for better parallelization
|
||||||
|
CLANG_TIDY_SPLIT_THRESHOLD = 65
|
||||||
|
|
||||||
|
|
||||||
class Platform(StrEnum):
|
class Platform(StrEnum):
|
||||||
"""Platform identifiers for memory impact analysis."""
|
"""Platform identifiers for memory impact analysis."""
|
||||||
@@ -78,11 +83,16 @@ MEMORY_IMPACT_FALLBACK_COMPONENT = "api" # Representative component for core ch
|
|||||||
MEMORY_IMPACT_FALLBACK_PLATFORM = Platform.ESP32_IDF # Most representative platform
|
MEMORY_IMPACT_FALLBACK_PLATFORM = Platform.ESP32_IDF # Most representative platform
|
||||||
|
|
||||||
# Platform preference order for memory impact analysis
|
# Platform preference order for memory impact analysis
|
||||||
# Prefer newer platforms first as they represent the future of ESPHome
|
# This order is used when no platform-specific hints are detected from filenames
|
||||||
# ESP8266 is most constrained but many new features don't support it
|
# Priority rationale:
|
||||||
|
# 1. ESP32-C6 IDF - Newest platform, supports Thread/Zigbee
|
||||||
|
# 2. ESP8266 Arduino - Most memory constrained (best for detecting memory impact),
|
||||||
|
# fastest build times, most sensitive to code size changes
|
||||||
|
# 3. ESP32 IDF - Primary ESP32 platform, most representative of modern ESPHome
|
||||||
|
# 4-6. Other ESP32 variants - Less commonly used but still supported
|
||||||
MEMORY_IMPACT_PLATFORM_PREFERENCE = [
|
MEMORY_IMPACT_PLATFORM_PREFERENCE = [
|
||||||
Platform.ESP32_C6_IDF, # ESP32-C6 IDF (newest, supports Thread/Zigbee)
|
Platform.ESP32_C6_IDF, # ESP32-C6 IDF (newest, supports Thread/Zigbee)
|
||||||
Platform.ESP8266_ARD, # ESP8266 Arduino (most memory constrained - best for impact analysis)
|
Platform.ESP8266_ARD, # ESP8266 Arduino (most memory constrained, fastest builds)
|
||||||
Platform.ESP32_IDF, # ESP32 IDF platform (primary ESP32 platform, most representative)
|
Platform.ESP32_IDF, # ESP32 IDF platform (primary ESP32 platform, most representative)
|
||||||
Platform.ESP32_C3_IDF, # ESP32-C3 IDF
|
Platform.ESP32_C3_IDF, # ESP32-C3 IDF
|
||||||
Platform.ESP32_S2_IDF, # ESP32-S2 IDF
|
Platform.ESP32_S2_IDF, # ESP32-S2 IDF
|
||||||
@@ -210,6 +220,22 @@ def should_run_clang_tidy(branch: str | None = None) -> bool:
|
|||||||
return _any_changed_file_endswith(branch, CPP_FILE_EXTENSIONS)
|
return _any_changed_file_endswith(branch, CPP_FILE_EXTENSIONS)
|
||||||
|
|
||||||
|
|
||||||
|
def count_changed_cpp_files(branch: str | None = None) -> int:
|
||||||
|
"""Count the number of changed C++ files.
|
||||||
|
|
||||||
|
This is used to determine whether to split clang-tidy jobs or run them as a single job.
|
||||||
|
For PRs with < 65 changed C++ files, running a single job is faster than splitting.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
branch: Branch to compare against. If None, uses default.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Number of changed C++ files.
|
||||||
|
"""
|
||||||
|
files = changed_files(branch)
|
||||||
|
return sum(1 for file in files if file.endswith(CPP_FILE_EXTENSIONS))
|
||||||
|
|
||||||
|
|
||||||
def should_run_clang_format(branch: str | None = None) -> bool:
|
def should_run_clang_format(branch: str | None = None) -> bool:
|
||||||
"""Determine if clang-format should run based on changed files.
|
"""Determine if clang-format should run based on changed files.
|
||||||
|
|
||||||
@@ -264,6 +290,91 @@ def _component_has_tests(component: str) -> bool:
|
|||||||
return bool(get_component_test_files(component))
|
return bool(get_component_test_files(component))
|
||||||
|
|
||||||
|
|
||||||
|
def _select_platform_by_preference(
|
||||||
|
platforms: list[Platform] | set[Platform],
|
||||||
|
) -> Platform:
|
||||||
|
"""Select the most preferred platform from a list/set based on MEMORY_IMPACT_PLATFORM_PREFERENCE.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
platforms: List or set of platforms to choose from
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The most preferred platform (earliest in MEMORY_IMPACT_PLATFORM_PREFERENCE)
|
||||||
|
"""
|
||||||
|
return min(platforms, key=MEMORY_IMPACT_PLATFORM_PREFERENCE.index)
|
||||||
|
|
||||||
|
|
||||||
|
def _select_platform_by_count(
|
||||||
|
platform_counts: Counter[Platform],
|
||||||
|
) -> Platform:
|
||||||
|
"""Select platform by count, using MEMORY_IMPACT_PLATFORM_PREFERENCE as tiebreaker.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
platform_counts: Counter mapping platforms to their counts
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Platform with highest count, breaking ties by preference order
|
||||||
|
"""
|
||||||
|
return min(
|
||||||
|
platform_counts.keys(),
|
||||||
|
key=lambda p: (
|
||||||
|
-platform_counts[p], # Negative to prefer higher counts
|
||||||
|
MEMORY_IMPACT_PLATFORM_PREFERENCE.index(p),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _detect_platform_hint_from_filename(filename: str) -> Platform | None:
|
||||||
|
"""Detect platform hint from filename patterns.
|
||||||
|
|
||||||
|
Detects platform-specific files using patterns like:
|
||||||
|
- wifi_component_esp_idf.cpp, *_idf.h -> ESP32 IDF variants
|
||||||
|
- wifi_component_esp8266.cpp, *_esp8266.h -> ESP8266_ARD
|
||||||
|
- *_esp32*.cpp -> ESP32 IDF (generic)
|
||||||
|
- *_libretiny.cpp, *_retiny.* -> LibreTiny (not in preference list)
|
||||||
|
- *_pico.cpp, *_rp2040.* -> RP2040 (not in preference list)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filename: File path to check
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Platform enum if a specific platform is detected, None otherwise
|
||||||
|
"""
|
||||||
|
filename_lower = filename.lower()
|
||||||
|
|
||||||
|
# ESP-IDF platforms (check specific variants first)
|
||||||
|
if "esp_idf" in filename_lower or "_idf" in filename_lower:
|
||||||
|
# Check for specific ESP32 variants
|
||||||
|
if "c6" in filename_lower or "esp32c6" in filename_lower:
|
||||||
|
return Platform.ESP32_C6_IDF
|
||||||
|
if "c3" in filename_lower or "esp32c3" in filename_lower:
|
||||||
|
return Platform.ESP32_C3_IDF
|
||||||
|
if "s2" in filename_lower or "esp32s2" in filename_lower:
|
||||||
|
return Platform.ESP32_S2_IDF
|
||||||
|
if "s3" in filename_lower or "esp32s3" in filename_lower:
|
||||||
|
return Platform.ESP32_S3_IDF
|
||||||
|
# Default to ESP32 IDF for generic esp_idf files
|
||||||
|
return Platform.ESP32_IDF
|
||||||
|
|
||||||
|
# ESP8266 Arduino
|
||||||
|
if "esp8266" in filename_lower:
|
||||||
|
return Platform.ESP8266_ARD
|
||||||
|
|
||||||
|
# Generic ESP32 (without _idf suffix, could be Arduino or shared code)
|
||||||
|
# Prefer IDF as it's the modern platform
|
||||||
|
if "esp32" in filename_lower:
|
||||||
|
return Platform.ESP32_IDF
|
||||||
|
|
||||||
|
# LibreTiny and RP2040 are not in MEMORY_IMPACT_PLATFORM_PREFERENCE
|
||||||
|
# so we don't return them as hints
|
||||||
|
# if "retiny" in filename_lower or "libretiny" in filename_lower:
|
||||||
|
# return None # No specific LibreTiny platform preference
|
||||||
|
# if "pico" in filename_lower or "rp2040" in filename_lower:
|
||||||
|
# return None # No RP2040 platform preference
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def detect_memory_impact_config(
|
def detect_memory_impact_config(
|
||||||
branch: str | None = None,
|
branch: str | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
@@ -273,6 +384,12 @@ def detect_memory_impact_config(
|
|||||||
building a merged configuration with all changed components (like
|
building a merged configuration with all changed components (like
|
||||||
test_build_components.py does) to get comprehensive memory analysis.
|
test_build_components.py does) to get comprehensive memory analysis.
|
||||||
|
|
||||||
|
When platform-specific files are detected (e.g., wifi_component_esp_idf.cpp),
|
||||||
|
prefers that platform for testing to ensure the most relevant memory analysis.
|
||||||
|
|
||||||
|
For core C++ file changes without component changes, runs a fallback
|
||||||
|
analysis using a representative component to measure the impact.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
branch: Branch to compare against
|
branch: Branch to compare against
|
||||||
|
|
||||||
@@ -288,8 +405,10 @@ def detect_memory_impact_config(
|
|||||||
files = changed_files(branch)
|
files = changed_files(branch)
|
||||||
|
|
||||||
# Find all changed components (excluding core and base bus components)
|
# Find all changed components (excluding core and base bus components)
|
||||||
|
# Also collect platform hints from platform-specific filenames
|
||||||
changed_component_set: set[str] = set()
|
changed_component_set: set[str] = set()
|
||||||
has_core_changes = False
|
has_core_cpp_changes = False
|
||||||
|
platform_hints: list[Platform] = []
|
||||||
|
|
||||||
for file in files:
|
for file in files:
|
||||||
component = get_component_from_path(file)
|
component = get_component_from_path(file)
|
||||||
@@ -297,22 +416,27 @@ def detect_memory_impact_config(
|
|||||||
# Skip base bus components as they're used across many builds
|
# Skip base bus components as they're used across many builds
|
||||||
if component not in BASE_BUS_COMPONENTS:
|
if component not in BASE_BUS_COMPONENTS:
|
||||||
changed_component_set.add(component)
|
changed_component_set.add(component)
|
||||||
elif file.startswith("esphome/"):
|
# Check if this is a platform-specific file
|
||||||
# Core ESPHome files changed (not component-specific)
|
platform_hint = _detect_platform_hint_from_filename(file)
|
||||||
has_core_changes = True
|
if platform_hint:
|
||||||
|
platform_hints.append(platform_hint)
|
||||||
|
elif file.startswith("esphome/") and file.endswith(CPP_FILE_EXTENSIONS):
|
||||||
|
# Core ESPHome C++ files changed (not component-specific)
|
||||||
|
# Only C++ files affect memory usage
|
||||||
|
has_core_cpp_changes = True
|
||||||
|
|
||||||
# If no components changed but core changed, test representative component
|
# If no components changed but core C++ changed, test representative component
|
||||||
force_fallback_platform = False
|
force_fallback_platform = False
|
||||||
if not changed_component_set and has_core_changes:
|
if not changed_component_set and has_core_cpp_changes:
|
||||||
print(
|
print(
|
||||||
f"Memory impact: No components changed, but core files changed. "
|
f"Memory impact: No components changed, but core C++ files changed. "
|
||||||
f"Testing {MEMORY_IMPACT_FALLBACK_COMPONENT} component on {MEMORY_IMPACT_FALLBACK_PLATFORM}.",
|
f"Testing {MEMORY_IMPACT_FALLBACK_COMPONENT} component on {MEMORY_IMPACT_FALLBACK_PLATFORM}.",
|
||||||
file=sys.stderr,
|
file=sys.stderr,
|
||||||
)
|
)
|
||||||
changed_component_set.add(MEMORY_IMPACT_FALLBACK_COMPONENT)
|
changed_component_set.add(MEMORY_IMPACT_FALLBACK_COMPONENT)
|
||||||
force_fallback_platform = True # Use fallback platform (most representative)
|
force_fallback_platform = True # Use fallback platform (most representative)
|
||||||
elif not changed_component_set:
|
elif not changed_component_set:
|
||||||
# No components and no core changes
|
# No components and no core C++ changes
|
||||||
return {"should_run": "false"}
|
return {"should_run": "false"}
|
||||||
|
|
||||||
# Find components that have tests and collect their supported platforms
|
# Find components that have tests and collect their supported platforms
|
||||||
@@ -352,27 +476,42 @@ def detect_memory_impact_config(
|
|||||||
common_platforms &= platforms
|
common_platforms &= platforms
|
||||||
|
|
||||||
# Select the most preferred platform from the common set
|
# Select the most preferred platform from the common set
|
||||||
# Exception: for core changes, use fallback platform (most representative of codebase)
|
# Priority order:
|
||||||
if force_fallback_platform:
|
# 1. Platform hints from filenames (e.g., wifi_component_esp_idf.cpp suggests ESP32_IDF)
|
||||||
|
# 2. Core changes use fallback platform (most representative of codebase)
|
||||||
|
# 3. Common platforms supported by all components
|
||||||
|
# 4. Most commonly supported platform
|
||||||
|
if platform_hints:
|
||||||
|
# Use most common platform hint that's also supported by all components
|
||||||
|
hint_counts = Counter(platform_hints)
|
||||||
|
# Filter to only hints that are in common_platforms (if any common platforms exist)
|
||||||
|
valid_hints = (
|
||||||
|
[h for h in hint_counts if h in common_platforms]
|
||||||
|
if common_platforms
|
||||||
|
else list(hint_counts.keys())
|
||||||
|
)
|
||||||
|
if valid_hints:
|
||||||
|
platform = _select_platform_by_count(
|
||||||
|
Counter({p: hint_counts[p] for p in valid_hints})
|
||||||
|
)
|
||||||
|
elif common_platforms:
|
||||||
|
# Hints exist but none match common platforms, use common platform logic
|
||||||
|
platform = _select_platform_by_preference(common_platforms)
|
||||||
|
else:
|
||||||
|
# Use the most common hint even if it's not in common platforms
|
||||||
|
platform = _select_platform_by_count(hint_counts)
|
||||||
|
elif force_fallback_platform:
|
||||||
platform = MEMORY_IMPACT_FALLBACK_PLATFORM
|
platform = MEMORY_IMPACT_FALLBACK_PLATFORM
|
||||||
elif common_platforms:
|
elif common_platforms:
|
||||||
# Pick the most preferred platform that all components support
|
# Pick the most preferred platform that all components support
|
||||||
platform = min(common_platforms, key=MEMORY_IMPACT_PLATFORM_PREFERENCE.index)
|
platform = _select_platform_by_preference(common_platforms)
|
||||||
else:
|
else:
|
||||||
# No common platform - pick the most commonly supported platform
|
# No common platform - pick the most commonly supported platform
|
||||||
# This allows testing components individually even if they can't be merged
|
|
||||||
# Count how many components support each platform
|
# Count how many components support each platform
|
||||||
platform_counts = Counter(
|
platform_counts = Counter(
|
||||||
p for platforms in component_platforms_map.values() for p in platforms
|
p for platforms in component_platforms_map.values() for p in platforms
|
||||||
)
|
)
|
||||||
# Pick the platform supported by most components, preferring earlier in MEMORY_IMPACT_PLATFORM_PREFERENCE
|
platform = _select_platform_by_count(platform_counts)
|
||||||
platform = max(
|
|
||||||
platform_counts.keys(),
|
|
||||||
key=lambda p: (
|
|
||||||
platform_counts[p],
|
|
||||||
-MEMORY_IMPACT_PLATFORM_PREFERENCE.index(p),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Debug output
|
# Debug output
|
||||||
print("Memory impact analysis:", file=sys.stderr)
|
print("Memory impact analysis:", file=sys.stderr)
|
||||||
@@ -382,6 +521,7 @@ def detect_memory_impact_config(
|
|||||||
f" Component platforms: {dict(sorted(component_platforms_map.items()))}",
|
f" Component platforms: {dict(sorted(component_platforms_map.items()))}",
|
||||||
file=sys.stderr,
|
file=sys.stderr,
|
||||||
)
|
)
|
||||||
|
print(f" Platform hints from filenames: {platform_hints}", file=sys.stderr)
|
||||||
print(f" Common platforms: {sorted(common_platforms)}", file=sys.stderr)
|
print(f" Common platforms: {sorted(common_platforms)}", file=sys.stderr)
|
||||||
print(f" Selected platform: {platform}", file=sys.stderr)
|
print(f" Selected platform: {platform}", file=sys.stderr)
|
||||||
|
|
||||||
@@ -408,6 +548,7 @@ def main() -> None:
|
|||||||
run_clang_tidy = should_run_clang_tidy(args.branch)
|
run_clang_tidy = should_run_clang_tidy(args.branch)
|
||||||
run_clang_format = should_run_clang_format(args.branch)
|
run_clang_format = should_run_clang_format(args.branch)
|
||||||
run_python_linters = should_run_python_linters(args.branch)
|
run_python_linters = should_run_python_linters(args.branch)
|
||||||
|
changed_cpp_file_count = count_changed_cpp_files(args.branch)
|
||||||
|
|
||||||
# Get both directly changed and all changed components (with dependencies) in one call
|
# Get both directly changed and all changed components (with dependencies) in one call
|
||||||
script_path = Path(__file__).parent / "list-components.py"
|
script_path = Path(__file__).parent / "list-components.py"
|
||||||
@@ -445,10 +586,19 @@ def main() -> None:
|
|||||||
# Detect components for memory impact analysis (merged config)
|
# Detect components for memory impact analysis (merged config)
|
||||||
memory_impact = detect_memory_impact_config(args.branch)
|
memory_impact = detect_memory_impact_config(args.branch)
|
||||||
|
|
||||||
|
if run_clang_tidy:
|
||||||
|
if changed_cpp_file_count < CLANG_TIDY_SPLIT_THRESHOLD:
|
||||||
|
clang_tidy_mode = "nosplit"
|
||||||
|
else:
|
||||||
|
clang_tidy_mode = "split"
|
||||||
|
else:
|
||||||
|
clang_tidy_mode = "disabled"
|
||||||
|
|
||||||
# Build output
|
# Build output
|
||||||
output: dict[str, Any] = {
|
output: dict[str, Any] = {
|
||||||
"integration_tests": run_integration,
|
"integration_tests": run_integration,
|
||||||
"clang_tidy": run_clang_tidy,
|
"clang_tidy": run_clang_tidy,
|
||||||
|
"clang_tidy_mode": clang_tidy_mode,
|
||||||
"clang_format": run_clang_format,
|
"clang_format": run_clang_format,
|
||||||
"python_linters": run_python_linters,
|
"python_linters": run_python_linters,
|
||||||
"changed_components": changed_components,
|
"changed_components": changed_components,
|
||||||
@@ -458,6 +608,7 @@ def main() -> None:
|
|||||||
"component_test_count": len(changed_components_with_tests),
|
"component_test_count": len(changed_components_with_tests),
|
||||||
"directly_changed_count": len(directly_changed_with_tests),
|
"directly_changed_count": len(directly_changed_with_tests),
|
||||||
"dependency_only_count": len(dependency_only_components),
|
"dependency_only_count": len(dependency_only_components),
|
||||||
|
"changed_cpp_file_count": changed_cpp_file_count,
|
||||||
"memory_impact": memory_impact,
|
"memory_impact": memory_impact,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
31
tests/components/climate/common.yaml
Normal file
31
tests/components/climate/common.yaml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
switch:
|
||||||
|
- platform: template
|
||||||
|
id: climate_heater_switch
|
||||||
|
optimistic: true
|
||||||
|
- platform: template
|
||||||
|
id: climate_cooler_switch
|
||||||
|
optimistic: true
|
||||||
|
|
||||||
|
sensor:
|
||||||
|
- platform: template
|
||||||
|
id: climate_temperature_sensor
|
||||||
|
lambda: |-
|
||||||
|
return 21.5;
|
||||||
|
update_interval: 60s
|
||||||
|
|
||||||
|
climate:
|
||||||
|
- platform: bang_bang
|
||||||
|
id: climate_test_climate
|
||||||
|
name: Test Climate
|
||||||
|
sensor: climate_temperature_sensor
|
||||||
|
default_target_temperature_low: 18°C
|
||||||
|
default_target_temperature_high: 24°C
|
||||||
|
idle_action:
|
||||||
|
- switch.turn_off: climate_heater_switch
|
||||||
|
- switch.turn_off: climate_cooler_switch
|
||||||
|
cool_action:
|
||||||
|
- switch.turn_on: climate_cooler_switch
|
||||||
|
- switch.turn_off: climate_heater_switch
|
||||||
|
heat_action:
|
||||||
|
- switch.turn_on: climate_heater_switch
|
||||||
|
- switch.turn_off: climate_cooler_switch
|
||||||
1
tests/components/climate/test.esp8266-ard.yaml
Normal file
1
tests/components/climate/test.esp8266-ard.yaml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<<: !include common.yaml
|
||||||
@@ -16,3 +16,4 @@ esp32_improv:
|
|||||||
authorizer: io0_button
|
authorizer: io0_button
|
||||||
authorized_duration: 1min
|
authorized_duration: 1min
|
||||||
status_indicator: built_in_led
|
status_indicator: built_in_led
|
||||||
|
next_url: "https://example.com/setup?device={{device_name}}&ip={{ip_address}}&version={{esphome_version}}"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
espnow:
|
espnow:
|
||||||
|
id: espnow_component
|
||||||
auto_add_peer: false
|
auto_add_peer: false
|
||||||
channel: 1
|
channel: 1
|
||||||
peers:
|
peers:
|
||||||
@@ -50,3 +51,26 @@ espnow:
|
|||||||
- format_mac_address_pretty(info.src_addr).c_str()
|
- format_mac_address_pretty(info.src_addr).c_str()
|
||||||
- format_hex_pretty(data, size).c_str()
|
- format_hex_pretty(data, size).c_str()
|
||||||
- info.rx_ctrl->rssi
|
- info.rx_ctrl->rssi
|
||||||
|
|
||||||
|
packet_transport:
|
||||||
|
- platform: espnow
|
||||||
|
id: transport1
|
||||||
|
espnow_id: espnow_component
|
||||||
|
peer_address: "FF:FF:FF:FF:FF:FF"
|
||||||
|
encryption:
|
||||||
|
key: "0123456789abcdef0123456789abcdef"
|
||||||
|
sensors:
|
||||||
|
- temp_sensor
|
||||||
|
providers:
|
||||||
|
- name: test_provider
|
||||||
|
encryption:
|
||||||
|
key: "0123456789abcdef0123456789abcdef"
|
||||||
|
|
||||||
|
sensor:
|
||||||
|
- platform: internal_temperature
|
||||||
|
id: temp_sensor
|
||||||
|
|
||||||
|
- platform: packet_transport
|
||||||
|
provider: test_provider
|
||||||
|
remote_id: temp_sensor
|
||||||
|
id: remote_temp
|
||||||
|
|||||||
33
tests/components/json/common.yaml
Normal file
33
tests/components/json/common.yaml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
json:
|
||||||
|
|
||||||
|
interval:
|
||||||
|
- interval: 60s
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
// Test build_json
|
||||||
|
std::string json_str = esphome::json::build_json([](JsonObject root) {
|
||||||
|
root["sensor"] = "temperature";
|
||||||
|
root["value"] = 23.5;
|
||||||
|
root["unit"] = "°C";
|
||||||
|
});
|
||||||
|
ESP_LOGD("test", "Built JSON: %s", json_str.c_str());
|
||||||
|
|
||||||
|
// Test parse_json
|
||||||
|
bool parse_ok = esphome::json::parse_json(json_str, [](JsonObject root) {
|
||||||
|
if (root.containsKey("sensor") && root.containsKey("value")) {
|
||||||
|
const char* sensor = root["sensor"];
|
||||||
|
float value = root["value"];
|
||||||
|
ESP_LOGD("test", "Parsed: sensor=%s, value=%.1f", sensor, value);
|
||||||
|
} else {
|
||||||
|
ESP_LOGD("test", "Parsed JSON missing required keys");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ESP_LOGD("test", "Parse result (JSON syntax only): %s", parse_ok ? "success" : "failed");
|
||||||
|
|
||||||
|
// Test JsonBuilder class
|
||||||
|
esphome::json::JsonBuilder builder;
|
||||||
|
JsonObject obj = builder.root();
|
||||||
|
obj["test"] = "direct_builder";
|
||||||
|
obj["count"] = 42;
|
||||||
|
std::string result = builder.serialize();
|
||||||
|
ESP_LOGD("test", "JsonBuilder result: %s", result.c_str());
|
||||||
1
tests/components/json/test.esp32-idf.yaml
Normal file
1
tests/components/json/test.esp32-idf.yaml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<<: !include common.yaml
|
||||||
1
tests/components/json/test.esp8266-ard.yaml
Normal file
1
tests/components/json/test.esp8266-ard.yaml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<<: !include common.yaml
|
||||||
@@ -99,3 +99,77 @@ sensor:
|
|||||||
window_size: 10
|
window_size: 10
|
||||||
send_every: 10
|
send_every: 10
|
||||||
send_first_at: 1 # Send after first value
|
send_first_at: 1 # Send after first value
|
||||||
|
|
||||||
|
# ValueListFilter-based filters tests
|
||||||
|
# FilterOutValueFilter - single value
|
||||||
|
- platform: copy
|
||||||
|
source_id: source_sensor
|
||||||
|
name: "Filter Out Single Value"
|
||||||
|
filters:
|
||||||
|
- filter_out: 42.0 # Should filter out exactly 42.0
|
||||||
|
|
||||||
|
# FilterOutValueFilter - multiple values
|
||||||
|
- platform: copy
|
||||||
|
source_id: source_sensor
|
||||||
|
name: "Filter Out Multiple Values"
|
||||||
|
filters:
|
||||||
|
- filter_out: [0.0, 42.0, 100.0] # List of values to filter
|
||||||
|
|
||||||
|
# FilterOutValueFilter - with NaN
|
||||||
|
- platform: copy
|
||||||
|
source_id: source_sensor
|
||||||
|
name: "Filter Out NaN"
|
||||||
|
filters:
|
||||||
|
- filter_out: nan # Filter out NaN values
|
||||||
|
|
||||||
|
# FilterOutValueFilter - mixed values with NaN
|
||||||
|
- platform: copy
|
||||||
|
source_id: source_sensor
|
||||||
|
name: "Filter Out Mixed with NaN"
|
||||||
|
filters:
|
||||||
|
- filter_out: [nan, 0.0, 42.0]
|
||||||
|
|
||||||
|
# ThrottleWithPriorityFilter - single priority value
|
||||||
|
- platform: copy
|
||||||
|
source_id: source_sensor
|
||||||
|
name: "Throttle with Single Priority"
|
||||||
|
filters:
|
||||||
|
- throttle_with_priority:
|
||||||
|
timeout: 1000ms
|
||||||
|
value: 42.0 # Priority value bypasses throttle
|
||||||
|
|
||||||
|
# ThrottleWithPriorityFilter - multiple priority values
|
||||||
|
- platform: copy
|
||||||
|
source_id: source_sensor
|
||||||
|
name: "Throttle with Multiple Priorities"
|
||||||
|
filters:
|
||||||
|
- throttle_with_priority:
|
||||||
|
timeout: 500ms
|
||||||
|
value: [0.0, 42.0, 100.0] # Multiple priority values
|
||||||
|
|
||||||
|
# ThrottleWithPriorityFilter - with NaN priority
|
||||||
|
- platform: copy
|
||||||
|
source_id: source_sensor
|
||||||
|
name: "Throttle with NaN Priority"
|
||||||
|
filters:
|
||||||
|
- throttle_with_priority:
|
||||||
|
timeout: 1000ms
|
||||||
|
value: nan # NaN as priority value
|
||||||
|
|
||||||
|
# Combined filters - FilterOutValueFilter + other filters
|
||||||
|
- platform: copy
|
||||||
|
source_id: source_sensor
|
||||||
|
name: "Filter Out Then Throttle"
|
||||||
|
filters:
|
||||||
|
- filter_out: [0.0, 100.0]
|
||||||
|
- throttle: 500ms
|
||||||
|
|
||||||
|
# Combined filters - ThrottleWithPriorityFilter + other filters
|
||||||
|
- platform: copy
|
||||||
|
source_id: source_sensor
|
||||||
|
name: "Throttle Priority Then Scale"
|
||||||
|
filters:
|
||||||
|
- throttle_with_priority:
|
||||||
|
timeout: 1000ms
|
||||||
|
value: [42.0]
|
||||||
|
- multiply: 2.0
|
||||||
|
|||||||
66
tests/components/text_sensor/common.yaml
Normal file
66
tests/components/text_sensor/common.yaml
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
text_sensor:
|
||||||
|
- platform: template
|
||||||
|
name: "Test Substitute Single"
|
||||||
|
id: test_substitute_single
|
||||||
|
filters:
|
||||||
|
- substitute:
|
||||||
|
- ERROR -> Error
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "Test Substitute Multiple"
|
||||||
|
id: test_substitute_multiple
|
||||||
|
filters:
|
||||||
|
- substitute:
|
||||||
|
- ERROR -> Error
|
||||||
|
- WARN -> Warning
|
||||||
|
- INFO -> Information
|
||||||
|
- DEBUG -> Debug
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "Test Substitute Chained"
|
||||||
|
id: test_substitute_chained
|
||||||
|
filters:
|
||||||
|
- substitute:
|
||||||
|
- foo -> bar
|
||||||
|
- to_upper
|
||||||
|
- substitute:
|
||||||
|
- BAR -> baz
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "Test Map Single"
|
||||||
|
id: test_map_single
|
||||||
|
filters:
|
||||||
|
- map:
|
||||||
|
- ON -> Active
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "Test Map Multiple"
|
||||||
|
id: test_map_multiple
|
||||||
|
filters:
|
||||||
|
- map:
|
||||||
|
- ON -> Active
|
||||||
|
- OFF -> Inactive
|
||||||
|
- UNKNOWN -> Error
|
||||||
|
- IDLE -> Standby
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "Test Map Passthrough"
|
||||||
|
id: test_map_passthrough
|
||||||
|
filters:
|
||||||
|
- map:
|
||||||
|
- Good -> Excellent
|
||||||
|
- Bad -> Poor
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "Test All Filters"
|
||||||
|
id: test_all_filters
|
||||||
|
filters:
|
||||||
|
- to_upper
|
||||||
|
- to_lower
|
||||||
|
- append: " suffix"
|
||||||
|
- prepend: "prefix "
|
||||||
|
- substitute:
|
||||||
|
- prefix -> PREFIX
|
||||||
|
- suffix -> SUFFIX
|
||||||
|
- map:
|
||||||
|
- PREFIX text SUFFIX -> mapped
|
||||||
1
tests/components/text_sensor/test.esp8266-ard.yaml
Normal file
1
tests/components/text_sensor/test.esp8266-ard.yaml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<<: !include common.yaml
|
||||||
@@ -11,18 +11,17 @@ time:
|
|||||||
- 192.168.178.1
|
- 192.168.178.1
|
||||||
|
|
||||||
uponor_smatrix:
|
uponor_smatrix:
|
||||||
address: 0x110B
|
|
||||||
time_id: sntp_time
|
time_id: sntp_time
|
||||||
time_device_address: 0xDE13
|
time_device_address: 0x110BDE13
|
||||||
|
|
||||||
climate:
|
climate:
|
||||||
- platform: uponor_smatrix
|
- platform: uponor_smatrix
|
||||||
address: 0xDE13
|
address: 0x110BDE13
|
||||||
name: Thermostat Living Room
|
name: Thermostat Living Room
|
||||||
|
|
||||||
sensor:
|
sensor:
|
||||||
- platform: uponor_smatrix
|
- platform: uponor_smatrix
|
||||||
address: 0xDE13
|
address: 0x110BDE13
|
||||||
humidity:
|
humidity:
|
||||||
name: Thermostat Humidity Living Room
|
name: Thermostat Humidity Living Room
|
||||||
temperature:
|
temperature:
|
||||||
|
|||||||
332
tests/integration/fixtures/sensor_filters_value_list.yaml
Normal file
332
tests/integration/fixtures/sensor_filters_value_list.yaml
Normal file
@@ -0,0 +1,332 @@
|
|||||||
|
esphome:
|
||||||
|
name: test-value-list-filters
|
||||||
|
|
||||||
|
host:
|
||||||
|
api:
|
||||||
|
batch_delay: 0ms # Disable batching to receive all state updates
|
||||||
|
logger:
|
||||||
|
level: DEBUG
|
||||||
|
|
||||||
|
# Template sensors - one for each test to avoid cross-test interference
|
||||||
|
sensor:
|
||||||
|
- platform: template
|
||||||
|
name: "Source Sensor 1"
|
||||||
|
id: source_sensor_1
|
||||||
|
accuracy_decimals: 1
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "Source Sensor 2"
|
||||||
|
id: source_sensor_2
|
||||||
|
accuracy_decimals: 1
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "Source Sensor 3"
|
||||||
|
id: source_sensor_3
|
||||||
|
accuracy_decimals: 1
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "Source Sensor 4"
|
||||||
|
id: source_sensor_4
|
||||||
|
accuracy_decimals: 1
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "Source Sensor 5"
|
||||||
|
id: source_sensor_5
|
||||||
|
accuracy_decimals: 1
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "Source Sensor 6"
|
||||||
|
id: source_sensor_6
|
||||||
|
accuracy_decimals: 2
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "Source Sensor 7"
|
||||||
|
id: source_sensor_7
|
||||||
|
accuracy_decimals: 1
|
||||||
|
|
||||||
|
# FilterOutValueFilter - single value
|
||||||
|
- platform: copy
|
||||||
|
source_id: source_sensor_1
|
||||||
|
name: "Filter Out Single"
|
||||||
|
id: filter_out_single
|
||||||
|
filters:
|
||||||
|
- filter_out: 42.0
|
||||||
|
|
||||||
|
# FilterOutValueFilter - multiple values
|
||||||
|
- platform: copy
|
||||||
|
source_id: source_sensor_2
|
||||||
|
name: "Filter Out Multiple"
|
||||||
|
id: filter_out_multiple
|
||||||
|
filters:
|
||||||
|
- filter_out: [0.0, 42.0, 100.0]
|
||||||
|
|
||||||
|
# FilterOutValueFilter - with NaN
|
||||||
|
- platform: copy
|
||||||
|
source_id: source_sensor_1
|
||||||
|
name: "Filter Out NaN"
|
||||||
|
id: filter_out_nan
|
||||||
|
filters:
|
||||||
|
- filter_out: nan
|
||||||
|
|
||||||
|
# ThrottleWithPriorityFilter - single priority value
|
||||||
|
- platform: copy
|
||||||
|
source_id: source_sensor_3
|
||||||
|
name: "Throttle Priority Single"
|
||||||
|
id: throttle_priority_single
|
||||||
|
filters:
|
||||||
|
- throttle_with_priority:
|
||||||
|
timeout: 200ms
|
||||||
|
value: 42.0
|
||||||
|
|
||||||
|
# ThrottleWithPriorityFilter - multiple priority values
|
||||||
|
- platform: copy
|
||||||
|
source_id: source_sensor_4
|
||||||
|
name: "Throttle Priority Multiple"
|
||||||
|
id: throttle_priority_multiple
|
||||||
|
filters:
|
||||||
|
- throttle_with_priority:
|
||||||
|
timeout: 200ms
|
||||||
|
value: [0.0, 42.0, 100.0]
|
||||||
|
|
||||||
|
# Edge case: Filter Out NaN explicitly
|
||||||
|
- platform: copy
|
||||||
|
source_id: source_sensor_5
|
||||||
|
name: "Filter Out NaN Test"
|
||||||
|
id: filter_out_nan_test
|
||||||
|
filters:
|
||||||
|
- filter_out: nan
|
||||||
|
|
||||||
|
# Edge case: Accuracy decimals - 2 decimals
|
||||||
|
- platform: copy
|
||||||
|
source_id: source_sensor_6
|
||||||
|
name: "Filter Out Accuracy 2"
|
||||||
|
id: filter_out_accuracy_2
|
||||||
|
filters:
|
||||||
|
- filter_out: 42.0
|
||||||
|
|
||||||
|
# Edge case: Throttle with NaN priority
|
||||||
|
- platform: copy
|
||||||
|
source_id: source_sensor_7
|
||||||
|
name: "Throttle Priority NaN"
|
||||||
|
id: throttle_priority_nan
|
||||||
|
filters:
|
||||||
|
- throttle_with_priority:
|
||||||
|
timeout: 200ms
|
||||||
|
value: nan
|
||||||
|
|
||||||
|
# Script to test FilterOutValueFilter
|
||||||
|
script:
|
||||||
|
- id: test_filter_out_single
|
||||||
|
then:
|
||||||
|
# Should pass through: 1.0, 2.0, 3.0
|
||||||
|
# Should filter out: 42.0
|
||||||
|
- sensor.template.publish:
|
||||||
|
id: source_sensor_1
|
||||||
|
state: 1.0
|
||||||
|
- delay: 20ms
|
||||||
|
- sensor.template.publish:
|
||||||
|
id: source_sensor_1
|
||||||
|
state: 42.0 # Filtered out
|
||||||
|
- delay: 20ms
|
||||||
|
- sensor.template.publish:
|
||||||
|
id: source_sensor_1
|
||||||
|
state: 2.0
|
||||||
|
- delay: 20ms
|
||||||
|
- sensor.template.publish:
|
||||||
|
id: source_sensor_1
|
||||||
|
state: 42.0 # Filtered out
|
||||||
|
- delay: 20ms
|
||||||
|
- sensor.template.publish:
|
||||||
|
id: source_sensor_1
|
||||||
|
state: 3.0
|
||||||
|
|
||||||
|
- id: test_filter_out_multiple
|
||||||
|
then:
|
||||||
|
# Should filter out: 0.0, 42.0, 100.0
|
||||||
|
# Should pass through: 1.0, 2.0, 50.0
|
||||||
|
- sensor.template.publish:
|
||||||
|
id: source_sensor_2
|
||||||
|
state: 0.0 # Filtered out
|
||||||
|
- delay: 20ms
|
||||||
|
- sensor.template.publish:
|
||||||
|
id: source_sensor_2
|
||||||
|
state: 1.0
|
||||||
|
- delay: 20ms
|
||||||
|
- sensor.template.publish:
|
||||||
|
id: source_sensor_2
|
||||||
|
state: 42.0 # Filtered out
|
||||||
|
- delay: 20ms
|
||||||
|
- sensor.template.publish:
|
||||||
|
id: source_sensor_2
|
||||||
|
state: 2.0
|
||||||
|
- delay: 20ms
|
||||||
|
- sensor.template.publish:
|
||||||
|
id: source_sensor_2
|
||||||
|
state: 100.0 # Filtered out
|
||||||
|
- delay: 20ms
|
||||||
|
- sensor.template.publish:
|
||||||
|
id: source_sensor_2
|
||||||
|
state: 50.0
|
||||||
|
|
||||||
|
- id: test_throttle_priority_single
|
||||||
|
then:
|
||||||
|
# 42.0 bypasses throttle, other values are throttled
|
||||||
|
- sensor.template.publish:
|
||||||
|
id: source_sensor_3
|
||||||
|
state: 1.0 # First value - passes
|
||||||
|
- delay: 50ms
|
||||||
|
- sensor.template.publish:
|
||||||
|
id: source_sensor_3
|
||||||
|
state: 2.0 # Throttled
|
||||||
|
- delay: 50ms
|
||||||
|
- sensor.template.publish:
|
||||||
|
id: source_sensor_3
|
||||||
|
state: 42.0 # Priority - passes immediately
|
||||||
|
- delay: 50ms
|
||||||
|
- sensor.template.publish:
|
||||||
|
id: source_sensor_3
|
||||||
|
state: 3.0 # Throttled
|
||||||
|
- delay: 250ms # Wait for throttle to expire
|
||||||
|
- sensor.template.publish:
|
||||||
|
id: source_sensor_3
|
||||||
|
state: 4.0 # Passes after timeout
|
||||||
|
|
||||||
|
- id: test_throttle_priority_multiple
|
||||||
|
then:
|
||||||
|
# 0.0, 42.0, 100.0 bypass throttle
|
||||||
|
- sensor.template.publish:
|
||||||
|
id: source_sensor_4
|
||||||
|
state: 1.0 # First value - passes
|
||||||
|
- delay: 50ms
|
||||||
|
- sensor.template.publish:
|
||||||
|
id: source_sensor_4
|
||||||
|
state: 2.0 # Throttled
|
||||||
|
- delay: 50ms
|
||||||
|
- sensor.template.publish:
|
||||||
|
id: source_sensor_4
|
||||||
|
state: 0.0 # Priority - passes
|
||||||
|
- delay: 50ms
|
||||||
|
- sensor.template.publish:
|
||||||
|
id: source_sensor_4
|
||||||
|
state: 3.0 # Throttled
|
||||||
|
- delay: 50ms
|
||||||
|
- sensor.template.publish:
|
||||||
|
id: source_sensor_4
|
||||||
|
state: 42.0 # Priority - passes
|
||||||
|
- delay: 50ms
|
||||||
|
- sensor.template.publish:
|
||||||
|
id: source_sensor_4
|
||||||
|
state: 4.0 # Throttled
|
||||||
|
- delay: 50ms
|
||||||
|
- sensor.template.publish:
|
||||||
|
id: source_sensor_4
|
||||||
|
state: 100.0 # Priority - passes
|
||||||
|
|
||||||
|
- id: test_filter_out_nan
|
||||||
|
then:
|
||||||
|
# NaN should be filtered out, regular values pass
|
||||||
|
- sensor.template.publish:
|
||||||
|
id: source_sensor_5
|
||||||
|
state: 1.0 # Pass
|
||||||
|
- delay: 20ms
|
||||||
|
- sensor.template.publish:
|
||||||
|
id: source_sensor_5
|
||||||
|
state: !lambda 'return NAN;' # Filtered out
|
||||||
|
- delay: 20ms
|
||||||
|
- sensor.template.publish:
|
||||||
|
id: source_sensor_5
|
||||||
|
state: 2.0 # Pass
|
||||||
|
- delay: 20ms
|
||||||
|
- sensor.template.publish:
|
||||||
|
id: source_sensor_5
|
||||||
|
state: !lambda 'return NAN;' # Filtered out
|
||||||
|
- delay: 20ms
|
||||||
|
- sensor.template.publish:
|
||||||
|
id: source_sensor_5
|
||||||
|
state: 3.0 # Pass
|
||||||
|
|
||||||
|
- id: test_filter_out_accuracy_2
|
||||||
|
then:
|
||||||
|
# With 2 decimal places, 42.00 filtered, 42.01 and 42.15 pass
|
||||||
|
- sensor.template.publish:
|
||||||
|
id: source_sensor_6
|
||||||
|
state: 42.0 # Filtered (rounds to 42.00)
|
||||||
|
- delay: 20ms
|
||||||
|
- sensor.template.publish:
|
||||||
|
id: source_sensor_6
|
||||||
|
state: 42.01 # Pass (rounds to 42.01)
|
||||||
|
- delay: 20ms
|
||||||
|
- sensor.template.publish:
|
||||||
|
id: source_sensor_6
|
||||||
|
state: 42.15 # Pass (rounds to 42.15)
|
||||||
|
- delay: 20ms
|
||||||
|
- sensor.template.publish:
|
||||||
|
id: source_sensor_6
|
||||||
|
state: 42.0 # Filtered (rounds to 42.00)
|
||||||
|
|
||||||
|
- id: test_throttle_priority_nan
|
||||||
|
then:
|
||||||
|
# NaN bypasses throttle, regular values throttled
|
||||||
|
- sensor.template.publish:
|
||||||
|
id: source_sensor_7
|
||||||
|
state: 1.0 # First value - passes
|
||||||
|
- delay: 50ms
|
||||||
|
- sensor.template.publish:
|
||||||
|
id: source_sensor_7
|
||||||
|
state: 2.0 # Throttled
|
||||||
|
- delay: 50ms
|
||||||
|
- sensor.template.publish:
|
||||||
|
id: source_sensor_7
|
||||||
|
state: !lambda 'return NAN;' # Priority NaN - passes
|
||||||
|
- delay: 50ms
|
||||||
|
- sensor.template.publish:
|
||||||
|
id: source_sensor_7
|
||||||
|
state: 3.0 # Throttled
|
||||||
|
- delay: 50ms
|
||||||
|
- sensor.template.publish:
|
||||||
|
id: source_sensor_7
|
||||||
|
state: !lambda 'return NAN;' # Priority NaN - passes
|
||||||
|
|
||||||
|
# Buttons to trigger each test
|
||||||
|
button:
|
||||||
|
- platform: template
|
||||||
|
name: "Test Filter Out Single"
|
||||||
|
id: btn_filter_out_single
|
||||||
|
on_press:
|
||||||
|
- script.execute: test_filter_out_single
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "Test Filter Out Multiple"
|
||||||
|
id: btn_filter_out_multiple
|
||||||
|
on_press:
|
||||||
|
- script.execute: test_filter_out_multiple
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "Test Throttle Priority Single"
|
||||||
|
id: btn_throttle_priority_single
|
||||||
|
on_press:
|
||||||
|
- script.execute: test_throttle_priority_single
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "Test Throttle Priority Multiple"
|
||||||
|
id: btn_throttle_priority_multiple
|
||||||
|
on_press:
|
||||||
|
- script.execute: test_throttle_priority_multiple
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "Test Filter Out NaN"
|
||||||
|
id: btn_filter_out_nan
|
||||||
|
on_press:
|
||||||
|
- script.execute: test_filter_out_nan
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "Test Filter Out Accuracy 2"
|
||||||
|
id: btn_filter_out_accuracy_2
|
||||||
|
on_press:
|
||||||
|
- script.execute: test_filter_out_accuracy_2
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "Test Throttle Priority NaN"
|
||||||
|
id: btn_throttle_priority_nan
|
||||||
|
on_press:
|
||||||
|
- script.execute: test_throttle_priority_nan
|
||||||
@@ -281,8 +281,12 @@ async def test_noise_corrupt_encrypted_frame(
|
|||||||
# Check for signs that the process exited/crashed
|
# Check for signs that the process exited/crashed
|
||||||
if "Segmentation fault" in line or "core dumped" in line:
|
if "Segmentation fault" in line or "core dumped" in line:
|
||||||
process_exited = True
|
process_exited = True
|
||||||
# Check for the expected warning about decryption failure
|
# Check for the expected log about decryption failure
|
||||||
|
# This can appear as either a VV-level log from noise or a W-level log from connection
|
||||||
if (
|
if (
|
||||||
|
"[VV][api.noise" in line
|
||||||
|
and "noise_cipherstate_decrypt failed: MAC_FAILURE" in line
|
||||||
|
) or (
|
||||||
"[W][api.connection" in line
|
"[W][api.connection" in line
|
||||||
and "Reading failed CIPHERSTATE_DECRYPT_FAILED" in line
|
and "Reading failed CIPHERSTATE_DECRYPT_FAILED" in line
|
||||||
):
|
):
|
||||||
@@ -322,9 +326,9 @@ async def test_noise_corrupt_encrypted_frame(
|
|||||||
assert not process_exited, (
|
assert not process_exited, (
|
||||||
"ESPHome process should not crash on corrupt encrypted frames"
|
"ESPHome process should not crash on corrupt encrypted frames"
|
||||||
)
|
)
|
||||||
# Verify we saw the expected warning message
|
# Verify we saw the expected log message about decryption failure
|
||||||
assert cipherstate_failed, (
|
assert cipherstate_failed, (
|
||||||
"Expected to see warning about CIPHERSTATE_DECRYPT_FAILED"
|
"Expected to see log about noise_cipherstate_decrypt failure or CIPHERSTATE_DECRYPT_FAILED"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Verify we can still reconnect after handling the corrupt frame
|
# Verify we can still reconnect after handling the corrupt frame
|
||||||
|
|||||||
263
tests/integration/test_sensor_filters_value_list.py
Normal file
263
tests/integration/test_sensor_filters_value_list.py
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
"""Test sensor ValueListFilter functionality (FilterOutValueFilter and ThrottleWithPriorityFilter)."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import math
|
||||||
|
|
||||||
|
from aioesphomeapi import ButtonInfo, EntityState, SensorState
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from .state_utils import InitialStateHelper, build_key_to_entity_mapping
|
||||||
|
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_sensor_filters_value_list(
|
||||||
|
yaml_config: str,
|
||||||
|
run_compiled: RunCompiledFunction,
|
||||||
|
api_client_connected: APIClientConnectedFactory,
|
||||||
|
) -> None:
|
||||||
|
"""Test that ValueListFilter-based filters work correctly."""
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
|
||||||
|
# Track state changes for all sensors
|
||||||
|
sensor_values: dict[str, list[float]] = {
|
||||||
|
"filter_out_single": [],
|
||||||
|
"filter_out_multiple": [],
|
||||||
|
"throttle_priority_single": [],
|
||||||
|
"throttle_priority_multiple": [],
|
||||||
|
"filter_out_nan_test": [],
|
||||||
|
"filter_out_accuracy_2": [],
|
||||||
|
"throttle_priority_nan": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Futures for each test
|
||||||
|
filter_out_single_done = loop.create_future()
|
||||||
|
filter_out_multiple_done = loop.create_future()
|
||||||
|
throttle_single_done = loop.create_future()
|
||||||
|
throttle_multiple_done = loop.create_future()
|
||||||
|
filter_out_nan_done = loop.create_future()
|
||||||
|
filter_out_accuracy_2_done = loop.create_future()
|
||||||
|
throttle_nan_done = loop.create_future()
|
||||||
|
|
||||||
|
def on_state(state: EntityState) -> None:
|
||||||
|
"""Track sensor state updates."""
|
||||||
|
if not isinstance(state, SensorState) or state.missing_state:
|
||||||
|
return
|
||||||
|
|
||||||
|
sensor_name = key_to_sensor.get(state.key)
|
||||||
|
if sensor_name not in sensor_values:
|
||||||
|
return
|
||||||
|
|
||||||
|
sensor_values[sensor_name].append(state.state)
|
||||||
|
|
||||||
|
# Check completion conditions
|
||||||
|
if (
|
||||||
|
sensor_name == "filter_out_single"
|
||||||
|
and len(sensor_values[sensor_name]) == 3
|
||||||
|
and not filter_out_single_done.done()
|
||||||
|
):
|
||||||
|
filter_out_single_done.set_result(True)
|
||||||
|
elif (
|
||||||
|
sensor_name == "filter_out_multiple"
|
||||||
|
and len(sensor_values[sensor_name]) == 3
|
||||||
|
and not filter_out_multiple_done.done()
|
||||||
|
):
|
||||||
|
filter_out_multiple_done.set_result(True)
|
||||||
|
elif (
|
||||||
|
sensor_name == "throttle_priority_single"
|
||||||
|
and len(sensor_values[sensor_name]) == 3
|
||||||
|
and not throttle_single_done.done()
|
||||||
|
):
|
||||||
|
throttle_single_done.set_result(True)
|
||||||
|
elif (
|
||||||
|
sensor_name == "throttle_priority_multiple"
|
||||||
|
and len(sensor_values[sensor_name]) == 4
|
||||||
|
and not throttle_multiple_done.done()
|
||||||
|
):
|
||||||
|
throttle_multiple_done.set_result(True)
|
||||||
|
elif (
|
||||||
|
sensor_name == "filter_out_nan_test"
|
||||||
|
and len(sensor_values[sensor_name]) == 3
|
||||||
|
and not filter_out_nan_done.done()
|
||||||
|
):
|
||||||
|
filter_out_nan_done.set_result(True)
|
||||||
|
elif (
|
||||||
|
sensor_name == "filter_out_accuracy_2"
|
||||||
|
and len(sensor_values[sensor_name]) == 2
|
||||||
|
and not filter_out_accuracy_2_done.done()
|
||||||
|
):
|
||||||
|
filter_out_accuracy_2_done.set_result(True)
|
||||||
|
elif (
|
||||||
|
sensor_name == "throttle_priority_nan"
|
||||||
|
and len(sensor_values[sensor_name]) == 3
|
||||||
|
and not throttle_nan_done.done()
|
||||||
|
):
|
||||||
|
throttle_nan_done.set_result(True)
|
||||||
|
|
||||||
|
async with (
|
||||||
|
run_compiled(yaml_config),
|
||||||
|
api_client_connected() as client,
|
||||||
|
):
|
||||||
|
# Get entities and build key mapping
|
||||||
|
entities, _ = await client.list_entities_services()
|
||||||
|
key_to_sensor = build_key_to_entity_mapping(
|
||||||
|
entities,
|
||||||
|
{
|
||||||
|
"filter_out_single": "Filter Out Single",
|
||||||
|
"filter_out_multiple": "Filter Out Multiple",
|
||||||
|
"throttle_priority_single": "Throttle Priority Single",
|
||||||
|
"throttle_priority_multiple": "Throttle Priority Multiple",
|
||||||
|
"filter_out_nan_test": "Filter Out NaN Test",
|
||||||
|
"filter_out_accuracy_2": "Filter Out Accuracy 2",
|
||||||
|
"throttle_priority_nan": "Throttle Priority NaN",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set up initial state helper with all entities
|
||||||
|
initial_state_helper = InitialStateHelper(entities)
|
||||||
|
|
||||||
|
# Subscribe to state changes with wrapper
|
||||||
|
client.subscribe_states(initial_state_helper.on_state_wrapper(on_state))
|
||||||
|
|
||||||
|
# Wait for initial states
|
||||||
|
await initial_state_helper.wait_for_initial_states()
|
||||||
|
|
||||||
|
# Find all buttons
|
||||||
|
button_name_map = {
|
||||||
|
"Test Filter Out Single": "filter_out_single",
|
||||||
|
"Test Filter Out Multiple": "filter_out_multiple",
|
||||||
|
"Test Throttle Priority Single": "throttle_priority_single",
|
||||||
|
"Test Throttle Priority Multiple": "throttle_priority_multiple",
|
||||||
|
"Test Filter Out NaN": "filter_out_nan",
|
||||||
|
"Test Filter Out Accuracy 2": "filter_out_accuracy_2",
|
||||||
|
"Test Throttle Priority NaN": "throttle_priority_nan",
|
||||||
|
}
|
||||||
|
buttons = {}
|
||||||
|
for entity in entities:
|
||||||
|
if isinstance(entity, ButtonInfo) and entity.name in button_name_map:
|
||||||
|
buttons[button_name_map[entity.name]] = entity.key
|
||||||
|
|
||||||
|
assert len(buttons) == 7, f"Expected 7 buttons, found {len(buttons)}"
|
||||||
|
|
||||||
|
# Test 1: FilterOutValueFilter - single value
|
||||||
|
sensor_values["filter_out_single"].clear()
|
||||||
|
client.button_command(buttons["filter_out_single"])
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(filter_out_single_done, timeout=2.0)
|
||||||
|
except TimeoutError:
|
||||||
|
pytest.fail(
|
||||||
|
f"Test 1 timed out. Values: {sensor_values['filter_out_single']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
expected = [1.0, 2.0, 3.0]
|
||||||
|
assert sensor_values["filter_out_single"] == pytest.approx(expected), (
|
||||||
|
f"Test 1 failed: expected {expected}, got {sensor_values['filter_out_single']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test 2: FilterOutValueFilter - multiple values
|
||||||
|
sensor_values["filter_out_multiple"].clear()
|
||||||
|
filter_out_multiple_done = loop.create_future()
|
||||||
|
client.button_command(buttons["filter_out_multiple"])
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(filter_out_multiple_done, timeout=2.0)
|
||||||
|
except TimeoutError:
|
||||||
|
pytest.fail(
|
||||||
|
f"Test 2 timed out. Values: {sensor_values['filter_out_multiple']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
expected = [1.0, 2.0, 50.0]
|
||||||
|
assert sensor_values["filter_out_multiple"] == pytest.approx(expected), (
|
||||||
|
f"Test 2 failed: expected {expected}, got {sensor_values['filter_out_multiple']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test 3: ThrottleWithPriorityFilter - single priority
|
||||||
|
sensor_values["throttle_priority_single"].clear()
|
||||||
|
throttle_single_done = loop.create_future()
|
||||||
|
client.button_command(buttons["throttle_priority_single"])
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(throttle_single_done, timeout=2.0)
|
||||||
|
except TimeoutError:
|
||||||
|
pytest.fail(
|
||||||
|
f"Test 3 timed out. Values: {sensor_values['throttle_priority_single']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
expected = [1.0, 42.0, 4.0]
|
||||||
|
assert sensor_values["throttle_priority_single"] == pytest.approx(expected), (
|
||||||
|
f"Test 3 failed: expected {expected}, got {sensor_values['throttle_priority_single']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test 4: ThrottleWithPriorityFilter - multiple priorities
|
||||||
|
sensor_values["throttle_priority_multiple"].clear()
|
||||||
|
throttle_multiple_done = loop.create_future()
|
||||||
|
client.button_command(buttons["throttle_priority_multiple"])
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(throttle_multiple_done, timeout=2.0)
|
||||||
|
except TimeoutError:
|
||||||
|
pytest.fail(
|
||||||
|
f"Test 4 timed out. Values: {sensor_values['throttle_priority_multiple']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
expected = [1.0, 0.0, 42.0, 100.0]
|
||||||
|
assert sensor_values["throttle_priority_multiple"] == pytest.approx(expected), (
|
||||||
|
f"Test 4 failed: expected {expected}, got {sensor_values['throttle_priority_multiple']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test 5: FilterOutValueFilter - NaN handling
|
||||||
|
sensor_values["filter_out_nan_test"].clear()
|
||||||
|
filter_out_nan_done = loop.create_future()
|
||||||
|
client.button_command(buttons["filter_out_nan"])
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(filter_out_nan_done, timeout=2.0)
|
||||||
|
except TimeoutError:
|
||||||
|
pytest.fail(
|
||||||
|
f"Test 5 timed out. Values: {sensor_values['filter_out_nan_test']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
expected = [1.0, 2.0, 3.0]
|
||||||
|
assert sensor_values["filter_out_nan_test"] == pytest.approx(expected), (
|
||||||
|
f"Test 5 failed: expected {expected}, got {sensor_values['filter_out_nan_test']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test 6: FilterOutValueFilter - Accuracy decimals (2)
|
||||||
|
sensor_values["filter_out_accuracy_2"].clear()
|
||||||
|
filter_out_accuracy_2_done = loop.create_future()
|
||||||
|
client.button_command(buttons["filter_out_accuracy_2"])
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(filter_out_accuracy_2_done, timeout=2.0)
|
||||||
|
except TimeoutError:
|
||||||
|
pytest.fail(
|
||||||
|
f"Test 6 timed out. Values: {sensor_values['filter_out_accuracy_2']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
expected = [42.01, 42.15]
|
||||||
|
assert sensor_values["filter_out_accuracy_2"] == pytest.approx(expected), (
|
||||||
|
f"Test 6 failed: expected {expected}, got {sensor_values['filter_out_accuracy_2']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test 7: ThrottleWithPriorityFilter - NaN priority
|
||||||
|
sensor_values["throttle_priority_nan"].clear()
|
||||||
|
throttle_nan_done = loop.create_future()
|
||||||
|
client.button_command(buttons["throttle_priority_nan"])
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(throttle_nan_done, timeout=2.0)
|
||||||
|
except TimeoutError:
|
||||||
|
pytest.fail(
|
||||||
|
f"Test 7 timed out. Values: {sensor_values['throttle_priority_nan']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# First value (1.0) + two NaN priority values
|
||||||
|
# NaN values will be compared using math.isnan
|
||||||
|
assert len(sensor_values["throttle_priority_nan"]) == 3, (
|
||||||
|
f"Test 7 failed: expected 3 values, got {len(sensor_values['throttle_priority_nan'])}"
|
||||||
|
)
|
||||||
|
assert sensor_values["throttle_priority_nan"][0] == pytest.approx(1.0), (
|
||||||
|
f"Test 7 failed: first value should be 1.0, got {sensor_values['throttle_priority_nan'][0]}"
|
||||||
|
)
|
||||||
|
assert math.isnan(sensor_values["throttle_priority_nan"][1]), (
|
||||||
|
f"Test 7 failed: second value should be NaN, got {sensor_values['throttle_priority_nan'][1]}"
|
||||||
|
)
|
||||||
|
assert math.isnan(sensor_values["throttle_priority_nan"][2]), (
|
||||||
|
f"Test 7 failed: third value should be NaN, got {sensor_values['throttle_priority_nan'][2]}"
|
||||||
|
)
|
||||||
@@ -107,6 +107,7 @@ def test_main_all_tests_should_run(
|
|||||||
|
|
||||||
assert output["integration_tests"] is True
|
assert output["integration_tests"] is True
|
||||||
assert output["clang_tidy"] is True
|
assert output["clang_tidy"] is True
|
||||||
|
assert output["clang_tidy_mode"] in ["nosplit", "split"]
|
||||||
assert output["clang_format"] is True
|
assert output["clang_format"] is True
|
||||||
assert output["python_linters"] is True
|
assert output["python_linters"] is True
|
||||||
assert output["changed_components"] == ["wifi", "api", "sensor"]
|
assert output["changed_components"] == ["wifi", "api", "sensor"]
|
||||||
@@ -117,6 +118,9 @@ def test_main_all_tests_should_run(
|
|||||||
assert output["component_test_count"] == len(
|
assert output["component_test_count"] == len(
|
||||||
output["changed_components_with_tests"]
|
output["changed_components_with_tests"]
|
||||||
)
|
)
|
||||||
|
# changed_cpp_file_count should be present
|
||||||
|
assert "changed_cpp_file_count" in output
|
||||||
|
assert isinstance(output["changed_cpp_file_count"], int)
|
||||||
# memory_impact should be present
|
# memory_impact should be present
|
||||||
assert "memory_impact" in output
|
assert "memory_impact" in output
|
||||||
assert output["memory_impact"]["should_run"] == "false" # No files changed
|
assert output["memory_impact"]["should_run"] == "false" # No files changed
|
||||||
@@ -156,11 +160,14 @@ def test_main_no_tests_should_run(
|
|||||||
|
|
||||||
assert output["integration_tests"] is False
|
assert output["integration_tests"] is False
|
||||||
assert output["clang_tidy"] is False
|
assert output["clang_tidy"] is False
|
||||||
|
assert output["clang_tidy_mode"] == "disabled"
|
||||||
assert output["clang_format"] is False
|
assert output["clang_format"] is False
|
||||||
assert output["python_linters"] is False
|
assert output["python_linters"] is False
|
||||||
assert output["changed_components"] == []
|
assert output["changed_components"] == []
|
||||||
assert output["changed_components_with_tests"] == []
|
assert output["changed_components_with_tests"] == []
|
||||||
assert output["component_test_count"] == 0
|
assert output["component_test_count"] == 0
|
||||||
|
# changed_cpp_file_count should be 0
|
||||||
|
assert output["changed_cpp_file_count"] == 0
|
||||||
# memory_impact should be present
|
# memory_impact should be present
|
||||||
assert "memory_impact" in output
|
assert "memory_impact" in output
|
||||||
assert output["memory_impact"]["should_run"] == "false"
|
assert output["memory_impact"]["should_run"] == "false"
|
||||||
@@ -239,6 +246,7 @@ def test_main_with_branch_argument(
|
|||||||
|
|
||||||
assert output["integration_tests"] is False
|
assert output["integration_tests"] is False
|
||||||
assert output["clang_tidy"] is True
|
assert output["clang_tidy"] is True
|
||||||
|
assert output["clang_tidy_mode"] in ["nosplit", "split"]
|
||||||
assert output["clang_format"] is False
|
assert output["clang_format"] is False
|
||||||
assert output["python_linters"] is True
|
assert output["python_linters"] is True
|
||||||
assert output["changed_components"] == ["mqtt"]
|
assert output["changed_components"] == ["mqtt"]
|
||||||
@@ -249,6 +257,9 @@ def test_main_with_branch_argument(
|
|||||||
assert output["component_test_count"] == len(
|
assert output["component_test_count"] == len(
|
||||||
output["changed_components_with_tests"]
|
output["changed_components_with_tests"]
|
||||||
)
|
)
|
||||||
|
# changed_cpp_file_count should be present
|
||||||
|
assert "changed_cpp_file_count" in output
|
||||||
|
assert isinstance(output["changed_cpp_file_count"], int)
|
||||||
# memory_impact should be present
|
# memory_impact should be present
|
||||||
assert "memory_impact" in output
|
assert "memory_impact" in output
|
||||||
assert output["memory_impact"]["should_run"] == "false"
|
assert output["memory_impact"]["should_run"] == "false"
|
||||||
@@ -433,6 +444,40 @@ def test_should_run_clang_format_with_branch() -> None:
|
|||||||
mock_changed.assert_called_once_with("release")
|
mock_changed.assert_called_once_with("release")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("changed_files", "expected_count"),
|
||||||
|
[
|
||||||
|
(["esphome/core.cpp"], 1),
|
||||||
|
(["esphome/core.h"], 1),
|
||||||
|
(["test.hpp"], 1),
|
||||||
|
(["test.cc"], 1),
|
||||||
|
(["test.cxx"], 1),
|
||||||
|
(["test.c"], 1),
|
||||||
|
(["test.tcc"], 1),
|
||||||
|
(["esphome/core.cpp", "esphome/core.h"], 2),
|
||||||
|
(["esphome/core.cpp", "esphome/core.h", "test.cc"], 3),
|
||||||
|
(["README.md"], 0),
|
||||||
|
(["esphome/config.py"], 0),
|
||||||
|
(["README.md", "esphome/config.py"], 0),
|
||||||
|
(["esphome/core.cpp", "README.md", "esphome/config.py"], 1),
|
||||||
|
([], 0),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_count_changed_cpp_files(changed_files: list[str], expected_count: int) -> None:
|
||||||
|
"""Test count_changed_cpp_files function."""
|
||||||
|
with patch.object(determine_jobs, "changed_files", return_value=changed_files):
|
||||||
|
result = determine_jobs.count_changed_cpp_files()
|
||||||
|
assert result == expected_count
|
||||||
|
|
||||||
|
|
||||||
|
def test_count_changed_cpp_files_with_branch() -> None:
|
||||||
|
"""Test count_changed_cpp_files with branch argument."""
|
||||||
|
with patch.object(determine_jobs, "changed_files") as mock_changed:
|
||||||
|
mock_changed.return_value = []
|
||||||
|
determine_jobs.count_changed_cpp_files("release")
|
||||||
|
mock_changed.assert_called_once_with("release")
|
||||||
|
|
||||||
|
|
||||||
def test_main_filters_components_without_tests(
|
def test_main_filters_components_without_tests(
|
||||||
mock_should_run_integration_tests: Mock,
|
mock_should_run_integration_tests: Mock,
|
||||||
mock_should_run_clang_tidy: Mock,
|
mock_should_run_clang_tidy: Mock,
|
||||||
@@ -501,6 +546,9 @@ def test_main_filters_components_without_tests(
|
|||||||
assert set(output["changed_components_with_tests"]) == {"wifi", "sensor"}
|
assert set(output["changed_components_with_tests"]) == {"wifi", "sensor"}
|
||||||
# component_test_count should be based on components with tests
|
# component_test_count should be based on components with tests
|
||||||
assert output["component_test_count"] == 2
|
assert output["component_test_count"] == 2
|
||||||
|
# changed_cpp_file_count should be present
|
||||||
|
assert "changed_cpp_file_count" in output
|
||||||
|
assert isinstance(output["changed_cpp_file_count"], int)
|
||||||
# memory_impact should be present
|
# memory_impact should be present
|
||||||
assert "memory_impact" in output
|
assert "memory_impact" in output
|
||||||
assert output["memory_impact"]["should_run"] == "false"
|
assert output["memory_impact"]["should_run"] == "false"
|
||||||
@@ -545,7 +593,7 @@ def test_detect_memory_impact_config_with_common_platform(tmp_path: Path) -> Non
|
|||||||
|
|
||||||
|
|
||||||
def test_detect_memory_impact_config_core_only_changes(tmp_path: Path) -> None:
|
def test_detect_memory_impact_config_core_only_changes(tmp_path: Path) -> None:
|
||||||
"""Test memory impact detection with core-only changes (no component changes)."""
|
"""Test memory impact detection with core C++ changes (no component changes)."""
|
||||||
# Create test directory structure with fallback component
|
# Create test directory structure with fallback component
|
||||||
tests_dir = tmp_path / "tests" / "components"
|
tests_dir = tmp_path / "tests" / "components"
|
||||||
|
|
||||||
@@ -554,7 +602,7 @@ def test_detect_memory_impact_config_core_only_changes(tmp_path: Path) -> None:
|
|||||||
api_dir.mkdir(parents=True)
|
api_dir.mkdir(parents=True)
|
||||||
(api_dir / "test.esp32-idf.yaml").write_text("test: api")
|
(api_dir / "test.esp32-idf.yaml").write_text("test: api")
|
||||||
|
|
||||||
# Mock changed_files to return only core files (no component files)
|
# Mock changed_files to return only core C++ files (no component files)
|
||||||
with (
|
with (
|
||||||
patch.object(determine_jobs, "root_path", str(tmp_path)),
|
patch.object(determine_jobs, "root_path", str(tmp_path)),
|
||||||
patch.object(helpers, "root_path", str(tmp_path)),
|
patch.object(helpers, "root_path", str(tmp_path)),
|
||||||
@@ -574,6 +622,35 @@ def test_detect_memory_impact_config_core_only_changes(tmp_path: Path) -> None:
|
|||||||
assert result["use_merged_config"] == "true"
|
assert result["use_merged_config"] == "true"
|
||||||
|
|
||||||
|
|
||||||
|
def test_detect_memory_impact_config_core_python_only_changes(tmp_path: Path) -> None:
|
||||||
|
"""Test that Python-only core changes don't trigger memory impact analysis."""
|
||||||
|
# Create test directory structure with fallback component
|
||||||
|
tests_dir = tmp_path / "tests" / "components"
|
||||||
|
|
||||||
|
# api component (fallback component) with esp32-idf test
|
||||||
|
api_dir = tests_dir / "api"
|
||||||
|
api_dir.mkdir(parents=True)
|
||||||
|
(api_dir / "test.esp32-idf.yaml").write_text("test: api")
|
||||||
|
|
||||||
|
# Mock changed_files to return only core Python files (no C++ files)
|
||||||
|
with (
|
||||||
|
patch.object(determine_jobs, "root_path", str(tmp_path)),
|
||||||
|
patch.object(helpers, "root_path", str(tmp_path)),
|
||||||
|
patch.object(determine_jobs, "changed_files") as mock_changed_files,
|
||||||
|
):
|
||||||
|
mock_changed_files.return_value = [
|
||||||
|
"esphome/__main__.py",
|
||||||
|
"esphome/config.py",
|
||||||
|
"esphome/core/config.py",
|
||||||
|
]
|
||||||
|
determine_jobs._component_has_tests.cache_clear()
|
||||||
|
|
||||||
|
result = determine_jobs.detect_memory_impact_config()
|
||||||
|
|
||||||
|
# Python-only changes should NOT trigger memory impact analysis
|
||||||
|
assert result["should_run"] == "false"
|
||||||
|
|
||||||
|
|
||||||
def test_detect_memory_impact_config_no_common_platform(tmp_path: Path) -> None:
|
def test_detect_memory_impact_config_no_common_platform(tmp_path: Path) -> None:
|
||||||
"""Test memory impact detection when components have no common platform."""
|
"""Test memory impact detection when components have no common platform."""
|
||||||
# Create test directory structure
|
# Create test directory structure
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ substitutions:
|
|||||||
area: 25
|
area: 25
|
||||||
numberOne: 1
|
numberOne: 1
|
||||||
var1: 79
|
var1: 79
|
||||||
|
double_width: 14
|
||||||
test_list:
|
test_list:
|
||||||
- The area is 56
|
- The area is 56
|
||||||
- 56
|
- 56
|
||||||
@@ -25,3 +26,4 @@ test_list:
|
|||||||
- ord("a") = 97
|
- ord("a") = 97
|
||||||
- chr(97) = a
|
- chr(97) = a
|
||||||
- len([1,2,3]) = 3
|
- len([1,2,3]) = 3
|
||||||
|
- width = 7, double_width = 14
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ substitutions:
|
|||||||
area: 25
|
area: 25
|
||||||
numberOne: 1
|
numberOne: 1
|
||||||
var1: 79
|
var1: 79
|
||||||
|
double_width: ${width * 2}
|
||||||
|
|
||||||
test_list:
|
test_list:
|
||||||
- "The area is ${width * height}"
|
- "The area is ${width * height}"
|
||||||
@@ -23,3 +24,4 @@ test_list:
|
|||||||
- ord("a") = ${ ord("a") }
|
- ord("a") = ${ ord("a") }
|
||||||
- chr(97) = ${ chr(97) }
|
- chr(97) = ${ chr(97) }
|
||||||
- len([1,2,3]) = ${ len([1,2,3]) }
|
- len([1,2,3]) = ${ len([1,2,3]) }
|
||||||
|
- width = ${width}, double_width = ${double_width}
|
||||||
|
|||||||
@@ -17,10 +17,12 @@ from esphome import platformio_api
|
|||||||
from esphome.__main__ import (
|
from esphome.__main__ import (
|
||||||
Purpose,
|
Purpose,
|
||||||
choose_upload_log_host,
|
choose_upload_log_host,
|
||||||
|
command_analyze_memory,
|
||||||
command_clean_all,
|
command_clean_all,
|
||||||
command_rename,
|
command_rename,
|
||||||
command_update_all,
|
command_update_all,
|
||||||
command_wizard,
|
command_wizard,
|
||||||
|
detect_external_components,
|
||||||
get_port_type,
|
get_port_type,
|
||||||
has_ip_address,
|
has_ip_address,
|
||||||
has_mqtt,
|
has_mqtt,
|
||||||
@@ -226,13 +228,47 @@ def mock_run_external_process() -> Generator[Mock]:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_run_external_command() -> Generator[Mock]:
|
def mock_run_external_command_main() -> Generator[Mock]:
|
||||||
"""Mock run_external_command for testing."""
|
"""Mock run_external_command in __main__ module (different from platformio_api)."""
|
||||||
with patch("esphome.__main__.run_external_command") as mock:
|
with patch("esphome.__main__.run_external_command") as mock:
|
||||||
mock.return_value = 0 # Default to success
|
mock.return_value = 0 # Default to success
|
||||||
yield mock
|
yield mock
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_write_cpp() -> Generator[Mock]:
|
||||||
|
"""Mock write_cpp for testing."""
|
||||||
|
with patch("esphome.__main__.write_cpp") as mock:
|
||||||
|
mock.return_value = 0 # Default to success
|
||||||
|
yield mock
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_compile_program() -> Generator[Mock]:
|
||||||
|
"""Mock compile_program for testing."""
|
||||||
|
with patch("esphome.__main__.compile_program") as mock:
|
||||||
|
mock.return_value = 0 # Default to success
|
||||||
|
yield mock
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_get_esphome_components() -> Generator[Mock]:
|
||||||
|
"""Mock get_esphome_components for testing."""
|
||||||
|
with patch("esphome.analyze_memory.helpers.get_esphome_components") as mock:
|
||||||
|
mock.return_value = {"logger", "api", "ota"}
|
||||||
|
yield mock
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_memory_analyzer_cli() -> Generator[Mock]:
|
||||||
|
"""Mock MemoryAnalyzerCLI for testing."""
|
||||||
|
with patch("esphome.analyze_memory.cli.MemoryAnalyzerCLI") as mock_class:
|
||||||
|
mock_analyzer = MagicMock()
|
||||||
|
mock_analyzer.generate_report.return_value = "Mock Memory Report"
|
||||||
|
mock_class.return_value = mock_analyzer
|
||||||
|
yield mock_class
|
||||||
|
|
||||||
|
|
||||||
def test_choose_upload_log_host_with_string_default() -> None:
|
def test_choose_upload_log_host_with_string_default() -> None:
|
||||||
"""Test with a single string default device."""
|
"""Test with a single string default device."""
|
||||||
setup_core()
|
setup_core()
|
||||||
@@ -839,7 +875,7 @@ def test_upload_program_serial_esp8266_with_file(
|
|||||||
|
|
||||||
def test_upload_using_esptool_path_conversion(
|
def test_upload_using_esptool_path_conversion(
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
mock_run_external_command: Mock,
|
mock_run_external_command_main: Mock,
|
||||||
mock_get_idedata: Mock,
|
mock_get_idedata: Mock,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test upload_using_esptool properly converts Path objects to strings for esptool.
|
"""Test upload_using_esptool properly converts Path objects to strings for esptool.
|
||||||
@@ -875,10 +911,10 @@ def test_upload_using_esptool_path_conversion(
|
|||||||
assert result == 0
|
assert result == 0
|
||||||
|
|
||||||
# Verify that run_external_command was called
|
# Verify that run_external_command was called
|
||||||
assert mock_run_external_command.call_count == 1
|
assert mock_run_external_command_main.call_count == 1
|
||||||
|
|
||||||
# Get the actual call arguments
|
# Get the actual call arguments
|
||||||
call_args = mock_run_external_command.call_args[0]
|
call_args = mock_run_external_command_main.call_args[0]
|
||||||
|
|
||||||
# The first argument should be esptool.main function,
|
# The first argument should be esptool.main function,
|
||||||
# followed by the command arguments
|
# followed by the command arguments
|
||||||
@@ -917,7 +953,7 @@ def test_upload_using_esptool_path_conversion(
|
|||||||
|
|
||||||
def test_upload_using_esptool_with_file_path(
|
def test_upload_using_esptool_with_file_path(
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
mock_run_external_command: Mock,
|
mock_run_external_command_main: Mock,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test upload_using_esptool with a custom file that's a Path object."""
|
"""Test upload_using_esptool with a custom file that's a Path object."""
|
||||||
setup_core(platform=PLATFORM_ESP8266, tmp_path=tmp_path, name="test")
|
setup_core(platform=PLATFORM_ESP8266, tmp_path=tmp_path, name="test")
|
||||||
@@ -934,10 +970,10 @@ def test_upload_using_esptool_with_file_path(
|
|||||||
assert result == 0
|
assert result == 0
|
||||||
|
|
||||||
# Verify that run_external_command was called
|
# Verify that run_external_command was called
|
||||||
mock_run_external_command.assert_called_once()
|
mock_run_external_command_main.assert_called_once()
|
||||||
|
|
||||||
# Get the actual call arguments
|
# Get the actual call arguments
|
||||||
call_args = mock_run_external_command.call_args[0]
|
call_args = mock_run_external_command_main.call_args[0]
|
||||||
cmd_list = list(call_args[1:]) # Skip the esptool.main function
|
cmd_list = list(call_args[1:]) # Skip the esptool.main function
|
||||||
|
|
||||||
# Find the firmware path in the command
|
# Find the firmware path in the command
|
||||||
@@ -2273,3 +2309,226 @@ def test_show_logs_api_mqtt_timeout_fallback(
|
|||||||
|
|
||||||
# Verify run_logs was called with only the static IP (MQTT failed)
|
# Verify run_logs was called with only the static IP (MQTT failed)
|
||||||
mock_run_logs.assert_called_once_with(CORE.config, ["192.168.1.100"])
|
mock_run_logs.assert_called_once_with(CORE.config, ["192.168.1.100"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_detect_external_components_no_external(
|
||||||
|
mock_get_esphome_components: Mock,
|
||||||
|
) -> None:
|
||||||
|
"""Test detect_external_components with no external components."""
|
||||||
|
config = {
|
||||||
|
CONF_ESPHOME: {CONF_NAME: "test_device"},
|
||||||
|
"logger": {},
|
||||||
|
"api": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
result = detect_external_components(config)
|
||||||
|
|
||||||
|
assert result == set()
|
||||||
|
mock_get_esphome_components.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_detect_external_components_with_external(
|
||||||
|
mock_get_esphome_components: Mock,
|
||||||
|
) -> None:
|
||||||
|
"""Test detect_external_components detects external components."""
|
||||||
|
config = {
|
||||||
|
CONF_ESPHOME: {CONF_NAME: "test_device"},
|
||||||
|
"logger": {}, # Built-in
|
||||||
|
"api": {}, # Built-in
|
||||||
|
"my_custom_sensor": {}, # External
|
||||||
|
"another_custom": {}, # External
|
||||||
|
"external_components": [], # Special key, not a component
|
||||||
|
"substitutions": {}, # Special key, not a component
|
||||||
|
}
|
||||||
|
|
||||||
|
result = detect_external_components(config)
|
||||||
|
|
||||||
|
assert result == {"my_custom_sensor", "another_custom"}
|
||||||
|
mock_get_esphome_components.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_detect_external_components_filters_special_keys(
|
||||||
|
mock_get_esphome_components: Mock,
|
||||||
|
) -> None:
|
||||||
|
"""Test detect_external_components filters out special config keys."""
|
||||||
|
config = {
|
||||||
|
CONF_ESPHOME: {CONF_NAME: "test_device"},
|
||||||
|
"substitutions": {"key": "value"},
|
||||||
|
"packages": {},
|
||||||
|
"globals": [],
|
||||||
|
"external_components": [],
|
||||||
|
"<<": {}, # YAML merge key
|
||||||
|
}
|
||||||
|
|
||||||
|
result = detect_external_components(config)
|
||||||
|
|
||||||
|
assert result == set()
|
||||||
|
mock_get_esphome_components.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_command_analyze_memory_success(
|
||||||
|
tmp_path: Path,
|
||||||
|
capfd: CaptureFixture[str],
|
||||||
|
mock_write_cpp: Mock,
|
||||||
|
mock_compile_program: Mock,
|
||||||
|
mock_get_idedata: Mock,
|
||||||
|
mock_get_esphome_components: Mock,
|
||||||
|
mock_memory_analyzer_cli: Mock,
|
||||||
|
) -> None:
|
||||||
|
"""Test command_analyze_memory with successful compilation and analysis."""
|
||||||
|
setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path, name="test_device")
|
||||||
|
|
||||||
|
# Create firmware.elf file
|
||||||
|
firmware_path = (
|
||||||
|
tmp_path / ".esphome" / "build" / "test_device" / ".pioenvs" / "test_device"
|
||||||
|
)
|
||||||
|
firmware_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
firmware_elf = firmware_path / "firmware.elf"
|
||||||
|
firmware_elf.write_text("mock elf file")
|
||||||
|
|
||||||
|
# Mock idedata
|
||||||
|
mock_idedata_obj = MagicMock(spec=platformio_api.IDEData)
|
||||||
|
mock_idedata_obj.firmware_elf_path = str(firmware_elf)
|
||||||
|
mock_idedata_obj.objdump_path = "/path/to/objdump"
|
||||||
|
mock_idedata_obj.readelf_path = "/path/to/readelf"
|
||||||
|
mock_get_idedata.return_value = mock_idedata_obj
|
||||||
|
|
||||||
|
config = {
|
||||||
|
CONF_ESPHOME: {CONF_NAME: "test_device"},
|
||||||
|
"logger": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
args = MockArgs()
|
||||||
|
|
||||||
|
result = command_analyze_memory(args, config)
|
||||||
|
|
||||||
|
assert result == 0
|
||||||
|
|
||||||
|
# Verify compilation was done
|
||||||
|
mock_write_cpp.assert_called_once_with(config)
|
||||||
|
mock_compile_program.assert_called_once_with(args, config)
|
||||||
|
|
||||||
|
# Verify analyzer was created with correct parameters
|
||||||
|
mock_memory_analyzer_cli.assert_called_once_with(
|
||||||
|
str(firmware_elf),
|
||||||
|
"/path/to/objdump",
|
||||||
|
"/path/to/readelf",
|
||||||
|
set(), # No external components
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify analysis was run
|
||||||
|
mock_analyzer = mock_memory_analyzer_cli.return_value
|
||||||
|
mock_analyzer.analyze.assert_called_once()
|
||||||
|
mock_analyzer.generate_report.assert_called_once()
|
||||||
|
|
||||||
|
# Verify report was printed
|
||||||
|
captured = capfd.readouterr()
|
||||||
|
assert "Mock Memory Report" in captured.out
|
||||||
|
|
||||||
|
|
||||||
|
def test_command_analyze_memory_with_external_components(
|
||||||
|
tmp_path: Path,
|
||||||
|
mock_write_cpp: Mock,
|
||||||
|
mock_compile_program: Mock,
|
||||||
|
mock_get_idedata: Mock,
|
||||||
|
mock_get_esphome_components: Mock,
|
||||||
|
mock_memory_analyzer_cli: Mock,
|
||||||
|
) -> None:
|
||||||
|
"""Test command_analyze_memory detects external components."""
|
||||||
|
setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path, name="test_device")
|
||||||
|
|
||||||
|
# Create firmware.elf file
|
||||||
|
firmware_path = (
|
||||||
|
tmp_path / ".esphome" / "build" / "test_device" / ".pioenvs" / "test_device"
|
||||||
|
)
|
||||||
|
firmware_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
firmware_elf = firmware_path / "firmware.elf"
|
||||||
|
firmware_elf.write_text("mock elf file")
|
||||||
|
|
||||||
|
# Mock idedata
|
||||||
|
mock_idedata_obj = MagicMock(spec=platformio_api.IDEData)
|
||||||
|
mock_idedata_obj.firmware_elf_path = str(firmware_elf)
|
||||||
|
mock_idedata_obj.objdump_path = "/path/to/objdump"
|
||||||
|
mock_idedata_obj.readelf_path = "/path/to/readelf"
|
||||||
|
mock_get_idedata.return_value = mock_idedata_obj
|
||||||
|
|
||||||
|
config = {
|
||||||
|
CONF_ESPHOME: {CONF_NAME: "test_device"},
|
||||||
|
"logger": {},
|
||||||
|
"my_custom_component": {"param": "value"}, # External component
|
||||||
|
"external_components": [{"source": "github://user/repo"}], # Not a component
|
||||||
|
}
|
||||||
|
|
||||||
|
args = MockArgs()
|
||||||
|
|
||||||
|
result = command_analyze_memory(args, config)
|
||||||
|
|
||||||
|
assert result == 0
|
||||||
|
|
||||||
|
# Verify analyzer was created with external components detected
|
||||||
|
mock_memory_analyzer_cli.assert_called_once_with(
|
||||||
|
str(firmware_elf),
|
||||||
|
"/path/to/objdump",
|
||||||
|
"/path/to/readelf",
|
||||||
|
{"my_custom_component"}, # External component detected
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_command_analyze_memory_write_cpp_fails(
|
||||||
|
tmp_path: Path,
|
||||||
|
mock_write_cpp: Mock,
|
||||||
|
) -> None:
|
||||||
|
"""Test command_analyze_memory when write_cpp fails."""
|
||||||
|
setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path, name="test_device")
|
||||||
|
|
||||||
|
config = {CONF_ESPHOME: {CONF_NAME: "test_device"}}
|
||||||
|
args = MockArgs()
|
||||||
|
|
||||||
|
mock_write_cpp.return_value = 1 # Failure
|
||||||
|
|
||||||
|
result = command_analyze_memory(args, config)
|
||||||
|
|
||||||
|
assert result == 1
|
||||||
|
mock_write_cpp.assert_called_once_with(config)
|
||||||
|
|
||||||
|
|
||||||
|
def test_command_analyze_memory_compile_fails(
|
||||||
|
tmp_path: Path,
|
||||||
|
mock_write_cpp: Mock,
|
||||||
|
mock_compile_program: Mock,
|
||||||
|
) -> None:
|
||||||
|
"""Test command_analyze_memory when compilation fails."""
|
||||||
|
setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path, name="test_device")
|
||||||
|
|
||||||
|
config = {CONF_ESPHOME: {CONF_NAME: "test_device"}}
|
||||||
|
args = MockArgs()
|
||||||
|
|
||||||
|
mock_compile_program.return_value = 1 # Compilation failed
|
||||||
|
|
||||||
|
result = command_analyze_memory(args, config)
|
||||||
|
|
||||||
|
assert result == 1
|
||||||
|
mock_write_cpp.assert_called_once_with(config)
|
||||||
|
mock_compile_program.assert_called_once_with(args, config)
|
||||||
|
|
||||||
|
|
||||||
|
def test_command_analyze_memory_no_idedata(
|
||||||
|
tmp_path: Path,
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
mock_write_cpp: Mock,
|
||||||
|
mock_compile_program: Mock,
|
||||||
|
mock_get_idedata: Mock,
|
||||||
|
) -> None:
|
||||||
|
"""Test command_analyze_memory when idedata cannot be retrieved."""
|
||||||
|
setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path, name="test_device")
|
||||||
|
|
||||||
|
config = {CONF_ESPHOME: {CONF_NAME: "test_device"}}
|
||||||
|
args = MockArgs()
|
||||||
|
|
||||||
|
mock_get_idedata.return_value = None # Failed to get idedata
|
||||||
|
|
||||||
|
with caplog.at_level(logging.ERROR):
|
||||||
|
result = command_analyze_memory(args, config)
|
||||||
|
|
||||||
|
assert result == 1
|
||||||
|
assert "Failed to get IDE data for memory analysis" in caplog.text
|
||||||
|
|||||||
Reference in New Issue
Block a user