mirror of
https://github.com/esphome/esphome.git
synced 2026-01-20 01:49:11 -07:00
Compare commits
84 Commits
cache_gith
...
select_opt
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bfd115d25c | ||
|
|
bfacb8d363 | ||
|
|
d955479145 | ||
|
|
5a3251a693 | ||
|
|
ceea861cc4 | ||
|
|
cc6ee15e99 | ||
|
|
65c88e3ec4 | ||
|
|
7cd787075f | ||
|
|
88c7811e99 | ||
|
|
ae50a09b4e | ||
|
|
a966ac7255 | ||
|
|
1ea80594c6 | ||
|
|
8500323d39 | ||
|
|
6f7db2f5f7 | ||
|
|
9922c65912 | ||
|
|
f2469077d9 | ||
|
|
742eca92d8 | ||
|
|
548913b471 | ||
|
|
a05c5ea240 | ||
|
|
8e8a2bde95 | ||
|
|
80265a6bd2 | ||
|
|
87e9a7a1bd | ||
|
|
3aedfe8be3 | ||
|
|
7f2cc47ed6 | ||
|
|
a5542e0d2b | ||
|
|
66afe4a9be | ||
|
|
0ae9009e41 | ||
|
|
0b2f5fcd7e | ||
|
|
7a2887e2ed | ||
|
|
cd2d3f061d | ||
|
|
73f5d01c2d | ||
|
|
0938609f7a | ||
|
|
77203f0cb4 | ||
|
|
040130e357 | ||
|
|
85959e3004 | ||
|
|
a809a13729 | ||
|
|
3b6ff615e8 | ||
|
|
05216db5f0 | ||
|
|
9f668b0c4b | ||
|
|
6a239f4d1c | ||
|
|
ffb0e854b6 | ||
|
|
6fbd0e3385 | ||
|
|
426511e78d | ||
|
|
97d91fee85 | ||
|
|
0f4b54aa82 | ||
|
|
1706a69fad | ||
|
|
e23d66a8cf | ||
|
|
46101fd830 | ||
|
|
e988905c2f | ||
|
|
abb57f08f5 | ||
|
|
ca2fe994a1 | ||
|
|
03def13917 | ||
|
|
63f100a8ca | ||
|
|
ea4e5fd7bd | ||
|
|
12e9c5e60e | ||
|
|
3d82c5baf7 | ||
|
|
6f5e36ffc3 | ||
|
|
118b1d8593 | ||
|
|
319ba4a504 | ||
|
|
ae8336c268 | ||
|
|
1b38518c63 | ||
|
|
c00977df54 | ||
|
|
255b5a3abd | ||
|
|
dd732dd155 | ||
|
|
22fec4329f | ||
|
|
8f1c4634ec | ||
|
|
c15f1a9be8 | ||
|
|
11b53096a6 | ||
|
|
6a18367949 | ||
|
|
a59b1494d8 | ||
|
|
e6ce5c58d1 | ||
|
|
ebc0f5f7c9 | ||
|
|
87ca8784ef | ||
|
|
a186c1062f | ||
|
|
ea38237f29 | ||
|
|
6aff1394ad | ||
|
|
0e34d1b64d | ||
|
|
1483cee0fb | ||
|
|
8c1bd2fd85 | ||
|
|
ea609dc0f6 | ||
|
|
913095f6be | ||
|
|
bb24ad4a30 | ||
|
|
0d612fecfc | ||
|
|
9c235b4140 |
@@ -1 +1 @@
|
|||||||
d7693a1e996cacd4a3d1c9a16336799c2a8cc3db02e4e74084151ce964581248
|
3d46b63015d761c85ca9cb77ab79a389509e5776701fb22aed16e7b79d432c0c
|
||||||
|
|||||||
7
.github/workflows/auto-label-pr.yml
vendored
7
.github/workflows/auto-label-pr.yml
vendored
@@ -53,6 +53,7 @@ jobs:
|
|||||||
'new-target-platform',
|
'new-target-platform',
|
||||||
'merging-to-release',
|
'merging-to-release',
|
||||||
'merging-to-beta',
|
'merging-to-beta',
|
||||||
|
'chained-pr',
|
||||||
'core',
|
'core',
|
||||||
'small-pr',
|
'small-pr',
|
||||||
'dashboard',
|
'dashboard',
|
||||||
@@ -140,6 +141,8 @@ jobs:
|
|||||||
labels.add('merging-to-release');
|
labels.add('merging-to-release');
|
||||||
} else if (baseRef === 'beta') {
|
} else if (baseRef === 'beta') {
|
||||||
labels.add('merging-to-beta');
|
labels.add('merging-to-beta');
|
||||||
|
} else if (baseRef !== 'dev') {
|
||||||
|
labels.add('chained-pr');
|
||||||
}
|
}
|
||||||
|
|
||||||
return labels;
|
return labels;
|
||||||
@@ -528,8 +531,8 @@ jobs:
|
|||||||
const apiData = await fetchApiData();
|
const apiData = await fetchApiData();
|
||||||
const baseRef = context.payload.pull_request.base.ref;
|
const baseRef = context.payload.pull_request.base.ref;
|
||||||
|
|
||||||
// Early exit for non-dev branches
|
// Early exit for release and beta branches only
|
||||||
if (baseRef !== 'dev') {
|
if (baseRef === 'release' || baseRef === 'beta') {
|
||||||
const branchLabels = await detectMergeBranch();
|
const branchLabels = await detectMergeBranch();
|
||||||
const finalLabels = Array.from(branchLabels);
|
const finalLabels = Array.from(branchLabels);
|
||||||
|
|
||||||
|
|||||||
19
.github/workflows/ci-memory-impact-comment.yml
vendored
19
.github/workflows/ci-memory-impact-comment.yml
vendored
@@ -28,20 +28,23 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
# Get PR details by searching for PR with matching head SHA
|
# Get PR details by searching for PR with matching head SHA
|
||||||
# The workflow_run.pull_requests field is often empty for forks
|
# 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 }}"
|
head_sha="${{ github.event.workflow_run.head_sha }}"
|
||||||
pr_data=$(gh api "/repos/${{ github.repository }}/commits/$head_sha/pulls" \
|
pr_data=$(gh api --paginate "/repos/${{ github.repository }}/pulls" \
|
||||||
--jq '.[0] | {number: .number, base_ref: .base.ref}')
|
--jq ".[] | select(.head.sha == \"$head_sha\") | {number: .number, base_ref: .base.ref}" \
|
||||||
if [ -z "$pr_data" ] || [ "$pr_data" == "null" ]; then
|
| head -n 1)
|
||||||
|
|
||||||
|
if [ -z "$pr_data" ]; then
|
||||||
echo "No PR found for SHA $head_sha, skipping"
|
echo "No PR found for SHA $head_sha, skipping"
|
||||||
echo "skip=true" >> $GITHUB_OUTPUT
|
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
pr_number=$(echo "$pr_data" | jq -r '.number')
|
pr_number=$(echo "$pr_data" | jq -r '.number')
|
||||||
base_ref=$(echo "$pr_data" | jq -r '.base_ref')
|
base_ref=$(echo "$pr_data" | jq -r '.base_ref')
|
||||||
|
|
||||||
echo "pr_number=$pr_number" >> $GITHUB_OUTPUT
|
echo "pr_number=$pr_number" >> "$GITHUB_OUTPUT"
|
||||||
echo "base_ref=$base_ref" >> $GITHUB_OUTPUT
|
echo "base_ref=$base_ref" >> "$GITHUB_OUTPUT"
|
||||||
echo "Found PR #$pr_number targeting base branch: $base_ref"
|
echo "Found PR #$pr_number targeting base branch: $base_ref"
|
||||||
|
|
||||||
- name: Check out code from base repository
|
- name: Check out code from base repository
|
||||||
@@ -87,9 +90,9 @@ jobs:
|
|||||||
if: steps.pr.outputs.skip != 'true'
|
if: steps.pr.outputs.skip != 'true'
|
||||||
run: |
|
run: |
|
||||||
if [ -f ./memory-analysis/memory-analysis-target.json ] && [ -f ./memory-analysis/memory-analysis-pr.json ]; then
|
if [ -f ./memory-analysis/memory-analysis-target.json ] && [ -f ./memory-analysis/memory-analysis-pr.json ]; then
|
||||||
echo "found=true" >> $GITHUB_OUTPUT
|
echo "found=true" >> "$GITHUB_OUTPUT"
|
||||||
else
|
else
|
||||||
echo "found=false" >> $GITHUB_OUTPUT
|
echo "found=false" >> "$GITHUB_OUTPUT"
|
||||||
echo "Memory analysis artifacts not found, skipping comment"
|
echo "Memory analysis artifacts not found, skipping comment"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
221
.github/workflows/ci.yml
vendored
221
.github/workflows/ci.yml
vendored
@@ -170,12 +170,16 @@ 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 }}
|
||||||
|
cpp-unit-tests-run-all: ${{ steps.determine.outputs.cpp-unit-tests-run-all }}
|
||||||
|
cpp-unit-tests-components: ${{ steps.determine.outputs.cpp-unit-tests-components }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
@@ -200,12 +204,16 @@ 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
|
||||||
|
echo "cpp-unit-tests-run-all=$(echo "$output" | jq -r '.cpp_unit_tests_run_all')" >> $GITHUB_OUTPUT
|
||||||
|
echo "cpp-unit-tests-components=$(echo "$output" | jq -c '.cpp_unit_tests_components')" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
integration-tests:
|
integration-tests:
|
||||||
name: Run integration tests
|
name: Run integration tests
|
||||||
@@ -243,7 +251,34 @@ 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:
|
cpp-unit-tests:
|
||||||
|
name: Run C++ unit tests
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
needs:
|
||||||
|
- common
|
||||||
|
- determine-jobs
|
||||||
|
if: github.event_name == 'pull_request' && (needs.determine-jobs.outputs.cpp-unit-tests-run-all == 'true' || needs.determine-jobs.outputs.cpp-unit-tests-components != '[]')
|
||||||
|
steps:
|
||||||
|
- name: Check out code from GitHub
|
||||||
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
|
- name: Restore Python
|
||||||
|
uses: ./.github/actions/restore-python
|
||||||
|
with:
|
||||||
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||||
|
|
||||||
|
- name: Run cpp_unit_test.py
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
if [ "${{ needs.determine-jobs.outputs.cpp-unit-tests-run-all }}" = "true" ]; then
|
||||||
|
script/cpp_unit_test.py --all
|
||||||
|
else
|
||||||
|
ARGS=$(echo '${{ needs.determine-jobs.outputs.cpp-unit-tests-components }}' | jq -r '.[] | @sh' | xargs)
|
||||||
|
script/cpp_unit_test.py $ARGS
|
||||||
|
fi
|
||||||
|
|
||||||
|
clang-tidy-single:
|
||||||
name: ${{ matrix.name }}
|
name: ${{ matrix.name }}
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
needs:
|
needs:
|
||||||
@@ -261,22 +296,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 +376,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
|
||||||
@@ -764,13 +943,13 @@ 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
|
||||||
@@ -797,7 +976,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
|
||||||
|
|||||||
1
.github/workflows/status-check-labels.yml
vendored
1
.github/workflows/status-check-labels.yml
vendored
@@ -14,6 +14,7 @@ jobs:
|
|||||||
label:
|
label:
|
||||||
- needs-docs
|
- needs-docs
|
||||||
- merge-after-release
|
- merge-after-release
|
||||||
|
- chained-pr
|
||||||
steps:
|
steps:
|
||||||
- name: Check for ${{ matrix.label }} label
|
- name: Check for ${{ matrix.label }} label
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -892,6 +926,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:
|
||||||
@@ -1007,6 +1089,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 = [
|
||||||
@@ -1292,6 +1375,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>.
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -231,9 +231,22 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
|||||||
api_component = (name, mem)
|
api_component = (name, mem)
|
||||||
break
|
break
|
||||||
|
|
||||||
# Combine all components to analyze: top ESPHome + all external + API if not already included
|
# Also include wifi_stack and other important system components if they exist
|
||||||
components_to_analyze = list(top_esphome_components) + list(
|
system_components_to_include = [
|
||||||
top_external_components
|
# Empty list - we've finished debugging symbol categorization
|
||||||
|
# Add component names here if you need to debug their symbols
|
||||||
|
]
|
||||||
|
system_components = [
|
||||||
|
(name, mem)
|
||||||
|
for name, mem in components
|
||||||
|
if name in system_components_to_include
|
||||||
|
]
|
||||||
|
|
||||||
|
# Combine all components to analyze: top ESPHome + all external + API if not already included + system components
|
||||||
|
components_to_analyze = (
|
||||||
|
list(top_esphome_components)
|
||||||
|
+ list(top_external_components)
|
||||||
|
+ system_components
|
||||||
)
|
)
|
||||||
if api_component and api_component not in components_to_analyze:
|
if api_component and api_component not in components_to_analyze:
|
||||||
components_to_analyze.append(api_component)
|
components_to_analyze.append(api_component)
|
||||||
|
|||||||
@@ -127,40 +127,39 @@ SYMBOL_PATTERNS = {
|
|||||||
"tryget_socket_unconn",
|
"tryget_socket_unconn",
|
||||||
"cs_create_ctrl_sock",
|
"cs_create_ctrl_sock",
|
||||||
"netbuf_alloc",
|
"netbuf_alloc",
|
||||||
|
"tcp_", # TCP protocol functions
|
||||||
|
"udp_", # UDP protocol functions
|
||||||
|
"lwip_", # LwIP stack functions
|
||||||
|
"eagle_lwip", # ESP-specific LwIP functions
|
||||||
|
"new_linkoutput", # Link output function
|
||||||
|
"acd_", # Address Conflict Detection (ACD)
|
||||||
|
"eth_", # Ethernet functions
|
||||||
|
"mac_enable_bb", # MAC baseband enable
|
||||||
|
"reassemble_and_dispatch", # Packet reassembly
|
||||||
],
|
],
|
||||||
|
# dhcp must come before libc to avoid "dhcp_select" matching "select" pattern
|
||||||
|
"dhcp": ["dhcp", "handle_dhcp"],
|
||||||
"ipv6_stack": ["nd6_", "ip6_", "mld6_", "icmp6_", "icmp6_input"],
|
"ipv6_stack": ["nd6_", "ip6_", "mld6_", "icmp6_", "icmp6_input"],
|
||||||
"wifi_stack": [
|
# Order matters! More specific categories must come before general ones.
|
||||||
"ieee80211",
|
# mdns must come before bluetooth to avoid "_mdns_disable_pcb" matching "ble_" pattern
|
||||||
"hostap",
|
"mdns_lib": ["mdns"],
|
||||||
"sta_",
|
# memory_mgmt must come before wifi_stack to catch mmu_hal_* symbols
|
||||||
"ap_",
|
"memory_mgmt": [
|
||||||
"scan_",
|
"mem_",
|
||||||
"wifi_",
|
"memory_",
|
||||||
"wpa_",
|
"tlsf_",
|
||||||
"wps_",
|
"memp_",
|
||||||
"esp_wifi",
|
"pbuf_",
|
||||||
"cnx_",
|
"pbuf_alloc",
|
||||||
"wpa3_",
|
"pbuf_copy_partial_pbuf",
|
||||||
"sae_",
|
"esp_mmu_map",
|
||||||
"wDev_",
|
"mmu_hal_",
|
||||||
"ic_",
|
"s_do_mapping", # Memory mapping function, not WiFi
|
||||||
"mac_",
|
"hash_map_", # Hash map data structure
|
||||||
"esf_buf",
|
"umm_assimilate", # UMM malloc assimilation
|
||||||
"gWpaSm",
|
|
||||||
"sm_WPA",
|
|
||||||
"eapol_",
|
|
||||||
"owe_",
|
|
||||||
"wifiLowLevelInit",
|
|
||||||
"s_do_mapping",
|
|
||||||
"gScanStruct",
|
|
||||||
"ppSearchTxframe",
|
|
||||||
"ppMapWaitTxq",
|
|
||||||
"ppFillAMPDUBar",
|
|
||||||
"ppCheckTxConnTrafficIdle",
|
|
||||||
"ppCalTkipMic",
|
|
||||||
],
|
],
|
||||||
"bluetooth": ["bt_", "ble_", "l2c_", "gatt_", "gap_", "hci_", "BT_init"],
|
# Bluetooth categories must come BEFORE wifi_stack to avoid misclassification
|
||||||
"wifi_bt_coex": ["coex"],
|
# Many BLE symbols contain patterns like "ble_" that would otherwise match wifi patterns
|
||||||
"bluetooth_rom": ["r_ble", "r_lld", "r_llc", "r_llm"],
|
"bluetooth_rom": ["r_ble", "r_lld", "r_llc", "r_llm"],
|
||||||
"bluedroid_bt": [
|
"bluedroid_bt": [
|
||||||
"bluedroid",
|
"bluedroid",
|
||||||
@@ -207,6 +206,61 @@ SYMBOL_PATTERNS = {
|
|||||||
"copy_extra_byte_in_db",
|
"copy_extra_byte_in_db",
|
||||||
"parse_read_local_supported_commands_response",
|
"parse_read_local_supported_commands_response",
|
||||||
],
|
],
|
||||||
|
"bluetooth": [
|
||||||
|
"bt_",
|
||||||
|
"_ble_", # More specific than "ble_" to avoid matching "able_", "enable_", "disable_"
|
||||||
|
"l2c_",
|
||||||
|
"l2ble_", # L2CAP for BLE
|
||||||
|
"gatt_",
|
||||||
|
"gap_",
|
||||||
|
"hci_",
|
||||||
|
"btsnd_hcic_", # Bluetooth HCI command send functions
|
||||||
|
"BT_init",
|
||||||
|
"BT_tx_", # Bluetooth transmit functions
|
||||||
|
"esp_ble_", # Catch esp_ble_* functions
|
||||||
|
],
|
||||||
|
"bluetooth_ll": [
|
||||||
|
"llm_", # Link layer manager
|
||||||
|
"llc_", # Link layer control
|
||||||
|
"lld_", # Link layer driver
|
||||||
|
"ld_acl_", # Link layer ACL (Asynchronous Connection-Oriented)
|
||||||
|
"llcp_", # Link layer control protocol
|
||||||
|
"lmp_", # Link manager protocol
|
||||||
|
],
|
||||||
|
"wifi_bt_coex": ["coex"],
|
||||||
|
"wifi_stack": [
|
||||||
|
"ieee80211",
|
||||||
|
"hostap",
|
||||||
|
"sta_",
|
||||||
|
"wifi_ap_", # More specific than "ap_" to avoid matching "cap_", "map_"
|
||||||
|
"wifi_scan_", # More specific than "scan_" to avoid matching "_scan_" in other contexts
|
||||||
|
"wifi_",
|
||||||
|
"wpa_",
|
||||||
|
"wps_",
|
||||||
|
"esp_wifi",
|
||||||
|
"cnx_",
|
||||||
|
"wpa3_",
|
||||||
|
"sae_",
|
||||||
|
"wDev_",
|
||||||
|
"ic_mac_", # More specific than "mac_" to avoid matching emac_
|
||||||
|
"esf_buf",
|
||||||
|
"gWpaSm",
|
||||||
|
"sm_WPA",
|
||||||
|
"eapol_",
|
||||||
|
"owe_",
|
||||||
|
"wifiLowLevelInit",
|
||||||
|
# Removed "s_do_mapping" - this is memory management, not WiFi
|
||||||
|
"gScanStruct",
|
||||||
|
"ppSearchTxframe",
|
||||||
|
"ppMapWaitTxq",
|
||||||
|
"ppFillAMPDUBar",
|
||||||
|
"ppCheckTxConnTrafficIdle",
|
||||||
|
"ppCalTkipMic",
|
||||||
|
"phy_force_wifi",
|
||||||
|
"phy_unforce_wifi",
|
||||||
|
"write_wifi_chan",
|
||||||
|
"wifi_track_pll",
|
||||||
|
],
|
||||||
"crypto_math": [
|
"crypto_math": [
|
||||||
"ecp_",
|
"ecp_",
|
||||||
"bignum_",
|
"bignum_",
|
||||||
@@ -231,13 +285,36 @@ SYMBOL_PATTERNS = {
|
|||||||
"p_256_init_curve",
|
"p_256_init_curve",
|
||||||
"shift_sub_rows",
|
"shift_sub_rows",
|
||||||
"rshift",
|
"rshift",
|
||||||
|
"rijndaelEncrypt", # AES Rijndael encryption
|
||||||
|
],
|
||||||
|
# System and Arduino core functions must come before libc
|
||||||
|
"esp_system": [
|
||||||
|
"system_", # ESP system functions
|
||||||
|
"postmortem_", # Postmortem reporting
|
||||||
|
],
|
||||||
|
"arduino_core": [
|
||||||
|
"pinMode",
|
||||||
|
"resetPins",
|
||||||
|
"millis",
|
||||||
|
"micros",
|
||||||
|
"delay(", # More specific - Arduino delay function with parenthesis
|
||||||
|
"delayMicroseconds",
|
||||||
|
"digitalWrite",
|
||||||
|
"digitalRead",
|
||||||
|
],
|
||||||
|
"sntp": ["sntp_", "sntp_recv"],
|
||||||
|
"scheduler": [
|
||||||
|
"run_scheduled_",
|
||||||
|
"compute_scheduled_",
|
||||||
|
"event_TaskQueue",
|
||||||
],
|
],
|
||||||
"hw_crypto": ["esp_aes", "esp_sha", "esp_rsa", "esp_bignum", "esp_mpi"],
|
"hw_crypto": ["esp_aes", "esp_sha", "esp_rsa", "esp_bignum", "esp_mpi"],
|
||||||
"libc": [
|
"libc": [
|
||||||
"printf",
|
"printf",
|
||||||
"scanf",
|
"scanf",
|
||||||
"malloc",
|
"malloc",
|
||||||
"free",
|
"_free", # More specific than "free" to match _free, __free_r, etc. but not arbitrary "free" substring
|
||||||
|
"umm_free", # UMM malloc free function
|
||||||
"memcpy",
|
"memcpy",
|
||||||
"memset",
|
"memset",
|
||||||
"strcpy",
|
"strcpy",
|
||||||
@@ -259,7 +336,7 @@ SYMBOL_PATTERNS = {
|
|||||||
"_setenv_r",
|
"_setenv_r",
|
||||||
"_tzset_unlocked_r",
|
"_tzset_unlocked_r",
|
||||||
"__tzcalc_limits",
|
"__tzcalc_limits",
|
||||||
"select",
|
"_select", # More specific than "select" to avoid matching "dhcp_select", etc.
|
||||||
"scalbnf",
|
"scalbnf",
|
||||||
"strtof",
|
"strtof",
|
||||||
"strtof_l",
|
"strtof_l",
|
||||||
@@ -316,8 +393,24 @@ SYMBOL_PATTERNS = {
|
|||||||
"CSWTCH$",
|
"CSWTCH$",
|
||||||
"dst$",
|
"dst$",
|
||||||
"sulp",
|
"sulp",
|
||||||
|
"_strtol_l", # String to long with locale
|
||||||
|
"__cvt", # Convert
|
||||||
|
"__utoa", # Unsigned to ASCII
|
||||||
|
"__global_locale", # Global locale
|
||||||
|
"_ctype_", # Character type
|
||||||
|
"impure_data", # Impure data
|
||||||
|
],
|
||||||
|
"string_ops": [
|
||||||
|
"strcmp",
|
||||||
|
"strncmp",
|
||||||
|
"strchr",
|
||||||
|
"strstr",
|
||||||
|
"strtok",
|
||||||
|
"strdup",
|
||||||
|
"strncasecmp_P", # String compare (case insensitive, from program memory)
|
||||||
|
"strnlen_P", # String length (from program memory)
|
||||||
|
"strncat_P", # String concatenate (from program memory)
|
||||||
],
|
],
|
||||||
"string_ops": ["strcmp", "strncmp", "strchr", "strstr", "strtok", "strdup"],
|
|
||||||
"memory_alloc": ["malloc", "calloc", "realloc", "free", "_sbrk"],
|
"memory_alloc": ["malloc", "calloc", "realloc", "free", "_sbrk"],
|
||||||
"file_io": [
|
"file_io": [
|
||||||
"fread",
|
"fread",
|
||||||
@@ -338,10 +431,26 @@ SYMBOL_PATTERNS = {
|
|||||||
"vsscanf",
|
"vsscanf",
|
||||||
],
|
],
|
||||||
"cpp_anonymous": ["_GLOBAL__N_", "n$"],
|
"cpp_anonymous": ["_GLOBAL__N_", "n$"],
|
||||||
"cpp_runtime": ["__cxx", "_ZN", "_ZL", "_ZSt", "__gxx_personality", "_Z16"],
|
# Plain C patterns only - C++ symbols will be categorized via DEMANGLED_PATTERNS
|
||||||
"exception_handling": ["__cxa_", "_Unwind_", "__gcc_personality", "uw_frame_state"],
|
"nvs": ["nvs_"], # Plain C NVS functions
|
||||||
|
"ota": ["ota_", "OTA", "esp_ota", "app_desc"],
|
||||||
|
# cpp_runtime: Removed _ZN, _ZL to let DEMANGLED_PATTERNS categorize C++ symbols properly
|
||||||
|
# Only keep patterns that are truly runtime-specific and not categorizable by namespace
|
||||||
|
"cpp_runtime": ["__cxx", "_ZSt", "__gxx_personality", "_Z16"],
|
||||||
|
"exception_handling": [
|
||||||
|
"__cxa_",
|
||||||
|
"_Unwind_",
|
||||||
|
"__gcc_personality",
|
||||||
|
"uw_frame_state",
|
||||||
|
"search_object", # Search for exception handling object
|
||||||
|
"get_cie_encoding", # Get CIE encoding
|
||||||
|
"add_fdes", # Add frame description entries
|
||||||
|
"fde_unencoded_compare", # Compare FDEs
|
||||||
|
"fde_mixed_encoding_compare", # Compare mixed encoding FDEs
|
||||||
|
"frame_downheap", # Frame heap operations
|
||||||
|
"frame_heapsort", # Frame heap sorting
|
||||||
|
],
|
||||||
"static_init": ["_GLOBAL__sub_I_"],
|
"static_init": ["_GLOBAL__sub_I_"],
|
||||||
"mdns_lib": ["mdns"],
|
|
||||||
"phy_radio": [
|
"phy_radio": [
|
||||||
"phy_",
|
"phy_",
|
||||||
"rf_",
|
"rf_",
|
||||||
@@ -394,10 +503,47 @@ SYMBOL_PATTERNS = {
|
|||||||
"txcal_debuge_mode",
|
"txcal_debuge_mode",
|
||||||
"ant_wifitx_cfg",
|
"ant_wifitx_cfg",
|
||||||
"reg_init_begin",
|
"reg_init_begin",
|
||||||
|
"tx_cap_init", # TX capacitance init
|
||||||
|
"ram_set_txcap", # RAM TX capacitance setting
|
||||||
|
"tx_atten_", # TX attenuation
|
||||||
|
"txiq_", # TX I/Q calibration
|
||||||
|
"ram_cal_", # RAM calibration
|
||||||
|
"ram_rxiq_", # RAM RX I/Q
|
||||||
|
"readvdd33", # Read VDD33
|
||||||
|
"test_tout", # Test timeout
|
||||||
|
"tsen_meas", # Temperature sensor measurement
|
||||||
|
"bbpll_cal", # Baseband PLL calibration
|
||||||
|
"set_cal_", # Set calibration
|
||||||
|
"set_rfanagain_", # Set RF analog gain
|
||||||
|
"set_txdc_", # Set TX DC
|
||||||
|
"get_vdd33_", # Get VDD33
|
||||||
|
"gen_rx_gain_table", # Generate RX gain table
|
||||||
|
"ram_ana_inf_gating_en", # RAM analog interface gating enable
|
||||||
|
"tx_cont_en", # TX continuous enable
|
||||||
|
"tx_delay_cfg", # TX delay configuration
|
||||||
|
"tx_gain_table_set", # TX gain table set
|
||||||
|
"check_and_reset_hw_deadlock", # Hardware deadlock check
|
||||||
|
"s_config", # System/hardware config
|
||||||
|
"chan14_mic_cfg", # Channel 14 MIC config
|
||||||
|
],
|
||||||
|
"wifi_phy_pp": [
|
||||||
|
"pp_",
|
||||||
|
"ppT",
|
||||||
|
"ppR",
|
||||||
|
"ppP",
|
||||||
|
"ppInstall",
|
||||||
|
"ppCalTxAMPDULength",
|
||||||
|
"ppCheckTx", # Packet processor TX check
|
||||||
|
"ppCal", # Packet processor calibration
|
||||||
|
"HdlAllBuffedEb", # Handle buffered EB
|
||||||
],
|
],
|
||||||
"wifi_phy_pp": ["pp_", "ppT", "ppR", "ppP", "ppInstall", "ppCalTxAMPDULength"],
|
|
||||||
"wifi_lmac": ["lmac"],
|
"wifi_lmac": ["lmac"],
|
||||||
"wifi_device": ["wdev", "wDev_"],
|
"wifi_device": [
|
||||||
|
"wdev",
|
||||||
|
"wDev_",
|
||||||
|
"ic_set_sta", # Set station mode
|
||||||
|
"ic_set_vif", # Set virtual interface
|
||||||
|
],
|
||||||
"power_mgmt": [
|
"power_mgmt": [
|
||||||
"pm_",
|
"pm_",
|
||||||
"sleep",
|
"sleep",
|
||||||
@@ -406,15 +552,7 @@ SYMBOL_PATTERNS = {
|
|||||||
"deep_sleep",
|
"deep_sleep",
|
||||||
"power_down",
|
"power_down",
|
||||||
"g_pm",
|
"g_pm",
|
||||||
],
|
"pmc", # Power Management Controller
|
||||||
"memory_mgmt": [
|
|
||||||
"mem_",
|
|
||||||
"memory_",
|
|
||||||
"tlsf_",
|
|
||||||
"memp_",
|
|
||||||
"pbuf_",
|
|
||||||
"pbuf_alloc",
|
|
||||||
"pbuf_copy_partial_pbuf",
|
|
||||||
],
|
],
|
||||||
"hal_layer": ["hal_"],
|
"hal_layer": ["hal_"],
|
||||||
"clock_mgmt": [
|
"clock_mgmt": [
|
||||||
@@ -439,7 +577,6 @@ SYMBOL_PATTERNS = {
|
|||||||
"error_handling": ["panic", "abort", "assert", "error_", "fault"],
|
"error_handling": ["panic", "abort", "assert", "error_", "fault"],
|
||||||
"authentication": ["auth"],
|
"authentication": ["auth"],
|
||||||
"ppp_protocol": ["ppp", "ipcp_", "lcp_", "chap_", "LcpEchoCheck"],
|
"ppp_protocol": ["ppp", "ipcp_", "lcp_", "chap_", "LcpEchoCheck"],
|
||||||
"dhcp": ["dhcp", "handle_dhcp"],
|
|
||||||
"ethernet_phy": [
|
"ethernet_phy": [
|
||||||
"emac_",
|
"emac_",
|
||||||
"eth_phy_",
|
"eth_phy_",
|
||||||
@@ -618,7 +755,15 @@ SYMBOL_PATTERNS = {
|
|||||||
"ampdu_dispatch_upto",
|
"ampdu_dispatch_upto",
|
||||||
],
|
],
|
||||||
"ieee802_11": ["ieee802_11_", "ieee802_11_parse_elems"],
|
"ieee802_11": ["ieee802_11_", "ieee802_11_parse_elems"],
|
||||||
"rate_control": ["rssi_margin", "rcGetSched", "get_rate_fcc_index"],
|
"rate_control": [
|
||||||
|
"rssi_margin",
|
||||||
|
"rcGetSched",
|
||||||
|
"get_rate_fcc_index",
|
||||||
|
"rcGetRate", # Get rate
|
||||||
|
"rc_get_", # Rate control getters
|
||||||
|
"rc_set_", # Rate control setters
|
||||||
|
"rc_enable_", # Rate control enable functions
|
||||||
|
],
|
||||||
"nan": ["nan_dp_", "nan_dp_post_tx", "nan_dp_delete_peer"],
|
"nan": ["nan_dp_", "nan_dp_post_tx", "nan_dp_delete_peer"],
|
||||||
"channel_mgmt": ["chm_init", "chm_set_current_channel"],
|
"channel_mgmt": ["chm_init", "chm_set_current_channel"],
|
||||||
"trace": ["trc_init", "trc_onAmpduOp"],
|
"trace": ["trc_init", "trc_onAmpduOp"],
|
||||||
@@ -799,31 +944,18 @@ SYMBOL_PATTERNS = {
|
|||||||
"supports_interlaced_inquiry_scan",
|
"supports_interlaced_inquiry_scan",
|
||||||
"supports_reading_remote_extended_features",
|
"supports_reading_remote_extended_features",
|
||||||
],
|
],
|
||||||
"bluetooth_ll": [
|
|
||||||
"lld_pdu_",
|
|
||||||
"ld_acl_",
|
|
||||||
"lld_stop_ind_handler",
|
|
||||||
"lld_evt_winsize_change",
|
|
||||||
"config_lld_evt_funcs_reset",
|
|
||||||
"config_lld_funcs_reset",
|
|
||||||
"config_llm_funcs_reset",
|
|
||||||
"llm_set_long_adv_data",
|
|
||||||
"lld_retry_tx_prog",
|
|
||||||
"llc_link_sup_to_ind_handler",
|
|
||||||
"config_llc_funcs_reset",
|
|
||||||
"lld_evt_rxwin_compute",
|
|
||||||
"config_btdm_funcs_reset",
|
|
||||||
"config_ea_funcs_reset",
|
|
||||||
"llc_defalut_state_tab_reset",
|
|
||||||
"config_rwip_funcs_reset",
|
|
||||||
"ke_lmp_rx_flooding_detect",
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Demangled patterns: patterns found in demangled C++ names
|
# Demangled patterns: patterns found in demangled C++ names
|
||||||
DEMANGLED_PATTERNS = {
|
DEMANGLED_PATTERNS = {
|
||||||
"gpio_driver": ["GPIO"],
|
"gpio_driver": ["GPIO"],
|
||||||
"uart_driver": ["UART"],
|
"uart_driver": ["UART"],
|
||||||
|
# mdns_lib must come before network_stack to avoid "udp" matching "_udpReadBuffer" in MDNSResponder
|
||||||
|
"mdns_lib": [
|
||||||
|
"MDNSResponder",
|
||||||
|
"MDNSImplementation",
|
||||||
|
"MDNS",
|
||||||
|
],
|
||||||
"network_stack": [
|
"network_stack": [
|
||||||
"lwip",
|
"lwip",
|
||||||
"tcp",
|
"tcp",
|
||||||
@@ -836,6 +968,24 @@ DEMANGLED_PATTERNS = {
|
|||||||
"ethernet",
|
"ethernet",
|
||||||
"ppp",
|
"ppp",
|
||||||
"slip",
|
"slip",
|
||||||
|
"UdpContext", # UDP context class
|
||||||
|
"DhcpServer", # DHCP server class
|
||||||
|
],
|
||||||
|
"arduino_core": [
|
||||||
|
"String::", # Arduino String class
|
||||||
|
"Print::", # Arduino Print class
|
||||||
|
"HardwareSerial::", # Serial class
|
||||||
|
"IPAddress::", # IP address class
|
||||||
|
"EspClass::", # ESP class
|
||||||
|
"experimental::_SPI", # Experimental SPI
|
||||||
|
],
|
||||||
|
"ota": [
|
||||||
|
"UpdaterClass",
|
||||||
|
"Updater::",
|
||||||
|
],
|
||||||
|
"wifi": [
|
||||||
|
"ESP8266WiFi",
|
||||||
|
"WiFi::",
|
||||||
],
|
],
|
||||||
"wifi_stack": ["NetworkInterface"],
|
"wifi_stack": ["NetworkInterface"],
|
||||||
"nimble_bt": [
|
"nimble_bt": [
|
||||||
@@ -854,7 +1004,6 @@ DEMANGLED_PATTERNS = {
|
|||||||
"rtti": ["__type_info", "__class_type_info"],
|
"rtti": ["__type_info", "__class_type_info"],
|
||||||
"web_server_lib": ["AsyncWebServer", "AsyncWebHandler", "WebServer"],
|
"web_server_lib": ["AsyncWebServer", "AsyncWebHandler", "WebServer"],
|
||||||
"async_tcp": ["AsyncClient", "AsyncServer"],
|
"async_tcp": ["AsyncClient", "AsyncServer"],
|
||||||
"mdns_lib": ["mdns"],
|
|
||||||
"json_lib": [
|
"json_lib": [
|
||||||
"ArduinoJson",
|
"ArduinoJson",
|
||||||
"JsonDocument",
|
"JsonDocument",
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -155,6 +155,17 @@ def _validate_api_config(config: ConfigType) -> ConfigType:
|
|||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def _consume_api_sockets(config: ConfigType) -> ConfigType:
|
||||||
|
"""Register socket needs for API component."""
|
||||||
|
from esphome.components import socket
|
||||||
|
|
||||||
|
# API needs 1 listening socket + typically 3 concurrent client connections
|
||||||
|
# (not max_connections, which is the upper limit rarely reached)
|
||||||
|
sockets_needed = 1 + 3
|
||||||
|
socket.consume_sockets(sockets_needed, "api")(config)
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.All(
|
CONFIG_SCHEMA = cv.All(
|
||||||
cv.Schema(
|
cv.Schema(
|
||||||
{
|
{
|
||||||
@@ -222,6 +233,7 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
).extend(cv.COMPONENT_SCHEMA),
|
).extend(cv.COMPONENT_SCHEMA),
|
||||||
cv.rename_key(CONF_SERVICES, CONF_ACTIONS),
|
cv.rename_key(CONF_SERVICES, CONF_ACTIONS),
|
||||||
_validate_api_config,
|
_validate_api_config,
|
||||||
|
_consume_api_sockets,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1143,7 +1143,7 @@ message ListEntitiesSelectResponse {
|
|||||||
reserved 4; // Deprecated: was string unique_id
|
reserved 4; // Deprecated: was string unique_id
|
||||||
|
|
||||||
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
|
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
|
||||||
repeated string options = 6 [(container_pointer) = "std::vector"];
|
repeated string options = 6 [(container_pointer) = "FixedVector"];
|
||||||
bool disabled_by_default = 7;
|
bool disabled_by_default = 7;
|
||||||
EntityCategory entity_category = 8;
|
EntityCategory entity_category = 8;
|
||||||
uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"];
|
uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"];
|
||||||
|
|||||||
@@ -1534,7 +1534,7 @@ class ListEntitiesSelectResponse final : public InfoResponseProtoMessage {
|
|||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
const char *message_name() const override { return "list_entities_select_response"; }
|
const char *message_name() const override { return "list_entities_select_response"; }
|
||||||
#endif
|
#endif
|
||||||
const std::vector<std::string> *options{};
|
const FixedVector<std::string> *options{};
|
||||||
void encode(ProtoWriteBuffer buffer) const override;
|
void encode(ProtoWriteBuffer buffer) const override;
|
||||||
void calculate_size(ProtoSize &size) const override;
|
void calculate_size(ProtoSize &size) const override;
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -264,20 +264,31 @@ async def delayed_off_filter_to_code(config, filter_id):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
async def autorepeat_filter_to_code(config, filter_id):
|
async def autorepeat_filter_to_code(config, filter_id):
|
||||||
timings = []
|
|
||||||
if len(config) > 0:
|
if len(config) > 0:
|
||||||
timings.extend(
|
timings = [
|
||||||
(conf[CONF_DELAY], conf[CONF_TIME_OFF], conf[CONF_TIME_ON])
|
cg.StructInitializer(
|
||||||
for conf in config
|
cg.MockObj("AutorepeatFilterTiming", "esphome::binary_sensor::"),
|
||||||
)
|
("delay", conf[CONF_DELAY]),
|
||||||
else:
|
("time_off", conf[CONF_TIME_OFF]),
|
||||||
timings.append(
|
("time_on", conf[CONF_TIME_ON]),
|
||||||
(
|
|
||||||
cv.time_period_str_unit(DEFAULT_DELAY).total_milliseconds,
|
|
||||||
cv.time_period_str_unit(DEFAULT_TIME_OFF).total_milliseconds,
|
|
||||||
cv.time_period_str_unit(DEFAULT_TIME_ON).total_milliseconds,
|
|
||||||
)
|
)
|
||||||
)
|
for conf in config
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
timings = [
|
||||||
|
cg.StructInitializer(
|
||||||
|
cg.MockObj("AutorepeatFilterTiming", "esphome::binary_sensor::"),
|
||||||
|
("delay", cv.time_period_str_unit(DEFAULT_DELAY).total_milliseconds),
|
||||||
|
(
|
||||||
|
"time_off",
|
||||||
|
cv.time_period_str_unit(DEFAULT_TIME_OFF).total_milliseconds,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"time_on",
|
||||||
|
cv.time_period_str_unit(DEFAULT_TIME_ON).total_milliseconds,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
]
|
||||||
var = cg.new_Pvariable(filter_id, timings)
|
var = cg.new_Pvariable(filter_id, timings)
|
||||||
await cg.register_component(var, {})
|
await cg.register_component(var, {})
|
||||||
return var
|
return var
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ void BinarySensor::add_filter(Filter *filter) {
|
|||||||
last_filter->next_ = filter;
|
last_filter->next_ = filter;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
void BinarySensor::add_filters(const std::vector<Filter *> &filters) {
|
void BinarySensor::add_filters(std::initializer_list<Filter *> filters) {
|
||||||
for (Filter *filter : filters) {
|
for (Filter *filter : filters) {
|
||||||
this->add_filter(filter);
|
this->add_filter(filter);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/components/binary_sensor/filter.h"
|
#include "esphome/components/binary_sensor/filter.h"
|
||||||
|
|
||||||
#include <vector>
|
#include <initializer_list>
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ class BinarySensor : public StatefulEntityBase<bool>, public EntityBase_DeviceCl
|
|||||||
void publish_initial_state(bool new_state);
|
void publish_initial_state(bool new_state);
|
||||||
|
|
||||||
void add_filter(Filter *filter);
|
void add_filter(Filter *filter);
|
||||||
void add_filters(const std::vector<Filter *> &filters);
|
void add_filters(std::initializer_list<Filter *> filters);
|
||||||
|
|
||||||
// ========== INTERNAL METHODS ==========
|
// ========== INTERNAL METHODS ==========
|
||||||
// (In most use cases you won't need these)
|
// (In most use cases you won't need these)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
#include "filter.h"
|
#include "filter.h"
|
||||||
|
|
||||||
#include "binary_sensor.h"
|
#include "binary_sensor.h"
|
||||||
#include <utility>
|
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
|
|
||||||
@@ -68,7 +67,7 @@ float DelayedOffFilter::get_setup_priority() const { return setup_priority::HARD
|
|||||||
|
|
||||||
optional<bool> InvertFilter::new_value(bool value) { return !value; }
|
optional<bool> InvertFilter::new_value(bool value) { return !value; }
|
||||||
|
|
||||||
AutorepeatFilter::AutorepeatFilter(std::vector<AutorepeatFilterTiming> timings) : timings_(std::move(timings)) {}
|
AutorepeatFilter::AutorepeatFilter(std::initializer_list<AutorepeatFilterTiming> timings) : timings_(timings) {}
|
||||||
|
|
||||||
optional<bool> AutorepeatFilter::new_value(bool value) {
|
optional<bool> AutorepeatFilter::new_value(bool value) {
|
||||||
if (value) {
|
if (value) {
|
||||||
|
|||||||
@@ -4,8 +4,6 @@
|
|||||||
#include "esphome/core/component.h"
|
#include "esphome/core/component.h"
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
|
|
||||||
namespace binary_sensor {
|
namespace binary_sensor {
|
||||||
@@ -82,11 +80,6 @@ class InvertFilter : public Filter {
|
|||||||
};
|
};
|
||||||
|
|
||||||
struct AutorepeatFilterTiming {
|
struct AutorepeatFilterTiming {
|
||||||
AutorepeatFilterTiming(uint32_t delay, uint32_t off, uint32_t on) {
|
|
||||||
this->delay = delay;
|
|
||||||
this->time_off = off;
|
|
||||||
this->time_on = on;
|
|
||||||
}
|
|
||||||
uint32_t delay;
|
uint32_t delay;
|
||||||
uint32_t time_off;
|
uint32_t time_off;
|
||||||
uint32_t time_on;
|
uint32_t time_on;
|
||||||
@@ -94,7 +87,7 @@ struct AutorepeatFilterTiming {
|
|||||||
|
|
||||||
class AutorepeatFilter : public Filter, public Component {
|
class AutorepeatFilter : public Filter, public Component {
|
||||||
public:
|
public:
|
||||||
explicit AutorepeatFilter(std::vector<AutorepeatFilterTiming> timings);
|
explicit AutorepeatFilter(std::initializer_list<AutorepeatFilterTiming> timings);
|
||||||
|
|
||||||
optional<bool> new_value(bool value) override;
|
optional<bool> new_value(bool value) override;
|
||||||
|
|
||||||
@@ -104,7 +97,7 @@ class AutorepeatFilter : public Filter, public Component {
|
|||||||
void next_timing_();
|
void next_timing_();
|
||||||
void next_value_(bool val);
|
void next_value_(bool val);
|
||||||
|
|
||||||
std::vector<AutorepeatFilterTiming> timings_;
|
FixedVector<AutorepeatFilterTiming> timings_;
|
||||||
uint8_t active_timing_{0};
|
uint8_t active_timing_{0};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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)) {
|
|
||||||
this->custom_fan_mode_ = fan_mode;
|
|
||||||
this->fan_mode_.reset();
|
|
||||||
} else {
|
|
||||||
ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), fan_mode.c_str());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (this->parent_->get_traits().supports_custom_fan_mode(fan_mode)) {
|
||||||
|
this->custom_fan_mode_ = fan_mode;
|
||||||
|
this->fan_mode_.reset();
|
||||||
|
} else {
|
||||||
|
ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), fan_mode.c_str());
|
||||||
|
}
|
||||||
return *this;
|
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)) {
|
|
||||||
this->custom_preset_ = preset;
|
|
||||||
this->preset_.reset();
|
|
||||||
} else {
|
|
||||||
ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), preset.c_str());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (this->parent_->get_traits().supports_custom_preset(preset)) {
|
||||||
|
this->custom_preset_ = preset;
|
||||||
|
this->preset_.reset();
|
||||||
|
} else {
|
||||||
|
ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), preset.c_str());
|
||||||
|
}
|
||||||
return *this;
|
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)
|
||||||
@@ -369,12 +385,14 @@ void Climate::save_state_() {
|
|||||||
if (!traits.get_supported_custom_fan_modes().empty() && custom_fan_mode.has_value()) {
|
if (!traits.get_supported_custom_fan_modes().empty() && custom_fan_mode.has_value()) {
|
||||||
state.uses_custom_fan_mode = true;
|
state.uses_custom_fan_mode = true;
|
||||||
const auto &supported = traits.get_supported_custom_fan_modes();
|
const auto &supported = traits.get_supported_custom_fan_modes();
|
||||||
std::vector<std::string> vec{supported.begin(), supported.end()};
|
// std::set has consistent order (lexicographic for strings)
|
||||||
for (size_t i = 0; i < vec.size(); i++) {
|
size_t i = 0;
|
||||||
if (vec[i] == custom_fan_mode) {
|
for (const auto &mode : supported) {
|
||||||
|
if (mode == custom_fan_mode) {
|
||||||
state.custom_fan_mode = i;
|
state.custom_fan_mode = i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
i++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (traits.get_supports_presets() && preset.has_value()) {
|
if (traits.get_supports_presets() && preset.has_value()) {
|
||||||
@@ -384,12 +402,14 @@ void Climate::save_state_() {
|
|||||||
if (!traits.get_supported_custom_presets().empty() && custom_preset.has_value()) {
|
if (!traits.get_supported_custom_presets().empty() && custom_preset.has_value()) {
|
||||||
state.uses_custom_preset = true;
|
state.uses_custom_preset = true;
|
||||||
const auto &supported = traits.get_supported_custom_presets();
|
const auto &supported = traits.get_supported_custom_presets();
|
||||||
std::vector<std::string> vec{supported.begin(), supported.end()};
|
// std::set has consistent order (lexicographic for strings)
|
||||||
for (size_t i = 0; i < vec.size(); i++) {
|
size_t i = 0;
|
||||||
if (vec[i] == custom_preset) {
|
for (const auto &preset : supported) {
|
||||||
|
if (preset == custom_preset) {
|
||||||
state.custom_preset = i;
|
state.custom_preset = i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
i++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (traits.get_supports_swing_modes()) {
|
if (traits.get_supports_swing_modes()) {
|
||||||
@@ -398,6 +418,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 +490,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 +535,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;
|
||||||
@@ -527,22 +553,34 @@ void ClimateDeviceRestoreState::apply(Climate *climate) {
|
|||||||
climate->fan_mode = this->fan_mode;
|
climate->fan_mode = this->fan_mode;
|
||||||
}
|
}
|
||||||
if (!traits.get_supported_custom_fan_modes().empty() && this->uses_custom_fan_mode) {
|
if (!traits.get_supported_custom_fan_modes().empty() && this->uses_custom_fan_mode) {
|
||||||
// std::set has consistent order (lexicographic for strings), so this is ok
|
// std::set has consistent order (lexicographic for strings)
|
||||||
const auto &modes = traits.get_supported_custom_fan_modes();
|
const auto &modes = traits.get_supported_custom_fan_modes();
|
||||||
std::vector<std::string> modes_vec{modes.begin(), modes.end()};
|
if (custom_fan_mode < modes.size()) {
|
||||||
if (custom_fan_mode < modes_vec.size()) {
|
size_t i = 0;
|
||||||
climate->custom_fan_mode = modes_vec[this->custom_fan_mode];
|
for (const auto &mode : modes) {
|
||||||
|
if (i == this->custom_fan_mode) {
|
||||||
|
climate->custom_fan_mode = mode;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (traits.get_supports_presets() && !this->uses_custom_preset) {
|
if (traits.get_supports_presets() && !this->uses_custom_preset) {
|
||||||
climate->preset = this->preset;
|
climate->preset = this->preset;
|
||||||
}
|
}
|
||||||
if (!traits.get_supported_custom_presets().empty() && uses_custom_preset) {
|
if (!traits.get_supported_custom_presets().empty() && uses_custom_preset) {
|
||||||
// std::set has consistent order (lexicographic for strings), so this is ok
|
// std::set has consistent order (lexicographic for strings)
|
||||||
const auto &presets = traits.get_supported_custom_presets();
|
const auto &presets = traits.get_supported_custom_presets();
|
||||||
std::vector<std::string> presets_vec{presets.begin(), presets.end()};
|
if (custom_preset < presets.size()) {
|
||||||
if (custom_preset < presets_vec.size()) {
|
size_t i = 0;
|
||||||
climate->custom_preset = presets_vec[this->custom_preset];
|
for (const auto &preset : presets) {
|
||||||
|
if (i == this->custom_preset) {
|
||||||
|
climate->custom_preset = preset;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (traits.get_supports_swing_modes()) {
|
if (traits.get_supports_swing_modes()) {
|
||||||
@@ -579,68 +617,68 @@ 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"
|
||||||
" Target: %.1f",
|
" Target: %.1f",
|
||||||
traits.get_visual_min_temperature(), traits.get_visual_max_temperature(),
|
traits.get_visual_min_temperature(), traits.get_visual_max_temperature(),
|
||||||
traits.get_visual_target_temperature_step());
|
traits.get_visual_target_temperature_step());
|
||||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) {
|
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) {
|
||||||
ESP_LOGCONFIG(tag, " Current: %.1f", traits.get_visual_current_temperature_step());
|
ESP_LOGCONFIG(tag, " Current: %.1f", traits.get_visual_current_temperature_step());
|
||||||
}
|
}
|
||||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY |
|
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY |
|
||||||
climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) {
|
climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) {
|
||||||
ESP_LOGCONFIG(tag,
|
ESP_LOGCONFIG(tag,
|
||||||
" - Min humidity: %.0f\n"
|
" - Min humidity: %.0f\n"
|
||||||
" - Max humidity: %.0f",
|
" - Max humidity: %.0f",
|
||||||
traits.get_visual_min_humidity(), traits.get_visual_max_humidity());
|
traits.get_visual_min_humidity(), traits.get_visual_max_humidity());
|
||||||
}
|
}
|
||||||
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_);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ static const char *const TAG = "copy.select";
|
|||||||
void CopySelect::setup() {
|
void CopySelect::setup() {
|
||||||
source_->add_on_state_callback([this](const std::string &value, size_t index) { this->publish_state(value); });
|
source_->add_on_state_callback([this](const std::string &value, size_t index) { this->publish_state(value); });
|
||||||
|
|
||||||
traits.set_options(source_->traits.get_options());
|
this->traits.copy_options(source_->traits.get_options());
|
||||||
|
|
||||||
if (source_->has_state())
|
if (source_->has_state())
|
||||||
this->publish_state(source_->state);
|
this->publish_state(source_->state);
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import contextlib
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
import itertools
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
@@ -102,6 +103,10 @@ COMPILER_OPTIMIZATIONS = {
|
|||||||
"SIZE": "CONFIG_COMPILER_OPTIMIZATION_SIZE",
|
"SIZE": "CONFIG_COMPILER_OPTIMIZATION_SIZE",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Socket limit configuration for ESP-IDF
|
||||||
|
# ESP-IDF CONFIG_LWIP_MAX_SOCKETS has range 1-253, default 10
|
||||||
|
DEFAULT_MAX_SOCKETS = 10 # ESP-IDF default
|
||||||
|
|
||||||
ARDUINO_ALLOWED_VARIANTS = [
|
ARDUINO_ALLOWED_VARIANTS = [
|
||||||
VARIANT_ESP32,
|
VARIANT_ESP32,
|
||||||
VARIANT_ESP32C3,
|
VARIANT_ESP32C3,
|
||||||
@@ -545,6 +550,32 @@ CONF_ENABLE_LWIP_BRIDGE_INTERFACE = "enable_lwip_bridge_interface"
|
|||||||
CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING = "enable_lwip_tcpip_core_locking"
|
CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING = "enable_lwip_tcpip_core_locking"
|
||||||
CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY = "enable_lwip_check_thread_safety"
|
CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY = "enable_lwip_check_thread_safety"
|
||||||
CONF_DISABLE_LIBC_LOCKS_IN_IRAM = "disable_libc_locks_in_iram"
|
CONF_DISABLE_LIBC_LOCKS_IN_IRAM = "disable_libc_locks_in_iram"
|
||||||
|
CONF_DISABLE_VFS_SUPPORT_TERMIOS = "disable_vfs_support_termios"
|
||||||
|
CONF_DISABLE_VFS_SUPPORT_SELECT = "disable_vfs_support_select"
|
||||||
|
CONF_DISABLE_VFS_SUPPORT_DIR = "disable_vfs_support_dir"
|
||||||
|
|
||||||
|
# VFS requirement tracking
|
||||||
|
# Components that need VFS features can call require_vfs_select() or require_vfs_dir()
|
||||||
|
KEY_VFS_SELECT_REQUIRED = "vfs_select_required"
|
||||||
|
KEY_VFS_DIR_REQUIRED = "vfs_dir_required"
|
||||||
|
|
||||||
|
|
||||||
|
def require_vfs_select() -> None:
|
||||||
|
"""Mark that VFS select support is required by a component.
|
||||||
|
|
||||||
|
Call this from components that use esp_vfs_eventfd or other VFS select features.
|
||||||
|
This prevents CONFIG_VFS_SUPPORT_SELECT from being disabled.
|
||||||
|
"""
|
||||||
|
CORE.data[KEY_VFS_SELECT_REQUIRED] = True
|
||||||
|
|
||||||
|
|
||||||
|
def require_vfs_dir() -> None:
|
||||||
|
"""Mark that VFS directory support is required by a component.
|
||||||
|
|
||||||
|
Call this from components that use directory functions (opendir, readdir, mkdir, etc.).
|
||||||
|
This prevents CONFIG_VFS_SUPPORT_DIR from being disabled.
|
||||||
|
"""
|
||||||
|
CORE.data[KEY_VFS_DIR_REQUIRED] = True
|
||||||
|
|
||||||
|
|
||||||
def _validate_idf_component(config: ConfigType) -> ConfigType:
|
def _validate_idf_component(config: ConfigType) -> ConfigType:
|
||||||
@@ -610,6 +641,13 @@ FRAMEWORK_SCHEMA = cv.All(
|
|||||||
cv.Optional(
|
cv.Optional(
|
||||||
CONF_DISABLE_LIBC_LOCKS_IN_IRAM, default=True
|
CONF_DISABLE_LIBC_LOCKS_IN_IRAM, default=True
|
||||||
): cv.boolean,
|
): cv.boolean,
|
||||||
|
cv.Optional(
|
||||||
|
CONF_DISABLE_VFS_SUPPORT_TERMIOS, default=True
|
||||||
|
): cv.boolean,
|
||||||
|
cv.Optional(
|
||||||
|
CONF_DISABLE_VFS_SUPPORT_SELECT, default=True
|
||||||
|
): cv.boolean,
|
||||||
|
cv.Optional(CONF_DISABLE_VFS_SUPPORT_DIR, default=True): cv.boolean,
|
||||||
cv.Optional(CONF_EXECUTE_FROM_PSRAM): cv.boolean,
|
cv.Optional(CONF_EXECUTE_FROM_PSRAM): cv.boolean,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
@@ -746,6 +784,72 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
FINAL_VALIDATE_SCHEMA = cv.Schema(final_validate)
|
FINAL_VALIDATE_SCHEMA = cv.Schema(final_validate)
|
||||||
|
|
||||||
|
|
||||||
|
def _configure_lwip_max_sockets(conf: dict) -> None:
|
||||||
|
"""Calculate and set CONFIG_LWIP_MAX_SOCKETS based on component needs.
|
||||||
|
|
||||||
|
Socket component tracks consumer needs via consume_sockets() called during config validation.
|
||||||
|
This function runs in to_code() after all components have registered their socket needs.
|
||||||
|
User-provided sdkconfig_options take precedence.
|
||||||
|
"""
|
||||||
|
from esphome.components.socket import KEY_SOCKET_CONSUMERS
|
||||||
|
|
||||||
|
# Check if user manually specified CONFIG_LWIP_MAX_SOCKETS
|
||||||
|
user_max_sockets = conf.get(CONF_SDKCONFIG_OPTIONS, {}).get(
|
||||||
|
"CONFIG_LWIP_MAX_SOCKETS"
|
||||||
|
)
|
||||||
|
|
||||||
|
socket_consumers: dict[str, int] = CORE.data.get(KEY_SOCKET_CONSUMERS, {})
|
||||||
|
total_sockets = sum(socket_consumers.values())
|
||||||
|
|
||||||
|
# Early return if no sockets registered and no user override
|
||||||
|
if total_sockets == 0 and user_max_sockets is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
components_list = ", ".join(
|
||||||
|
f"{name}={count}" for name, count in sorted(socket_consumers.items())
|
||||||
|
)
|
||||||
|
|
||||||
|
# User specified their own value - respect it but warn if insufficient
|
||||||
|
if user_max_sockets is not None:
|
||||||
|
_LOGGER.info(
|
||||||
|
"Using user-provided CONFIG_LWIP_MAX_SOCKETS: %s",
|
||||||
|
user_max_sockets,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Warn if user's value is less than what components need
|
||||||
|
if total_sockets > 0:
|
||||||
|
user_sockets_int = 0
|
||||||
|
with contextlib.suppress(ValueError, TypeError):
|
||||||
|
user_sockets_int = int(user_max_sockets)
|
||||||
|
|
||||||
|
if user_sockets_int < total_sockets:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"CONFIG_LWIP_MAX_SOCKETS is set to %d but your configuration "
|
||||||
|
"needs %d sockets (registered: %s). You may experience socket "
|
||||||
|
"exhaustion errors. Consider increasing to at least %d.",
|
||||||
|
user_sockets_int,
|
||||||
|
total_sockets,
|
||||||
|
components_list,
|
||||||
|
total_sockets,
|
||||||
|
)
|
||||||
|
# User's value already added via sdkconfig_options processing
|
||||||
|
return
|
||||||
|
|
||||||
|
# Auto-calculate based on component needs
|
||||||
|
# Use at least the ESP-IDF default (10), or the total needed by components
|
||||||
|
max_sockets = max(DEFAULT_MAX_SOCKETS, total_sockets)
|
||||||
|
|
||||||
|
log_level = logging.INFO if max_sockets > DEFAULT_MAX_SOCKETS else logging.DEBUG
|
||||||
|
_LOGGER.log(
|
||||||
|
log_level,
|
||||||
|
"Setting CONFIG_LWIP_MAX_SOCKETS to %d (registered: %s)",
|
||||||
|
max_sockets,
|
||||||
|
components_list,
|
||||||
|
)
|
||||||
|
|
||||||
|
add_idf_sdkconfig_option("CONFIG_LWIP_MAX_SOCKETS", max_sockets)
|
||||||
|
|
||||||
|
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
cg.add_platformio_option("board", config[CONF_BOARD])
|
cg.add_platformio_option("board", config[CONF_BOARD])
|
||||||
cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE])
|
cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE])
|
||||||
@@ -779,6 +883,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")
|
||||||
@@ -805,6 +919,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")
|
||||||
|
|
||||||
@@ -855,6 +970,9 @@ async def to_code(config):
|
|||||||
add_idf_sdkconfig_option("CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES", False)
|
add_idf_sdkconfig_option("CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES", False)
|
||||||
if not advanced.get(CONF_ENABLE_LWIP_BRIDGE_INTERFACE, False):
|
if not advanced.get(CONF_ENABLE_LWIP_BRIDGE_INTERFACE, False):
|
||||||
add_idf_sdkconfig_option("CONFIG_LWIP_BRIDGEIF_MAX_PORTS", 0)
|
add_idf_sdkconfig_option("CONFIG_LWIP_BRIDGEIF_MAX_PORTS", 0)
|
||||||
|
|
||||||
|
_configure_lwip_max_sockets(conf)
|
||||||
|
|
||||||
if advanced.get(CONF_EXECUTE_FROM_PSRAM, False):
|
if advanced.get(CONF_EXECUTE_FROM_PSRAM, False):
|
||||||
add_idf_sdkconfig_option("CONFIG_SPIRAM_FETCH_INSTRUCTIONS", True)
|
add_idf_sdkconfig_option("CONFIG_SPIRAM_FETCH_INSTRUCTIONS", True)
|
||||||
add_idf_sdkconfig_option("CONFIG_SPIRAM_RODATA", True)
|
add_idf_sdkconfig_option("CONFIG_SPIRAM_RODATA", True)
|
||||||
@@ -877,6 +995,43 @@ async def to_code(config):
|
|||||||
if advanced.get(CONF_DISABLE_LIBC_LOCKS_IN_IRAM, True):
|
if advanced.get(CONF_DISABLE_LIBC_LOCKS_IN_IRAM, True):
|
||||||
add_idf_sdkconfig_option("CONFIG_LIBC_LOCKS_PLACE_IN_IRAM", False)
|
add_idf_sdkconfig_option("CONFIG_LIBC_LOCKS_PLACE_IN_IRAM", False)
|
||||||
|
|
||||||
|
# Disable VFS support for termios (terminal I/O functions)
|
||||||
|
# ESPHome doesn't use termios functions on ESP32 (only used in host UART driver).
|
||||||
|
# Saves approximately 1.8KB of flash when disabled (default).
|
||||||
|
add_idf_sdkconfig_option(
|
||||||
|
"CONFIG_VFS_SUPPORT_TERMIOS",
|
||||||
|
not advanced.get(CONF_DISABLE_VFS_SUPPORT_TERMIOS, True),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Disable VFS support for select() with file descriptors
|
||||||
|
# ESPHome only uses select() with sockets via lwip_select(), which still works.
|
||||||
|
# VFS select is only needed for UART/eventfd file descriptors.
|
||||||
|
# Components that need it (e.g., openthread) call require_vfs_select().
|
||||||
|
# Saves approximately 2.7KB of flash when disabled (default).
|
||||||
|
if CORE.data.get(KEY_VFS_SELECT_REQUIRED, False):
|
||||||
|
# Component requires VFS select - force enable regardless of user setting
|
||||||
|
add_idf_sdkconfig_option("CONFIG_VFS_SUPPORT_SELECT", True)
|
||||||
|
else:
|
||||||
|
# No component needs it - allow user to control (default: disabled)
|
||||||
|
add_idf_sdkconfig_option(
|
||||||
|
"CONFIG_VFS_SUPPORT_SELECT",
|
||||||
|
not advanced.get(CONF_DISABLE_VFS_SUPPORT_SELECT, True),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Disable VFS support for directory functions (opendir, readdir, mkdir, etc.)
|
||||||
|
# ESPHome doesn't use directory functions on ESP32.
|
||||||
|
# Components that need it (e.g., storage components) call require_vfs_dir().
|
||||||
|
# Saves approximately 0.5KB+ of flash when disabled (default).
|
||||||
|
if CORE.data.get(KEY_VFS_DIR_REQUIRED, False):
|
||||||
|
# Component requires VFS directory support - force enable regardless of user setting
|
||||||
|
add_idf_sdkconfig_option("CONFIG_VFS_SUPPORT_DIR", True)
|
||||||
|
else:
|
||||||
|
# No component needs it - allow user to control (default: disabled)
|
||||||
|
add_idf_sdkconfig_option(
|
||||||
|
"CONFIG_VFS_SUPPORT_DIR",
|
||||||
|
not advanced.get(CONF_DISABLE_VFS_SUPPORT_DIR, True),
|
||||||
|
)
|
||||||
|
|
||||||
cg.add_platformio_option("board_build.partitions", "partitions.csv")
|
cg.add_platformio_option("board_build.partitions", "partitions.csv")
|
||||||
if CONF_PARTITIONS in config:
|
if CONF_PARTITIONS in config:
|
||||||
add_extra_build_file(
|
add_extra_build_file(
|
||||||
|
|||||||
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)
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_ID, CONF_MODE, CONF_PORT
|
from esphome.const import CONF_ID, CONF_MODE, CONF_PORT
|
||||||
|
from esphome.types import ConfigType
|
||||||
|
|
||||||
CODEOWNERS = ["@ayufan"]
|
CODEOWNERS = ["@ayufan"]
|
||||||
AUTO_LOAD = ["camera"]
|
AUTO_LOAD = ["camera"]
|
||||||
@@ -13,13 +14,27 @@ Mode = esp32_camera_web_server_ns.enum("Mode")
|
|||||||
|
|
||||||
MODES = {"STREAM": Mode.STREAM, "SNAPSHOT": Mode.SNAPSHOT}
|
MODES = {"STREAM": Mode.STREAM, "SNAPSHOT": Mode.SNAPSHOT}
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.Schema(
|
|
||||||
{
|
def _consume_camera_web_server_sockets(config: ConfigType) -> ConfigType:
|
||||||
cv.GenerateID(): cv.declare_id(CameraWebServer),
|
"""Register socket needs for camera web server."""
|
||||||
cv.Required(CONF_PORT): cv.port,
|
from esphome.components import socket
|
||||||
cv.Required(CONF_MODE): cv.enum(MODES, upper=True),
|
|
||||||
},
|
# Each camera web server instance needs 1 listening socket + 2 client connections
|
||||||
).extend(cv.COMPONENT_SCHEMA)
|
sockets_needed = 3
|
||||||
|
socket.consume_sockets(sockets_needed, "esp32_camera_web_server")(config)
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = cv.All(
|
||||||
|
cv.Schema(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(CameraWebServer),
|
||||||
|
cv.Required(CONF_PORT): cv.port,
|
||||||
|
cv.Required(CONF_MODE): cv.enum(MODES, upper=True),
|
||||||
|
},
|
||||||
|
).extend(cv.COMPONENT_SCHEMA),
|
||||||
|
_consume_camera_web_server_sockets,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
|
|||||||
@@ -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,55 +44,63 @@ ESP32ImprovStoppedTrigger = esp32_improv_ns.class_(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.Schema(
|
CONFIG_SCHEMA = (
|
||||||
{
|
cv.Schema(
|
||||||
cv.GenerateID(): cv.declare_id(ESP32ImprovComponent),
|
{
|
||||||
cv.Required(CONF_AUTHORIZER): cv.Any(
|
cv.GenerateID(): cv.declare_id(ESP32ImprovComponent),
|
||||||
cv.none, cv.use_id(binary_sensor.BinarySensor)
|
cv.Required(CONF_AUTHORIZER): cv.Any(
|
||||||
),
|
cv.none, cv.use_id(binary_sensor.BinarySensor)
|
||||||
cv.Optional(CONF_STATUS_INDICATOR): cv.use_id(output.BinaryOutput),
|
),
|
||||||
cv.Optional(
|
cv.Optional(CONF_STATUS_INDICATOR): cv.use_id(output.BinaryOutput),
|
||||||
CONF_IDENTIFY_DURATION, default="10s"
|
cv.Optional(
|
||||||
): cv.positive_time_period_milliseconds,
|
CONF_IDENTIFY_DURATION, default="10s"
|
||||||
cv.Optional(
|
): cv.positive_time_period_milliseconds,
|
||||||
CONF_AUTHORIZED_DURATION, default="1min"
|
cv.Optional(
|
||||||
): cv.positive_time_period_milliseconds,
|
CONF_AUTHORIZED_DURATION, default="1min"
|
||||||
cv.Optional(
|
): cv.positive_time_period_milliseconds,
|
||||||
CONF_WIFI_TIMEOUT, default="1min"
|
cv.Optional(
|
||||||
): cv.positive_time_period_milliseconds,
|
CONF_WIFI_TIMEOUT, default="1min"
|
||||||
cv.Optional(CONF_ON_PROVISIONED): automation.validate_automation(
|
): cv.positive_time_period_milliseconds,
|
||||||
{
|
cv.Optional(CONF_ON_PROVISIONED): automation.validate_automation(
|
||||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
{
|
||||||
ESP32ImprovProvisionedTrigger
|
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||||
),
|
ESP32ImprovProvisionedTrigger
|
||||||
}
|
),
|
||||||
),
|
}
|
||||||
cv.Optional(CONF_ON_PROVISIONING): automation.validate_automation(
|
),
|
||||||
{
|
cv.Optional(CONF_ON_PROVISIONING): automation.validate_automation(
|
||||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
{
|
||||||
ESP32ImprovProvisioningTrigger
|
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||||
),
|
ESP32ImprovProvisioningTrigger
|
||||||
}
|
),
|
||||||
),
|
}
|
||||||
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.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESP32ImprovStateTrigger),
|
),
|
||||||
}
|
cv.Optional(CONF_ON_STATE): automation.validate_automation(
|
||||||
),
|
{
|
||||||
cv.Optional(CONF_ON_STOP): automation.validate_automation(
|
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||||
{
|
ESP32ImprovStateTrigger
|
||||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
),
|
||||||
ESP32ImprovStoppedTrigger
|
}
|
||||||
),
|
),
|
||||||
}
|
cv.Optional(CONF_ON_STOP): automation.validate_automation(
|
||||||
),
|
{
|
||||||
}
|
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||||
).extend(cv.COMPONENT_SCHEMA)
|
ESP32ImprovStoppedTrigger
|
||||||
|
),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.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, "esp32_improv")
|
||||||
|
|
||||||
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,34 @@ 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;
|
||||||
|
|
||||||
|
#ifdef USE_ESP32_IMPROV_NEXT_URL
|
||||||
|
// Add next_url if configured (should be first per Improv BLE spec)
|
||||||
|
std::string next_url = this->get_formatted_next_url_();
|
||||||
|
if (!next_url.empty()) {
|
||||||
|
url_strings[url_count++] = std::move(next_url);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Add default URLs for backward compatibility
|
||||||
|
url_strings[url_count++] = ESPHOME_MY_LINK;
|
||||||
#ifdef USE_WEBSERVER
|
#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;
|
||||||
|
|||||||
@@ -190,7 +190,9 @@ async def to_code(config):
|
|||||||
cg.add_define("ESPHOME_VARIANT", "ESP8266")
|
cg.add_define("ESPHOME_VARIANT", "ESP8266")
|
||||||
cg.add_define(ThreadModel.SINGLE)
|
cg.add_define(ThreadModel.SINGLE)
|
||||||
|
|
||||||
cg.add_platformio_option("extra_scripts", ["pre:iram_fix.py", "post:post_build.py"])
|
cg.add_platformio_option(
|
||||||
|
"extra_scripts", ["pre:testing_mode.py", "post:post_build.py"]
|
||||||
|
)
|
||||||
|
|
||||||
conf = config[CONF_FRAMEWORK]
|
conf = config[CONF_FRAMEWORK]
|
||||||
cg.add_platformio_option("framework", "arduino")
|
cg.add_platformio_option("framework", "arduino")
|
||||||
@@ -230,9 +232,9 @@ async def to_code(config):
|
|||||||
# For cases where nullptrs can be handled, use nothrow: `new (std::nothrow) T;`
|
# For cases where nullptrs can be handled, use nothrow: `new (std::nothrow) T;`
|
||||||
cg.add_build_flag("-DNEW_OOM_ABORT")
|
cg.add_build_flag("-DNEW_OOM_ABORT")
|
||||||
|
|
||||||
# In testing mode, fake a larger IRAM to allow linking grouped component tests
|
# In testing mode, fake larger memory to allow linking grouped component tests
|
||||||
# Real ESP8266 hardware only has 32KB IRAM, but for CI testing we pretend it has 2MB
|
# Real ESP8266 hardware only has 32KB IRAM and ~80KB RAM, but for CI testing
|
||||||
# This is done via a pre-build script that generates a custom linker script
|
# we pretend it has much larger memory to test that components compile together
|
||||||
if CORE.testing_mode:
|
if CORE.testing_mode:
|
||||||
cg.add_build_flag("-DESPHOME_TESTING_MODE")
|
cg.add_build_flag("-DESPHOME_TESTING_MODE")
|
||||||
|
|
||||||
@@ -271,8 +273,8 @@ def copy_files():
|
|||||||
post_build_file,
|
post_build_file,
|
||||||
CORE.relative_build_path("post_build.py"),
|
CORE.relative_build_path("post_build.py"),
|
||||||
)
|
)
|
||||||
iram_fix_file = dir / "iram_fix.py.script"
|
testing_mode_file = dir / "testing_mode.py.script"
|
||||||
copy_file_if_changed(
|
copy_file_if_changed(
|
||||||
iram_fix_file,
|
testing_mode_file,
|
||||||
CORE.relative_build_path("iram_fix.py"),
|
CORE.relative_build_path("testing_mode.py"),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
import os
|
|
||||||
import re
|
|
||||||
|
|
||||||
# pylint: disable=E0602
|
|
||||||
Import("env") # noqa
|
|
||||||
|
|
||||||
|
|
||||||
def patch_linker_script_after_preprocess(source, target, env):
|
|
||||||
"""Patch the local linker script after PlatformIO preprocesses it."""
|
|
||||||
# Check if we're in testing mode by looking for the define
|
|
||||||
build_flags = env.get("BUILD_FLAGS", [])
|
|
||||||
testing_mode = any("-DESPHOME_TESTING_MODE" in flag for flag in build_flags)
|
|
||||||
|
|
||||||
if not testing_mode:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Get the local linker script path
|
|
||||||
build_dir = env.subst("$BUILD_DIR")
|
|
||||||
local_ld = os.path.join(build_dir, "ld", "local.eagle.app.v6.common.ld")
|
|
||||||
|
|
||||||
if not os.path.exists(local_ld):
|
|
||||||
return
|
|
||||||
|
|
||||||
# Read the linker script
|
|
||||||
with open(local_ld, "r") as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
# Replace IRAM size from 0x8000 (32KB) to 0x200000 (2MB)
|
|
||||||
# The line looks like: iram1_0_seg : org = 0x40100000, len = 0x8000
|
|
||||||
updated = re.sub(
|
|
||||||
r"(iram1_0_seg\s*:\s*org\s*=\s*0x40100000\s*,\s*len\s*=\s*)0x8000",
|
|
||||||
r"\g<1>0x200000",
|
|
||||||
content,
|
|
||||||
)
|
|
||||||
|
|
||||||
if updated != content:
|
|
||||||
with open(local_ld, "w") as f:
|
|
||||||
f.write(updated)
|
|
||||||
print("ESPHome: Patched IRAM size to 2MB for testing mode")
|
|
||||||
|
|
||||||
|
|
||||||
# Hook into the build process right before linking
|
|
||||||
# This runs after PlatformIO has already preprocessed the linker scripts
|
|
||||||
env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", patch_linker_script_after_preprocess)
|
|
||||||
166
esphome/components/esp8266/testing_mode.py.script
Normal file
166
esphome/components/esp8266/testing_mode.py.script
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
# pylint: disable=E0602
|
||||||
|
Import("env") # noqa
|
||||||
|
|
||||||
|
|
||||||
|
# Memory sizes for testing mode (allow larger builds for CI component grouping)
|
||||||
|
TESTING_IRAM_SIZE = "0x200000" # 2MB
|
||||||
|
TESTING_DRAM_SIZE = "0x200000" # 2MB
|
||||||
|
TESTING_FLASH_SIZE = "0x2000000" # 32MB
|
||||||
|
|
||||||
|
|
||||||
|
def patch_segment_size(content, segment_name, new_size, label):
|
||||||
|
"""Patch a memory segment's length in linker script.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content: Linker script content
|
||||||
|
segment_name: Name of the segment (e.g., 'iram1_0_seg')
|
||||||
|
new_size: New size as hex string (e.g., '0x200000')
|
||||||
|
label: Human-readable label for logging (e.g., 'IRAM')
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (patched_content, was_patched)
|
||||||
|
"""
|
||||||
|
# Match: segment_name : org = 0x..., len = 0x...
|
||||||
|
pattern = rf"({segment_name}\s*:\s*org\s*=\s*0x[0-9a-fA-F]+\s*,\s*len\s*=\s*)0x[0-9a-fA-F]+"
|
||||||
|
new_content = re.sub(pattern, rf"\g<1>{new_size}", content)
|
||||||
|
return new_content, new_content != content
|
||||||
|
|
||||||
|
|
||||||
|
def apply_memory_patches(content):
|
||||||
|
"""Apply IRAM, DRAM, and Flash patches to linker script content.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content: Linker script content as string
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Patched content as string
|
||||||
|
"""
|
||||||
|
patches_applied = []
|
||||||
|
|
||||||
|
# Patch IRAM (for larger code in IRAM)
|
||||||
|
content, patched = patch_segment_size(content, "iram1_0_seg", TESTING_IRAM_SIZE, "IRAM")
|
||||||
|
if patched:
|
||||||
|
patches_applied.append("IRAM")
|
||||||
|
|
||||||
|
# Patch DRAM (for larger BSS/data sections)
|
||||||
|
content, patched = patch_segment_size(content, "dram0_0_seg", TESTING_DRAM_SIZE, "DRAM")
|
||||||
|
if patched:
|
||||||
|
patches_applied.append("DRAM")
|
||||||
|
|
||||||
|
# Patch Flash (for larger code sections)
|
||||||
|
content, patched = patch_segment_size(content, "irom0_0_seg", TESTING_FLASH_SIZE, "Flash")
|
||||||
|
if patched:
|
||||||
|
patches_applied.append("Flash")
|
||||||
|
|
||||||
|
if patches_applied:
|
||||||
|
iram_mb = int(TESTING_IRAM_SIZE, 16) // (1024 * 1024)
|
||||||
|
dram_mb = int(TESTING_DRAM_SIZE, 16) // (1024 * 1024)
|
||||||
|
flash_mb = int(TESTING_FLASH_SIZE, 16) // (1024 * 1024)
|
||||||
|
print(f" Patched memory segments: {', '.join(patches_applied)} (IRAM/DRAM: {iram_mb}MB, Flash: {flash_mb}MB)")
|
||||||
|
|
||||||
|
return content
|
||||||
|
|
||||||
|
|
||||||
|
def patch_linker_script_file(filepath, description):
|
||||||
|
"""Patch a linker script file in the build directory with enlarged memory segments.
|
||||||
|
|
||||||
|
This function modifies linker scripts in the build directory only (never SDK files).
|
||||||
|
It patches IRAM, DRAM, and Flash segments to allow larger builds in testing mode.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filepath: Path to the linker script file in the build directory
|
||||||
|
description: Human-readable description for logging
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if the file was patched, False if already patched or not found
|
||||||
|
"""
|
||||||
|
if not os.path.exists(filepath):
|
||||||
|
print(f"ESPHome: {description} not found at {filepath}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(f"ESPHome: Patching {description}...")
|
||||||
|
with open(filepath, "r") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
patched_content = apply_memory_patches(content)
|
||||||
|
|
||||||
|
if patched_content != content:
|
||||||
|
with open(filepath, "w") as f:
|
||||||
|
f.write(patched_content)
|
||||||
|
print(f"ESPHome: Successfully patched {description}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"ESPHome: {description} already patched or no changes needed")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def patch_local_linker_script(source, target, env):
|
||||||
|
"""Patch the local.eagle.app.v6.common.ld in build directory.
|
||||||
|
|
||||||
|
This patches the preprocessed linker script that PlatformIO creates in the build
|
||||||
|
directory, enlarging IRAM, DRAM, and Flash segments for testing mode.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source: SCons source nodes
|
||||||
|
target: SCons target nodes
|
||||||
|
env: SCons environment
|
||||||
|
"""
|
||||||
|
# Check if we're in testing mode
|
||||||
|
build_flags = env.get("BUILD_FLAGS", [])
|
||||||
|
testing_mode = any("-DESPHOME_TESTING_MODE" in flag for flag in build_flags)
|
||||||
|
|
||||||
|
if not testing_mode:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Patch the local linker script if it exists
|
||||||
|
build_dir = env.subst("$BUILD_DIR")
|
||||||
|
ld_dir = os.path.join(build_dir, "ld")
|
||||||
|
if os.path.exists(ld_dir):
|
||||||
|
local_ld = os.path.join(ld_dir, "local.eagle.app.v6.common.ld")
|
||||||
|
if os.path.exists(local_ld):
|
||||||
|
patch_linker_script_file(local_ld, "local.eagle.app.v6.common.ld")
|
||||||
|
|
||||||
|
|
||||||
|
# Check if we're in testing mode
|
||||||
|
build_flags = env.get("BUILD_FLAGS", [])
|
||||||
|
testing_mode = any("-DESPHOME_TESTING_MODE" in flag for flag in build_flags)
|
||||||
|
|
||||||
|
if testing_mode:
|
||||||
|
# Create a custom linker script in the build directory with patched memory limits
|
||||||
|
# This allows larger IRAM/DRAM/Flash for CI component grouping tests
|
||||||
|
build_dir = env.subst("$BUILD_DIR")
|
||||||
|
ldscript = env.GetProjectOption("board_build.ldscript", "")
|
||||||
|
assert ldscript, "No linker script configured in board_build.ldscript"
|
||||||
|
|
||||||
|
framework_dir = env.PioPlatform().get_package_dir("framework-arduinoespressif8266")
|
||||||
|
assert framework_dir is not None, "Could not find framework-arduinoespressif8266 package"
|
||||||
|
|
||||||
|
# Read the original SDK linker script (read-only, SDK is never modified)
|
||||||
|
sdk_ld = os.path.join(framework_dir, "tools", "sdk", "ld", ldscript)
|
||||||
|
# Create a custom version in the build directory (isolated, temporary)
|
||||||
|
custom_ld = os.path.join(build_dir, f"testing_{ldscript}")
|
||||||
|
|
||||||
|
if os.path.exists(sdk_ld) and not os.path.exists(custom_ld):
|
||||||
|
# Read the SDK linker script
|
||||||
|
with open(sdk_ld, "r") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Apply memory patches (IRAM: 2MB, DRAM: 2MB, Flash: 32MB)
|
||||||
|
patched_content = apply_memory_patches(content)
|
||||||
|
|
||||||
|
# Write the patched linker script to the build directory
|
||||||
|
with open(custom_ld, "w") as f:
|
||||||
|
f.write(patched_content)
|
||||||
|
|
||||||
|
print(f"ESPHome: Created custom linker script: {custom_ld}")
|
||||||
|
|
||||||
|
# Tell the linker to use our custom script from the build directory
|
||||||
|
assert os.path.exists(custom_ld), f"Custom linker script not found: {custom_ld}"
|
||||||
|
env.Replace(LDSCRIPT_PATH=custom_ld)
|
||||||
|
print(f"ESPHome: Using custom linker script with patched memory limits")
|
||||||
|
|
||||||
|
# Also patch local.eagle.app.v6.common.ld after PlatformIO creates it
|
||||||
|
env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", patch_local_linker_script)
|
||||||
@@ -103,7 +103,16 @@ def ota_esphome_final_validate(config):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = (
|
def _consume_ota_sockets(config: ConfigType) -> ConfigType:
|
||||||
|
"""Register socket needs for OTA component."""
|
||||||
|
from esphome.components import socket
|
||||||
|
|
||||||
|
# OTA needs 1 listening socket (client connections are temporary during updates)
|
||||||
|
socket.consume_sockets(1, "ota")(config)
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = cv.All(
|
||||||
cv.Schema(
|
cv.Schema(
|
||||||
{
|
{
|
||||||
cv.GenerateID(): cv.declare_id(ESPHomeOTAComponent),
|
cv.GenerateID(): cv.declare_id(ESPHomeOTAComponent),
|
||||||
@@ -130,7 +139,8 @@ CONFIG_SCHEMA = (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
.extend(BASE_OTA_SCHEMA)
|
.extend(BASE_OTA_SCHEMA)
|
||||||
.extend(cv.COMPONENT_SCHEMA)
|
.extend(cv.COMPONENT_SCHEMA),
|
||||||
|
_consume_ota_sockets,
|
||||||
)
|
)
|
||||||
|
|
||||||
FINAL_VALIDATE_SCHEMA = ota_esphome_final_validate
|
FINAL_VALIDATE_SCHEMA = ota_esphome_final_validate
|
||||||
|
|||||||
@@ -14,13 +14,13 @@ template<typename... Ts> class SendAction : public Action<Ts...>, public Parente
|
|||||||
TEMPLATABLE_VALUE(std::vector<uint8_t>, data);
|
TEMPLATABLE_VALUE(std::vector<uint8_t>, data);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
void add_on_sent(const std::vector<Action<Ts...> *> &actions) {
|
void add_on_sent(const std::initializer_list<Action<Ts...> *> &actions) {
|
||||||
this->sent_.add_actions(actions);
|
this->sent_.add_actions(actions);
|
||||||
if (this->flags_.wait_for_sent) {
|
if (this->flags_.wait_for_sent) {
|
||||||
this->sent_.add_action(new LambdaAction<Ts...>([this](Ts... x) { this->play_next_(x...); }));
|
this->sent_.add_action(new LambdaAction<Ts...>([this](Ts... x) { this->play_next_(x...); }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
void add_on_error(const std::vector<Action<Ts...> *> &actions) {
|
void add_on_error(const std::initializer_list<Action<Ts...> *> &actions) {
|
||||||
this->error_.add_actions(actions);
|
this->error_.add_actions(actions);
|
||||||
if (this->flags_.wait_for_sent) {
|
if (this->flags_.wait_for_sent) {
|
||||||
this->error_.add_action(new LambdaAction<Ts...>([this](Ts... x) {
|
this->error_.add_action(new LambdaAction<Ts...>([this](Ts... x) {
|
||||||
|
|||||||
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
|
|
||||||
@@ -67,7 +67,7 @@ void GPIOSwitch::write_state(bool state) {
|
|||||||
this->pin_->digital_write(state);
|
this->pin_->digital_write(state);
|
||||||
this->publish_state(state);
|
this->publish_state(state);
|
||||||
}
|
}
|
||||||
void GPIOSwitch::set_interlock(const std::vector<Switch *> &interlock) { this->interlock_ = interlock; }
|
void GPIOSwitch::set_interlock(const std::initializer_list<Switch *> &interlock) { this->interlock_ = interlock; }
|
||||||
|
|
||||||
} // namespace gpio
|
} // namespace gpio
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
|||||||
@@ -2,10 +2,9 @@
|
|||||||
|
|
||||||
#include "esphome/core/component.h"
|
#include "esphome/core/component.h"
|
||||||
#include "esphome/core/hal.h"
|
#include "esphome/core/hal.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/components/switch/switch.h"
|
#include "esphome/components/switch/switch.h"
|
||||||
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace gpio {
|
namespace gpio {
|
||||||
|
|
||||||
@@ -19,14 +18,14 @@ class GPIOSwitch : public switch_::Switch, public Component {
|
|||||||
|
|
||||||
void setup() override;
|
void setup() override;
|
||||||
void dump_config() override;
|
void dump_config() override;
|
||||||
void set_interlock(const std::vector<Switch *> &interlock);
|
void set_interlock(const std::initializer_list<Switch *> &interlock);
|
||||||
void set_interlock_wait_time(uint32_t interlock_wait_time) { interlock_wait_time_ = interlock_wait_time; }
|
void set_interlock_wait_time(uint32_t interlock_wait_time) { interlock_wait_time_ = interlock_wait_time; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void write_state(bool state) override;
|
void write_state(bool state) override;
|
||||||
|
|
||||||
GPIOPin *pin_;
|
GPIOPin *pin_;
|
||||||
std::vector<Switch *> interlock_;
|
FixedVector<Switch *> interlock_;
|
||||||
uint32_t interlock_wait_time_{0};
|
uint32_t interlock_wait_time_{0};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import re
|
|||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import __version__
|
from esphome.const import __version__
|
||||||
|
from esphome.cpp_generator import MockObj
|
||||||
|
from esphome.types import ConfigType
|
||||||
|
|
||||||
CODEOWNERS = ["@esphome/core"]
|
CODEOWNERS = ["@esphome/core"]
|
||||||
|
|
||||||
@@ -35,7 +37,9 @@ def _process_next_url(url: str):
|
|||||||
return url
|
return url
|
||||||
|
|
||||||
|
|
||||||
async def setup_improv_core(var, config):
|
async def setup_improv_core(var: MockObj, config: ConfigType, component: str):
|
||||||
if CONF_NEXT_URL in config:
|
if next_url := config.get(CONF_NEXT_URL):
|
||||||
cg.add(var.set_next_url(_process_next_url(config[CONF_NEXT_URL])))
|
cg.add(var.set_next_url(_process_next_url(next_url)))
|
||||||
|
cg.add_define(f"USE_{component.upper()}_NEXT_URL")
|
||||||
|
|
||||||
cg.add_library("improv/Improv", "1.2.4")
|
cg.add_library("improv/Improv", "1.2.4")
|
||||||
|
|||||||
@@ -2,36 +2,50 @@
|
|||||||
|
|
||||||
#include "esphome/components/network/util.h"
|
#include "esphome/components/network/util.h"
|
||||||
#include "esphome/core/application.h"
|
#include "esphome/core/application.h"
|
||||||
|
#include "esphome/core/defines.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace improv_base {
|
namespace improv_base {
|
||||||
|
|
||||||
|
#if defined(USE_ESP32_IMPROV_NEXT_URL) || defined(USE_IMPROV_SERIAL_NEXT_URL)
|
||||||
|
static constexpr const char DEVICE_NAME_PLACEHOLDER[] = "{{device_name}}";
|
||||||
|
static constexpr size_t DEVICE_NAME_PLACEHOLDER_LEN = sizeof(DEVICE_NAME_PLACEHOLDER) - 1;
|
||||||
|
static constexpr const char IP_ADDRESS_PLACEHOLDER[] = "{{ip_address}}";
|
||||||
|
static constexpr size_t IP_ADDRESS_PLACEHOLDER_LEN = sizeof(IP_ADDRESS_PLACEHOLDER) - 1;
|
||||||
|
|
||||||
|
static void replace_all_in_place(std::string &str, const char *placeholder, size_t placeholder_len,
|
||||||
|
const std::string &replacement) {
|
||||||
|
size_t pos = 0;
|
||||||
|
const size_t replacement_len = replacement.length();
|
||||||
|
while ((pos = str.find(placeholder, pos)) != std::string::npos) {
|
||||||
|
str.replace(pos, placeholder_len, replacement);
|
||||||
|
pos += replacement_len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
std::string ImprovBase::get_formatted_next_url_() {
|
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}}
|
||||||
for (auto &ip : network::get_ip_addresses()) {
|
replace_all_in_place(formatted_url, DEVICE_NAME_PLACEHOLDER, DEVICE_NAME_PLACEHOLDER_LEN, App.get_name());
|
||||||
if (ip.is_ip4()) {
|
|
||||||
std::string ipa = ip.str();
|
// Replace all occurrences of {{ip_address}}
|
||||||
copy.replace(pos, 14, ipa);
|
for (auto &ip : network::get_ip_addresses()) {
|
||||||
break;
|
if (ip.is_ip4()) {
|
||||||
}
|
replace_all_in_place(formatted_url, IP_ADDRESS_PLACEHOLDER, IP_ADDRESS_PLACEHOLDER_LEN, ip.str());
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return copy;
|
// Note: {{esphome_version}} is replaced at code generation time in Python
|
||||||
|
|
||||||
|
return formatted_url;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
} // namespace improv_base
|
} // namespace improv_base
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
|||||||
@@ -1,17 +1,22 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include "esphome/core/defines.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace improv_base {
|
namespace improv_base {
|
||||||
|
|
||||||
class ImprovBase {
|
class ImprovBase {
|
||||||
public:
|
public:
|
||||||
|
#if defined(USE_ESP32_IMPROV_NEXT_URL) || defined(USE_IMPROV_SERIAL_NEXT_URL)
|
||||||
void set_next_url(const std::string &next_url) { this->next_url_ = next_url; }
|
void set_next_url(const std::string &next_url) { this->next_url_ = next_url; }
|
||||||
|
#endif
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
#if defined(USE_ESP32_IMPROV_NEXT_URL) || defined(USE_IMPROV_SERIAL_NEXT_URL)
|
||||||
std::string get_formatted_next_url_();
|
std::string get_formatted_next_url_();
|
||||||
std::string next_url_;
|
std::string next_url_;
|
||||||
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace improv_base
|
} // namespace improv_base
|
||||||
|
|||||||
@@ -43,4 +43,4 @@ FINAL_VALIDATE_SCHEMA = validate_logger
|
|||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
var = cg.new_Pvariable(config[CONF_ID])
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
await cg.register_component(var, config)
|
await cg.register_component(var, config)
|
||||||
await improv_base.setup_improv_core(var, config)
|
await improv_base.setup_improv_core(var, config, "improv_serial")
|
||||||
|
|||||||
@@ -146,9 +146,11 @@ void ImprovSerialComponent::loop() {
|
|||||||
|
|
||||||
std::vector<uint8_t> ImprovSerialComponent::build_rpc_settings_response_(improv::Command command) {
|
std::vector<uint8_t> ImprovSerialComponent::build_rpc_settings_response_(improv::Command command) {
|
||||||
std::vector<std::string> urls;
|
std::vector<std::string> urls;
|
||||||
|
#ifdef USE_IMPROV_SERIAL_NEXT_URL
|
||||||
if (!this->next_url_.empty()) {
|
if (!this->next_url_.empty()) {
|
||||||
urls.push_back(this->get_formatted_next_url_());
|
urls.push_back(this->get_formatted_next_url_());
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
#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()) {
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ void AddressableLightTransformer::start() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
optional<LightColorValues> AddressableLightTransformer::apply() {
|
optional<LightColorValues> AddressableLightTransformer::apply() {
|
||||||
float smoothed_progress = LightTransitionTransformer::smoothed_progress(this->get_progress_());
|
float smoothed_progress = LightTransformer::smoothed_progress(this->get_progress_());
|
||||||
|
|
||||||
// When running an output-buffer modifying effect, don't try to transition individual LEDs, but instead just fade the
|
// When running an output-buffer modifying effect, don't try to transition individual LEDs, but instead just fade the
|
||||||
// LightColorValues. write_state() then picks up the change in brightness, and the color change is picked up by the
|
// LightColorValues. write_state() then picks up the change in brightness, and the color change is picked up by the
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
#include "esphome/core/defines.h"
|
#include "esphome/core/defines.h"
|
||||||
#include "light_output.h"
|
#include "light_output.h"
|
||||||
#include "light_state.h"
|
#include "light_state.h"
|
||||||
#include "transformers.h"
|
#include "light_transformer.h"
|
||||||
|
|
||||||
#ifdef USE_POWER_SUPPLY
|
#ifdef USE_POWER_SUPPLY
|
||||||
#include "esphome/components/power_supply/power_supply.h"
|
#include "esphome/components/power_supply/power_supply.h"
|
||||||
@@ -103,7 +103,7 @@ class AddressableLight : public LightOutput, public Component {
|
|||||||
bool effect_active_{false};
|
bool effect_active_{false};
|
||||||
};
|
};
|
||||||
|
|
||||||
class AddressableLightTransformer : public LightTransitionTransformer {
|
class AddressableLightTransformer : public LightTransformer {
|
||||||
public:
|
public:
|
||||||
AddressableLightTransformer(AddressableLight &light) : light_(light) {}
|
AddressableLightTransformer(AddressableLight &light) : light_(light) {}
|
||||||
|
|
||||||
|
|||||||
@@ -17,19 +17,19 @@ class ESPColorCorrection {
|
|||||||
this->color_correct_blue(color.blue), this->color_correct_white(color.white));
|
this->color_correct_blue(color.blue), this->color_correct_white(color.white));
|
||||||
}
|
}
|
||||||
inline uint8_t color_correct_red(uint8_t red) const ESPHOME_ALWAYS_INLINE {
|
inline uint8_t color_correct_red(uint8_t red) const ESPHOME_ALWAYS_INLINE {
|
||||||
uint8_t res = esp_scale8(esp_scale8(red, this->max_brightness_.red), this->local_brightness_);
|
uint8_t res = esp_scale8_twice(red, this->max_brightness_.red, this->local_brightness_);
|
||||||
return this->gamma_table_[res];
|
return this->gamma_table_[res];
|
||||||
}
|
}
|
||||||
inline uint8_t color_correct_green(uint8_t green) const ESPHOME_ALWAYS_INLINE {
|
inline uint8_t color_correct_green(uint8_t green) const ESPHOME_ALWAYS_INLINE {
|
||||||
uint8_t res = esp_scale8(esp_scale8(green, this->max_brightness_.green), this->local_brightness_);
|
uint8_t res = esp_scale8_twice(green, this->max_brightness_.green, this->local_brightness_);
|
||||||
return this->gamma_table_[res];
|
return this->gamma_table_[res];
|
||||||
}
|
}
|
||||||
inline uint8_t color_correct_blue(uint8_t blue) const ESPHOME_ALWAYS_INLINE {
|
inline uint8_t color_correct_blue(uint8_t blue) const ESPHOME_ALWAYS_INLINE {
|
||||||
uint8_t res = esp_scale8(esp_scale8(blue, this->max_brightness_.blue), this->local_brightness_);
|
uint8_t res = esp_scale8_twice(blue, this->max_brightness_.blue, this->local_brightness_);
|
||||||
return this->gamma_table_[res];
|
return this->gamma_table_[res];
|
||||||
}
|
}
|
||||||
inline uint8_t color_correct_white(uint8_t white) const ESPHOME_ALWAYS_INLINE {
|
inline uint8_t color_correct_white(uint8_t white) const ESPHOME_ALWAYS_INLINE {
|
||||||
uint8_t res = esp_scale8(esp_scale8(white, this->max_brightness_.white), this->local_brightness_);
|
uint8_t res = esp_scale8_twice(white, this->max_brightness_.white, this->local_brightness_);
|
||||||
return this->gamma_table_[res];
|
return this->gamma_table_[res];
|
||||||
}
|
}
|
||||||
inline Color color_uncorrect(Color color) const ESPHOME_ALWAYS_INLINE {
|
inline Color color_uncorrect(Color color) const ESPHOME_ALWAYS_INLINE {
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ class LightTransformer {
|
|||||||
const LightColorValues &get_target_values() const { return this->target_values_; }
|
const LightColorValues &get_target_values() const { return this->target_values_; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
// This looks crazy, but it reduces to 6x^5 - 15x^4 + 10x^3 which is just a smooth sigmoid-like
|
||||||
|
// transition from 0 to 1 on x = [0, 1]
|
||||||
|
static float smoothed_progress(float x) { return x * x * x * (x * (x * 6.0f - 15.0f) + 10.0f); }
|
||||||
|
|
||||||
/// The progress of this transition, on a scale of 0 to 1.
|
/// The progress of this transition, on a scale of 0 to 1.
|
||||||
float get_progress_() {
|
float get_progress_() {
|
||||||
uint32_t now = esphome::millis();
|
uint32_t now = esphome::millis();
|
||||||
|
|||||||
@@ -50,15 +50,11 @@ class LightTransitionTransformer : public LightTransformer {
|
|||||||
if (this->changing_color_mode_)
|
if (this->changing_color_mode_)
|
||||||
p = p < 0.5f ? p * 2 : (p - 0.5) * 2;
|
p = p < 0.5f ? p * 2 : (p - 0.5) * 2;
|
||||||
|
|
||||||
float v = LightTransitionTransformer::smoothed_progress(p);
|
float v = LightTransformer::smoothed_progress(p);
|
||||||
return LightColorValues::lerp(start, end, v);
|
return LightColorValues::lerp(start, end, v);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
// This looks crazy, but it reduces to 6x^5 - 15x^4 + 10x^3 which is just a smooth sigmoid-like
|
|
||||||
// transition from 0 to 1 on x = [0, 1]
|
|
||||||
static float smoothed_progress(float x) { return x * x * x * (x * (x * 6.0f - 15.0f) + 10.0f); }
|
|
||||||
|
|
||||||
LightColorValues end_values_{};
|
LightColorValues end_values_{};
|
||||||
LightColorValues intermediate_values_{};
|
LightColorValues intermediate_values_{};
|
||||||
bool changing_color_mode_{false};
|
bool changing_color_mode_{false};
|
||||||
|
|||||||
@@ -286,26 +286,24 @@ std::string LvSelectable::get_selected_text() {
|
|||||||
return this->options_[selected];
|
return this->options_[selected];
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::string join_string(std::vector<std::string> options) {
|
|
||||||
return std::accumulate(
|
|
||||||
options.begin(), options.end(), std::string(),
|
|
||||||
[](const std::string &a, const std::string &b) -> std::string { return a + (!a.empty() ? "\n" : "") + b; });
|
|
||||||
}
|
|
||||||
|
|
||||||
void LvSelectable::set_selected_text(const std::string &text, lv_anim_enable_t anim) {
|
void LvSelectable::set_selected_text(const std::string &text, lv_anim_enable_t anim) {
|
||||||
auto index = std::find(this->options_.begin(), this->options_.end(), text);
|
auto *index = std::find(this->options_.begin(), this->options_.end(), text);
|
||||||
if (index != this->options_.end()) {
|
if (index != this->options_.end()) {
|
||||||
this->set_selected_index(index - this->options_.begin(), anim);
|
this->set_selected_index(index - this->options_.begin(), anim);
|
||||||
lv_event_send(this->obj, lv_api_event, nullptr);
|
lv_event_send(this->obj, lv_api_event, nullptr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void LvSelectable::set_options(std::vector<std::string> options) {
|
void LvSelectable::set_options(const std::initializer_list<std::string> &options) {
|
||||||
auto index = this->get_selected_index();
|
auto index = this->get_selected_index();
|
||||||
if (index >= options.size())
|
if (index >= options.size())
|
||||||
index = options.size() - 1;
|
index = options.size() - 1;
|
||||||
this->options_ = std::move(options);
|
// Join strings directly from initializer_list to avoid double iteration
|
||||||
this->set_option_string(join_string(this->options_).c_str());
|
std::string joined = std::accumulate(
|
||||||
|
options.begin(), options.end(), std::string(),
|
||||||
|
[](const std::string &a, const std::string &b) -> std::string { return a + (!a.empty() ? "\n" : "") + b; });
|
||||||
|
this->options_ = options;
|
||||||
|
this->set_option_string(joined.c_str());
|
||||||
lv_event_send(this->obj, LV_EVENT_REFRESH, nullptr);
|
lv_event_send(this->obj, LV_EVENT_REFRESH, nullptr);
|
||||||
this->set_selected_index(index, LV_ANIM_OFF);
|
this->set_selected_index(index, LV_ANIM_OFF);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -358,12 +358,12 @@ class LvSelectable : public LvCompound {
|
|||||||
virtual void set_selected_index(size_t index, lv_anim_enable_t anim) = 0;
|
virtual void set_selected_index(size_t index, lv_anim_enable_t anim) = 0;
|
||||||
void set_selected_text(const std::string &text, lv_anim_enable_t anim);
|
void set_selected_text(const std::string &text, lv_anim_enable_t anim);
|
||||||
std::string get_selected_text();
|
std::string get_selected_text();
|
||||||
std::vector<std::string> get_options() { return this->options_; }
|
const FixedVector<std::string> &get_options() { return this->options_; }
|
||||||
void set_options(std::vector<std::string> options);
|
void set_options(const std::initializer_list<std::string> &options);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual void set_option_string(const char *options) = 0;
|
virtual void set_option_string(const char *options) = 0;
|
||||||
std::vector<std::string> options_{};
|
FixedVector<std::string> options_{};
|
||||||
};
|
};
|
||||||
|
|
||||||
#ifdef USE_LVGL_DROPDOWN
|
#ifdef USE_LVGL_DROPDOWN
|
||||||
|
|||||||
@@ -53,7 +53,10 @@ class LVGLSelect : public select::Select, public Component {
|
|||||||
this->widget_->set_selected_text(value, this->anim_);
|
this->widget_->set_selected_text(value, this->anim_);
|
||||||
this->publish();
|
this->publish();
|
||||||
}
|
}
|
||||||
void set_options_() { this->traits.set_options(this->widget_->get_options()); }
|
void set_options_() {
|
||||||
|
// Copy options from lvgl widget to select traits
|
||||||
|
this->traits.copy_options(this->widget_->get_options());
|
||||||
|
}
|
||||||
|
|
||||||
LvSelectable *widget_;
|
LvSelectable *widget_;
|
||||||
lv_anim_enable_t anim_;
|
lv_anim_enable_t anim_;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from esphome.const import (
|
|||||||
)
|
)
|
||||||
from esphome.core import CORE, Lambda, coroutine_with_priority
|
from esphome.core import CORE, Lambda, coroutine_with_priority
|
||||||
from esphome.coroutine import CoroPriority
|
from esphome.coroutine import CoroPriority
|
||||||
|
from esphome.types import ConfigType
|
||||||
|
|
||||||
CODEOWNERS = ["@esphome/core"]
|
CODEOWNERS = ["@esphome/core"]
|
||||||
DEPENDENCIES = ["network"]
|
DEPENDENCIES = ["network"]
|
||||||
@@ -46,6 +47,19 @@ SERVICE_SCHEMA = cv.Schema(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _consume_mdns_sockets(config: ConfigType) -> ConfigType:
|
||||||
|
"""Register socket needs for mDNS component."""
|
||||||
|
if config.get(CONF_DISABLED):
|
||||||
|
return config
|
||||||
|
|
||||||
|
from esphome.components import socket
|
||||||
|
|
||||||
|
# mDNS needs 2 sockets (IPv4 + IPv6 multicast)
|
||||||
|
socket.consume_sockets(2, "mdns")(config)
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.All(
|
CONFIG_SCHEMA = cv.All(
|
||||||
cv.Schema(
|
cv.Schema(
|
||||||
{
|
{
|
||||||
@@ -55,6 +69,7 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
_remove_id_if_disabled,
|
_remove_id_if_disabled,
|
||||||
|
_consume_mdns_sockets,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ from esphome.const import (
|
|||||||
PlatformFramework,
|
PlatformFramework,
|
||||||
)
|
)
|
||||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||||
|
from esphome.types import ConfigType
|
||||||
|
|
||||||
DEPENDENCIES = ["network"]
|
DEPENDENCIES = ["network"]
|
||||||
|
|
||||||
@@ -210,6 +211,15 @@ def validate_fingerprint(value):
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _consume_mqtt_sockets(config: ConfigType) -> ConfigType:
|
||||||
|
"""Register socket needs for MQTT component."""
|
||||||
|
from esphome.components import socket
|
||||||
|
|
||||||
|
# MQTT needs 1 socket for the broker connection
|
||||||
|
socket.consume_sockets(1, "mqtt")(config)
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.All(
|
CONFIG_SCHEMA = cv.All(
|
||||||
cv.Schema(
|
cv.Schema(
|
||||||
{
|
{
|
||||||
@@ -306,6 +316,7 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
),
|
),
|
||||||
validate_config,
|
validate_config,
|
||||||
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX]),
|
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX]),
|
||||||
|
_consume_mqtt_sockets,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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>();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from esphome.components.esp32 import (
|
|||||||
VARIANT_ESP32H2,
|
VARIANT_ESP32H2,
|
||||||
add_idf_sdkconfig_option,
|
add_idf_sdkconfig_option,
|
||||||
only_on_variant,
|
only_on_variant,
|
||||||
|
require_vfs_select,
|
||||||
)
|
)
|
||||||
from esphome.components.mdns import MDNSComponent, enable_mdns_storage
|
from esphome.components.mdns import MDNSComponent, enable_mdns_storage
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
@@ -106,6 +107,14 @@ _CONNECTION_SCHEMA = cv.Schema(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _require_vfs_select(config):
|
||||||
|
"""Register VFS select requirement during config validation."""
|
||||||
|
# OpenThread uses esp_vfs_eventfd which requires VFS select support
|
||||||
|
require_vfs_select()
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.All(
|
CONFIG_SCHEMA = cv.All(
|
||||||
cv.Schema(
|
cv.Schema(
|
||||||
{
|
{
|
||||||
@@ -122,6 +131,7 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
cv.has_exactly_one_key(CONF_NETWORK_KEY, CONF_TLV),
|
cv.has_exactly_one_key(CONF_NETWORK_KEY, CONF_TLV),
|
||||||
cv.only_with_esp_idf,
|
cv.only_with_esp_idf,
|
||||||
only_on_variant(supported=[VARIANT_ESP32C6, VARIANT_ESP32H2]),
|
only_on_variant(supported=[VARIANT_ESP32C6, VARIANT_ESP32H2]),
|
||||||
|
_require_vfs_select,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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_);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace select {
|
namespace select {
|
||||||
|
|
||||||
void SelectTraits::set_options(std::vector<std::string> options) { this->options_ = std::move(options); }
|
void SelectTraits::set_options(const std::initializer_list<std::string> &options) { this->options_ = options; }
|
||||||
|
|
||||||
const std::vector<std::string> &SelectTraits::get_options() const { return this->options_; }
|
const FixedVector<std::string> &SelectTraits::get_options() const { return this->options_; }
|
||||||
|
|
||||||
} // namespace select
|
} // namespace select
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <vector>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <initializer_list>
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace select {
|
namespace select {
|
||||||
|
|
||||||
class SelectTraits {
|
class SelectTraits {
|
||||||
public:
|
public:
|
||||||
void set_options(std::vector<std::string> options);
|
void set_options(const std::initializer_list<std::string> &options);
|
||||||
const std::vector<std::string> &get_options() const;
|
const FixedVector<std::string> &get_options() const;
|
||||||
|
/// Copy options from another SelectTraits (for copy_select, lvgl)
|
||||||
|
void copy_options(const FixedVector<std::string> &other) { this->options_.copy_from(other); }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
std::vector<std::string> options_;
|
FixedVector<std::string> options_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace select
|
} // namespace select
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ from esphome.const import (
|
|||||||
CONF_ON_RAW_VALUE,
|
CONF_ON_RAW_VALUE,
|
||||||
CONF_ON_VALUE,
|
CONF_ON_VALUE,
|
||||||
CONF_ON_VALUE_RANGE,
|
CONF_ON_VALUE_RANGE,
|
||||||
|
CONF_OPTIMISTIC,
|
||||||
|
CONF_PERIOD,
|
||||||
CONF_QUANTILE,
|
CONF_QUANTILE,
|
||||||
CONF_SEND_EVERY,
|
CONF_SEND_EVERY,
|
||||||
CONF_SEND_FIRST_AT,
|
CONF_SEND_FIRST_AT,
|
||||||
@@ -261,9 +263,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)
|
||||||
@@ -641,10 +646,29 @@ async def throttle_with_priority_filter_to_code(config, filter_id):
|
|||||||
return cg.new_Pvariable(filter_id, config[CONF_TIMEOUT], template_)
|
return cg.new_Pvariable(filter_id, config[CONF_TIMEOUT], template_)
|
||||||
|
|
||||||
|
|
||||||
|
HEARTBEAT_SCHEMA = cv.Schema(
|
||||||
|
{
|
||||||
|
cv.Required(CONF_PERIOD): cv.positive_time_period_milliseconds,
|
||||||
|
cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@FILTER_REGISTRY.register(
|
@FILTER_REGISTRY.register(
|
||||||
"heartbeat", HeartbeatFilter, cv.positive_time_period_milliseconds
|
"heartbeat",
|
||||||
|
HeartbeatFilter,
|
||||||
|
cv.Any(
|
||||||
|
cv.positive_time_period_milliseconds,
|
||||||
|
HEARTBEAT_SCHEMA,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
async def heartbeat_filter_to_code(config, filter_id):
|
async def heartbeat_filter_to_code(config, filter_id):
|
||||||
|
if isinstance(config, dict):
|
||||||
|
var = cg.new_Pvariable(filter_id, config[CONF_PERIOD])
|
||||||
|
await cg.register_component(var, {})
|
||||||
|
cg.add(var.set_optimistic(config[CONF_OPTIMISTIC]))
|
||||||
|
return var
|
||||||
|
|
||||||
var = cg.new_Pvariable(filter_id, config)
|
var = cg.new_Pvariable(filter_id, config)
|
||||||
await cg.register_component(var, {})
|
await cg.register_component(var, {})
|
||||||
return var
|
return var
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -318,7 +313,7 @@ optional<float> DeltaFilter::new_value(float value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// OrFilter
|
// OrFilter
|
||||||
OrFilter::OrFilter(std::vector<Filter *> filters) : filters_(std::move(filters)), phi_(this) {}
|
OrFilter::OrFilter(std::initializer_list<Filter *> filters) : filters_(filters), phi_(this) {}
|
||||||
OrFilter::PhiNode::PhiNode(OrFilter *or_parent) : or_parent_(or_parent) {}
|
OrFilter::PhiNode::PhiNode(OrFilter *or_parent) : or_parent_(or_parent) {}
|
||||||
|
|
||||||
optional<float> OrFilter::PhiNode::new_value(float value) {
|
optional<float> OrFilter::PhiNode::new_value(float value) {
|
||||||
@@ -331,14 +326,14 @@ optional<float> OrFilter::PhiNode::new_value(float value) {
|
|||||||
}
|
}
|
||||||
optional<float> OrFilter::new_value(float value) {
|
optional<float> OrFilter::new_value(float value) {
|
||||||
this->has_value_ = false;
|
this->has_value_ = false;
|
||||||
for (Filter *filter : this->filters_)
|
for (auto *filter : this->filters_)
|
||||||
filter->input(value);
|
filter->input(value);
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
void OrFilter::initialize(Sensor *parent, Filter *next) {
|
void OrFilter::initialize(Sensor *parent, Filter *next) {
|
||||||
Filter::initialize(parent, next);
|
Filter::initialize(parent, next);
|
||||||
for (Filter *filter : this->filters_) {
|
for (auto *filter : this->filters_) {
|
||||||
filter->initialize(parent, &this->phi_);
|
filter->initialize(parent, &this->phi_);
|
||||||
}
|
}
|
||||||
this->phi_.initialize(parent, nullptr);
|
this->phi_.initialize(parent, nullptr);
|
||||||
@@ -377,8 +372,12 @@ optional<float> HeartbeatFilter::new_value(float value) {
|
|||||||
this->last_input_ = value;
|
this->last_input_ = value;
|
||||||
this->has_value_ = true;
|
this->has_value_ = true;
|
||||||
|
|
||||||
|
if (this->optimistic_) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
void HeartbeatFilter::setup() {
|
void HeartbeatFilter::setup() {
|
||||||
this->set_interval("heartbeat", this->time_period_, [this]() {
|
this->set_interval("heartbeat", this->time_period_, [this]() {
|
||||||
ESP_LOGVV(TAG, "HeartbeatFilter(%p)::interval(has_value=%s, last_input=%f)", this, YESNO(this->has_value_),
|
ESP_LOGVV(TAG, "HeartbeatFilter(%p)::interval(has_value=%s, last_input=%f)", this, YESNO(this->has_value_),
|
||||||
@@ -389,20 +388,27 @@ void HeartbeatFilter::setup() {
|
|||||||
this->output(this->last_input_);
|
this->output(this->last_input_);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
float HeartbeatFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
|
float HeartbeatFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
|
||||||
|
|
||||||
|
CalibrateLinearFilter::CalibrateLinearFilter(std::initializer_list<std::array<float, 3>> linear_functions)
|
||||||
|
: linear_functions_(linear_functions) {}
|
||||||
|
|
||||||
optional<float> CalibrateLinearFilter::new_value(float value) {
|
optional<float> CalibrateLinearFilter::new_value(float value) {
|
||||||
for (std::array<float, 3> f : this->linear_functions_) {
|
for (const auto &f : this->linear_functions_) {
|
||||||
if (!std::isfinite(f[2]) || value < f[2])
|
if (!std::isfinite(f[2]) || value < f[2])
|
||||||
return (value * f[0]) + f[1];
|
return (value * f[0]) + f[1];
|
||||||
}
|
}
|
||||||
return NAN;
|
return NAN;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CalibratePolynomialFilter::CalibratePolynomialFilter(std::initializer_list<float> coefficients)
|
||||||
|
: coefficients_(coefficients) {}
|
||||||
|
|
||||||
optional<float> CalibratePolynomialFilter::new_value(float value) {
|
optional<float> CalibratePolynomialFilter::new_value(float value) {
|
||||||
float res = 0.0f;
|
float res = 0.0f;
|
||||||
float x = 1.0f;
|
float x = 1.0f;
|
||||||
for (float coefficient : this->coefficients_) {
|
for (const auto &coefficient : this->coefficients_) {
|
||||||
res += x * coefficient;
|
res += x * coefficient;
|
||||||
x *= value;
|
x *= 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 {
|
||||||
@@ -384,15 +396,16 @@ class HeartbeatFilter : public Filter, public Component {
|
|||||||
explicit HeartbeatFilter(uint32_t time_period);
|
explicit HeartbeatFilter(uint32_t time_period);
|
||||||
|
|
||||||
void setup() override;
|
void setup() override;
|
||||||
|
|
||||||
optional<float> new_value(float value) override;
|
optional<float> new_value(float value) override;
|
||||||
|
|
||||||
float get_setup_priority() const override;
|
float get_setup_priority() const override;
|
||||||
|
|
||||||
|
void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
uint32_t time_period_;
|
uint32_t time_period_;
|
||||||
float last_input_;
|
float last_input_;
|
||||||
bool has_value_{false};
|
bool has_value_{false};
|
||||||
|
bool optimistic_{false};
|
||||||
};
|
};
|
||||||
|
|
||||||
class DeltaFilter : public Filter {
|
class DeltaFilter : public Filter {
|
||||||
@@ -410,7 +423,7 @@ class DeltaFilter : public Filter {
|
|||||||
|
|
||||||
class OrFilter : public Filter {
|
class OrFilter : public Filter {
|
||||||
public:
|
public:
|
||||||
explicit OrFilter(std::vector<Filter *> filters);
|
explicit OrFilter(std::initializer_list<Filter *> filters);
|
||||||
|
|
||||||
void initialize(Sensor *parent, Filter *next) override;
|
void initialize(Sensor *parent, Filter *next) override;
|
||||||
|
|
||||||
@@ -426,28 +439,27 @@ class OrFilter : public Filter {
|
|||||||
OrFilter *or_parent_;
|
OrFilter *or_parent_;
|
||||||
};
|
};
|
||||||
|
|
||||||
std::vector<Filter *> filters_;
|
FixedVector<Filter *> filters_;
|
||||||
PhiNode phi_;
|
PhiNode phi_;
|
||||||
bool has_value_{false};
|
bool has_value_{false};
|
||||||
};
|
};
|
||||||
|
|
||||||
class CalibrateLinearFilter : public Filter {
|
class CalibrateLinearFilter : public Filter {
|
||||||
public:
|
public:
|
||||||
CalibrateLinearFilter(std::vector<std::array<float, 3>> linear_functions)
|
explicit CalibrateLinearFilter(std::initializer_list<std::array<float, 3>> linear_functions);
|
||||||
: linear_functions_(std::move(linear_functions)) {}
|
|
||||||
optional<float> new_value(float value) override;
|
optional<float> new_value(float value) override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
std::vector<std::array<float, 3>> linear_functions_;
|
FixedVector<std::array<float, 3>> linear_functions_;
|
||||||
};
|
};
|
||||||
|
|
||||||
class CalibratePolynomialFilter : public Filter {
|
class CalibratePolynomialFilter : public Filter {
|
||||||
public:
|
public:
|
||||||
CalibratePolynomialFilter(std::vector<float> coefficients) : coefficients_(std::move(coefficients)) {}
|
explicit CalibratePolynomialFilter(std::initializer_list<float> coefficients);
|
||||||
optional<float> new_value(float value) override;
|
optional<float> new_value(float value) override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
std::vector<float> coefficients_;
|
FixedVector<float> coefficients_;
|
||||||
};
|
};
|
||||||
|
|
||||||
class ClampFilter : public Filter {
|
class ClampFilter : public Filter {
|
||||||
|
|||||||
@@ -107,12 +107,12 @@ void Sensor::add_filter(Filter *filter) {
|
|||||||
}
|
}
|
||||||
filter->initialize(this, nullptr);
|
filter->initialize(this, nullptr);
|
||||||
}
|
}
|
||||||
void Sensor::add_filters(const std::vector<Filter *> &filters) {
|
void Sensor::add_filters(std::initializer_list<Filter *> filters) {
|
||||||
for (Filter *filter : filters) {
|
for (Filter *filter : filters) {
|
||||||
this->add_filter(filter);
|
this->add_filter(filter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
void Sensor::set_filters(const std::vector<Filter *> &filters) {
|
void Sensor::set_filters(std::initializer_list<Filter *> filters) {
|
||||||
this->clear_filters();
|
this->clear_filters();
|
||||||
this->add_filters(filters);
|
this->add_filters(filters);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
#include "esphome/components/sensor/filter.h"
|
#include "esphome/components/sensor/filter.h"
|
||||||
|
|
||||||
#include <vector>
|
#include <initializer_list>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
@@ -77,10 +77,10 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa
|
|||||||
* SlidingWindowMovingAverageFilter(15, 15), // average over last 15 values
|
* SlidingWindowMovingAverageFilter(15, 15), // average over last 15 values
|
||||||
* });
|
* });
|
||||||
*/
|
*/
|
||||||
void add_filters(const std::vector<Filter *> &filters);
|
void add_filters(std::initializer_list<Filter *> filters);
|
||||||
|
|
||||||
/// Clear the filters and replace them by filters.
|
/// Clear the filters and replace them by filters.
|
||||||
void set_filters(const std::vector<Filter *> &filters);
|
void set_filters(std::initializer_list<Filter *> filters);
|
||||||
|
|
||||||
/// Clear the entire filter chain.
|
/// Clear the entire filter chain.
|
||||||
void clear_filters();
|
void clear_filters();
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from collections.abc import Callable, MutableMapping
|
||||||
|
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.core import CORE
|
from esphome.core import CORE
|
||||||
@@ -9,6 +11,32 @@ IMPLEMENTATION_LWIP_TCP = "lwip_tcp"
|
|||||||
IMPLEMENTATION_LWIP_SOCKETS = "lwip_sockets"
|
IMPLEMENTATION_LWIP_SOCKETS = "lwip_sockets"
|
||||||
IMPLEMENTATION_BSD_SOCKETS = "bsd_sockets"
|
IMPLEMENTATION_BSD_SOCKETS = "bsd_sockets"
|
||||||
|
|
||||||
|
# Socket tracking infrastructure
|
||||||
|
# Components register their socket needs and platforms read this to configure appropriately
|
||||||
|
KEY_SOCKET_CONSUMERS = "socket_consumers"
|
||||||
|
|
||||||
|
|
||||||
|
def consume_sockets(
|
||||||
|
value: int, consumer: str
|
||||||
|
) -> Callable[[MutableMapping], MutableMapping]:
|
||||||
|
"""Register socket usage for a component.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value: Number of sockets needed by the component
|
||||||
|
consumer: Name of the component consuming the sockets
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A validator function that records the socket usage
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _consume_sockets(config: MutableMapping) -> MutableMapping:
|
||||||
|
consumers: dict[str, int] = CORE.data.setdefault(KEY_SOCKET_CONSUMERS, {})
|
||||||
|
consumers[consumer] = consumers.get(consumer, 0) + value
|
||||||
|
return config
|
||||||
|
|
||||||
|
return _consume_sockets
|
||||||
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.Schema(
|
CONFIG_SCHEMA = cv.Schema(
|
||||||
{
|
{
|
||||||
cv.SplitDefault(
|
cv.SplitDefault(
|
||||||
|
|||||||
@@ -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,27 @@ 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(const 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(const 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(const 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(const 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
|
||||||
|
|||||||
@@ -51,12 +51,12 @@ void TextSensor::add_filter(Filter *filter) {
|
|||||||
}
|
}
|
||||||
filter->initialize(this, nullptr);
|
filter->initialize(this, nullptr);
|
||||||
}
|
}
|
||||||
void TextSensor::add_filters(const std::vector<Filter *> &filters) {
|
void TextSensor::add_filters(std::initializer_list<Filter *> filters) {
|
||||||
for (Filter *filter : filters) {
|
for (Filter *filter : filters) {
|
||||||
this->add_filter(filter);
|
this->add_filter(filter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
void TextSensor::set_filters(const std::vector<Filter *> &filters) {
|
void TextSensor::set_filters(std::initializer_list<Filter *> filters) {
|
||||||
this->clear_filters();
|
this->clear_filters();
|
||||||
this->add_filters(filters);
|
this->add_filters(filters);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/components/text_sensor/filter.h"
|
#include "esphome/components/text_sensor/filter.h"
|
||||||
|
|
||||||
#include <vector>
|
#include <initializer_list>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
@@ -37,10 +37,10 @@ class TextSensor : public EntityBase, public EntityBase_DeviceClass {
|
|||||||
void add_filter(Filter *filter);
|
void add_filter(Filter *filter);
|
||||||
|
|
||||||
/// Add a list of vectors to the back of the filter chain.
|
/// Add a list of vectors to the back of the filter chain.
|
||||||
void add_filters(const std::vector<Filter *> &filters);
|
void add_filters(std::initializer_list<Filter *> filters);
|
||||||
|
|
||||||
/// Clear the filters and replace them by filters.
|
/// Clear the filters and replace them by filters.
|
||||||
void set_filters(const std::vector<Filter *> &filters);
|
void set_filters(std::initializer_list<Filter *> filters);
|
||||||
|
|
||||||
/// Clear the entire filter chain.
|
/// Clear the entire filter chain.
|
||||||
void clear_filters();
|
void clear_filters();
|
||||||
|
|||||||
@@ -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_)
|
||||||
|
|||||||
@@ -56,6 +56,13 @@ uint32_t ESP8266UartComponent::get_config() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ESP8266UartComponent::setup() {
|
void ESP8266UartComponent::setup() {
|
||||||
|
if (this->rx_pin_) {
|
||||||
|
this->rx_pin_->setup();
|
||||||
|
}
|
||||||
|
if (this->tx_pin_ && this->rx_pin_ != this->tx_pin_) {
|
||||||
|
this->tx_pin_->setup();
|
||||||
|
}
|
||||||
|
|
||||||
// Use Arduino HardwareSerial UARTs if all used pins match the ones
|
// Use Arduino HardwareSerial UARTs if all used pins match the ones
|
||||||
// preconfigured by the platform. For example if RX disabled but TX pin
|
// preconfigured by the platform. For example if RX disabled but TX pin
|
||||||
// is 1 we still want to use Serial.
|
// is 1 we still want to use Serial.
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
#include "esphome/core/defines.h"
|
#include "esphome/core/defines.h"
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
#include "esphome/core/gpio.h"
|
||||||
|
#include "driver/gpio.h"
|
||||||
|
#include "soc/gpio_num.h"
|
||||||
|
|
||||||
#ifdef USE_LOGGER
|
#ifdef USE_LOGGER
|
||||||
#include "esphome/components/logger/logger.h"
|
#include "esphome/components/logger/logger.h"
|
||||||
@@ -104,6 +107,13 @@ void IDFUARTComponent::load_settings(bool dump_config) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this->rx_pin_) {
|
||||||
|
this->rx_pin_->setup();
|
||||||
|
}
|
||||||
|
if (this->tx_pin_ && this->rx_pin_ != this->tx_pin_) {
|
||||||
|
this->tx_pin_->setup();
|
||||||
|
}
|
||||||
|
|
||||||
int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1;
|
int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1;
|
||||||
int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1;
|
int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1;
|
||||||
int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1;
|
int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1;
|
||||||
|
|||||||
@@ -46,6 +46,13 @@ uint16_t LibreTinyUARTComponent::get_config() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void LibreTinyUARTComponent::setup() {
|
void LibreTinyUARTComponent::setup() {
|
||||||
|
if (this->rx_pin_) {
|
||||||
|
this->rx_pin_->setup();
|
||||||
|
}
|
||||||
|
if (this->tx_pin_ && this->rx_pin_ != this->tx_pin_) {
|
||||||
|
this->tx_pin_->setup();
|
||||||
|
}
|
||||||
|
|
||||||
int8_t tx_pin = tx_pin_ == nullptr ? -1 : tx_pin_->get_pin();
|
int8_t tx_pin = tx_pin_ == nullptr ? -1 : tx_pin_->get_pin();
|
||||||
int8_t rx_pin = rx_pin_ == nullptr ? -1 : rx_pin_->get_pin();
|
int8_t rx_pin = rx_pin_ == nullptr ? -1 : rx_pin_->get_pin();
|
||||||
bool tx_inverted = tx_pin_ != nullptr && tx_pin_->is_inverted();
|
bool tx_inverted = tx_pin_ != nullptr && tx_pin_->is_inverted();
|
||||||
|
|||||||
@@ -52,6 +52,13 @@ uint16_t RP2040UartComponent::get_config() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void RP2040UartComponent::setup() {
|
void RP2040UartComponent::setup() {
|
||||||
|
if (this->rx_pin_) {
|
||||||
|
this->rx_pin_->setup();
|
||||||
|
}
|
||||||
|
if (this->tx_pin_ && this->rx_pin_ != this->tx_pin_) {
|
||||||
|
this->tx_pin_->setup();
|
||||||
|
}
|
||||||
|
|
||||||
uint16_t config = get_config();
|
uint16_t config = get_config();
|
||||||
|
|
||||||
constexpr uint32_t valid_tx_uart_0 = __bitset({0, 12, 16, 28});
|
constexpr uint32_t valid_tx_uart_0 = __bitset({0, 12, 16, 28});
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user