mirror of
https://github.com/esphome/esphome.git
synced 2025-11-03 16:41:50 +00:00
Compare commits
84 Commits
ci_impact_
...
fix_clang_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
326405ae8f | ||
|
|
c9e166905f | ||
|
|
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 | ||
|
|
0f87e7508b | ||
|
|
862bbb7fe1 | ||
|
|
020cea80b2 | ||
|
|
9c146a7070 | ||
|
|
afbd3f77af | ||
|
|
1e1fefbd0a | ||
|
|
1a2057df30 | ||
|
|
87ca8784ef | ||
|
|
a186c1062f | ||
|
|
ea38237f29 | ||
|
|
6aff1394ad | ||
|
|
0e34d1b64d | ||
|
|
1483cee0fb | ||
|
|
8c1bd2fd85 | ||
|
|
ea609dc0f6 | ||
|
|
913095f6be | ||
|
|
bb24ad4a30 | ||
|
|
0d612fecfc | ||
|
|
9c235b4140 | ||
|
|
70cb1793f3 | ||
|
|
3bdd351d49 | ||
|
|
b0ea3f57de | ||
|
|
c9312d5c27 | ||
|
|
33fea90c19 | ||
|
|
25f3b6a959 | ||
|
|
e993312640 | ||
|
|
85babe85e4 | ||
|
|
0266c897c9 | ||
|
|
bda7676e3a | ||
|
|
57e98ec3fc | ||
|
|
09b2ad071b | ||
|
|
fdecda3d65 | ||
|
|
a0922bc8b0 | ||
|
|
f25af18655 | ||
|
|
5db07c2d70 | ||
|
|
40823df7bc | ||
|
|
5e1019a6fa | ||
|
|
f3cdbd0a05 | ||
|
|
ddf1b67e49 | ||
|
|
b4d9fddd07 | ||
|
|
25f03074ab | ||
|
|
590f6ff70b | ||
|
|
a33ed5e47b | ||
|
|
c11a9bb97f | ||
|
|
acef2085d9 | ||
|
|
865663ce5f | ||
|
|
ae010fd6f1 | ||
|
|
91a10d0e36 | ||
|
|
d5c36eaf2a |
111
.github/workflows/ci-memory-impact-comment.yml
vendored
Normal file
111
.github/workflows/ci-memory-impact-comment.yml
vendored
Normal file
@@ -0,0 +1,111 @@
|
||||
---
|
||||
name: Memory Impact Comment (Forks)
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["CI"]
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
memory-impact-comment:
|
||||
name: Post memory impact comment (fork PRs only)
|
||||
runs-on: ubuntu-24.04
|
||||
# Only run for PRs from forks that had successful CI runs
|
||||
if: >
|
||||
github.event.workflow_run.event == 'pull_request' &&
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.head_repository.full_name != github.repository
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- name: Get PR details
|
||||
id: pr
|
||||
run: |
|
||||
# Get PR details by searching for PR with matching head SHA
|
||||
# The workflow_run.pull_requests field is often empty for forks
|
||||
# Use paginate to handle repos with many open PRs
|
||||
head_sha="${{ github.event.workflow_run.head_sha }}"
|
||||
pr_data=$(gh api --paginate "/repos/${{ github.repository }}/pulls" \
|
||||
--jq ".[] | select(.head.sha == \"$head_sha\") | {number: .number, base_ref: .base.ref}" \
|
||||
| head -n 1)
|
||||
|
||||
if [ -z "$pr_data" ]; then
|
||||
echo "No PR found for SHA $head_sha, skipping"
|
||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
pr_number=$(echo "$pr_data" | jq -r '.number')
|
||||
base_ref=$(echo "$pr_data" | jq -r '.base_ref')
|
||||
|
||||
echo "pr_number=$pr_number" >> "$GITHUB_OUTPUT"
|
||||
echo "base_ref=$base_ref" >> "$GITHUB_OUTPUT"
|
||||
echo "Found PR #$pr_number targeting base branch: $base_ref"
|
||||
|
||||
- name: Check out code from base repository
|
||||
if: steps.pr.outputs.skip != 'true'
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
# Always check out from the base repository (esphome/esphome), never from forks
|
||||
# Use the PR's target branch to ensure we run trusted code from the main repo
|
||||
repository: ${{ github.repository }}
|
||||
ref: ${{ steps.pr.outputs.base_ref }}
|
||||
|
||||
- name: Restore Python
|
||||
if: steps.pr.outputs.skip != 'true'
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
python-version: "3.11"
|
||||
cache-key: ${{ hashFiles('.cache-key') }}
|
||||
|
||||
- name: Download memory analysis artifacts
|
||||
if: steps.pr.outputs.skip != 'true'
|
||||
run: |
|
||||
run_id="${{ github.event.workflow_run.id }}"
|
||||
echo "Downloading artifacts from workflow run $run_id"
|
||||
|
||||
mkdir -p memory-analysis
|
||||
|
||||
# Download target analysis artifact
|
||||
if gh run download --name "memory-analysis-target" --dir memory-analysis --repo "${{ github.repository }}" "$run_id"; then
|
||||
echo "Downloaded memory-analysis-target artifact."
|
||||
else
|
||||
echo "No memory-analysis-target artifact found."
|
||||
fi
|
||||
|
||||
# Download PR analysis artifact
|
||||
if gh run download --name "memory-analysis-pr" --dir memory-analysis --repo "${{ github.repository }}" "$run_id"; then
|
||||
echo "Downloaded memory-analysis-pr artifact."
|
||||
else
|
||||
echo "No memory-analysis-pr artifact found."
|
||||
fi
|
||||
|
||||
- name: Check if artifacts exist
|
||||
id: check
|
||||
if: steps.pr.outputs.skip != 'true'
|
||||
run: |
|
||||
if [ -f ./memory-analysis/memory-analysis-target.json ] && [ -f ./memory-analysis/memory-analysis-pr.json ]; then
|
||||
echo "found=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "found=false" >> "$GITHUB_OUTPUT"
|
||||
echo "Memory analysis artifacts not found, skipping comment"
|
||||
fi
|
||||
|
||||
- name: Post or update PR comment
|
||||
if: steps.pr.outputs.skip != 'true' && steps.check.outputs.found == 'true'
|
||||
env:
|
||||
PR_NUMBER: ${{ steps.pr.outputs.pr_number }}
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
# Pass PR number and JSON file paths directly to Python script
|
||||
# Let Python parse the JSON to avoid shell injection risks
|
||||
# The script will validate and sanitize all inputs
|
||||
python script/ci_memory_impact_comment.py \
|
||||
--pr-number "$PR_NUMBER" \
|
||||
--target-json ./memory-analysis/memory-analysis-target.json \
|
||||
--pr-json ./memory-analysis/memory-analysis-pr.json
|
||||
255
.github/workflows/ci.yml
vendored
255
.github/workflows/ci.yml
vendored
@@ -170,11 +170,13 @@ jobs:
|
||||
outputs:
|
||||
integration-tests: ${{ steps.determine.outputs.integration-tests }}
|
||||
clang-tidy: ${{ steps.determine.outputs.clang-tidy }}
|
||||
clang-tidy-mode: ${{ steps.determine.outputs.clang-tidy-mode }}
|
||||
python-linters: ${{ steps.determine.outputs.python-linters }}
|
||||
changed-components: ${{ steps.determine.outputs.changed-components }}
|
||||
changed-components-with-tests: ${{ steps.determine.outputs.changed-components-with-tests }}
|
||||
directly-changed-components-with-tests: ${{ steps.determine.outputs.directly-changed-components-with-tests }}
|
||||
component-test-count: ${{ steps.determine.outputs.component-test-count }}
|
||||
changed-cpp-file-count: ${{ steps.determine.outputs.changed-cpp-file-count }}
|
||||
memory_impact: ${{ steps.determine.outputs.memory-impact }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
@@ -200,11 +202,13 @@ jobs:
|
||||
# Extract individual fields
|
||||
echo "integration-tests=$(echo "$output" | jq -r '.integration_tests')" >> $GITHUB_OUTPUT
|
||||
echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT
|
||||
echo "clang-tidy-mode=$(echo "$output" | jq -r '.clang_tidy_mode')" >> $GITHUB_OUTPUT
|
||||
echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT
|
||||
echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT
|
||||
echo "changed-components-with-tests=$(echo "$output" | jq -c '.changed_components_with_tests')" >> $GITHUB_OUTPUT
|
||||
echo "directly-changed-components-with-tests=$(echo "$output" | jq -c '.directly_changed_components_with_tests')" >> $GITHUB_OUTPUT
|
||||
echo "component-test-count=$(echo "$output" | jq -r '.component_test_count')" >> $GITHUB_OUTPUT
|
||||
echo "changed-cpp-file-count=$(echo "$output" | jq -r '.changed_cpp_file_count')" >> $GITHUB_OUTPUT
|
||||
echo "memory-impact=$(echo "$output" | jq -c '.memory_impact')" >> $GITHUB_OUTPUT
|
||||
|
||||
integration-tests:
|
||||
@@ -243,7 +247,7 @@ jobs:
|
||||
. venv/bin/activate
|
||||
pytest -vv --no-cov --tb=native -n auto tests/integration/
|
||||
|
||||
clang-tidy:
|
||||
clang-tidy-single:
|
||||
name: ${{ matrix.name }}
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
@@ -261,22 +265,6 @@ jobs:
|
||||
name: Run script/clang-tidy for ESP8266
|
||||
options: --environment esp8266-arduino-tidy --grep USE_ESP8266
|
||||
pio_cache_key: tidyesp8266
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ESP32 Arduino 1/4
|
||||
options: --environment esp32-arduino-tidy --split-num 4 --split-at 1
|
||||
pio_cache_key: tidyesp32
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ESP32 Arduino 2/4
|
||||
options: --environment esp32-arduino-tidy --split-num 4 --split-at 2
|
||||
pio_cache_key: tidyesp32
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ESP32 Arduino 3/4
|
||||
options: --environment esp32-arduino-tidy --split-num 4 --split-at 3
|
||||
pio_cache_key: tidyesp32
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ESP32 Arduino 4/4
|
||||
options: --environment esp32-arduino-tidy --split-num 4 --split-at 4
|
||||
pio_cache_key: tidyesp32
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ESP32 IDF
|
||||
options: --environment esp32-idf-tidy --grep USE_ESP_IDF
|
||||
@@ -357,6 +345,166 @@ jobs:
|
||||
# yamllint disable-line rule:line-length
|
||||
if: always()
|
||||
|
||||
clang-tidy-nosplit:
|
||||
name: Run script/clang-tidy for ESP32 Arduino
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- common
|
||||
- determine-jobs
|
||||
if: needs.determine-jobs.outputs.clang-tidy-mode == 'nosplit'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
# Need history for HEAD~1 to work for checking changed files
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref != 'refs/heads/dev'
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Register problem matchers
|
||||
run: |
|
||||
echo "::add-matcher::.github/workflows/matchers/gcc.json"
|
||||
echo "::add-matcher::.github/workflows/matchers/clang-tidy.json"
|
||||
|
||||
- name: Check if full clang-tidy scan needed
|
||||
id: check_full_scan
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
if python script/clang_tidy_hash.py --check; then
|
||||
echo "full_scan=true" >> $GITHUB_OUTPUT
|
||||
echo "reason=hash_changed" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "full_scan=false" >> $GITHUB_OUTPUT
|
||||
echo "reason=normal" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Run clang-tidy
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
if [ "${{ steps.check_full_scan.outputs.full_scan }}" = "true" ]; then
|
||||
echo "Running FULL clang-tidy scan (hash changed)"
|
||||
script/clang-tidy --all-headers --fix --environment esp32-arduino-tidy
|
||||
else
|
||||
echo "Running clang-tidy on changed files only"
|
||||
script/clang-tidy --all-headers --fix --changed --environment esp32-arduino-tidy
|
||||
fi
|
||||
env:
|
||||
# Also cache libdeps, store them in a ~/.platformio subfolder
|
||||
PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps
|
||||
|
||||
- name: Suggested changes
|
||||
run: script/ci-suggest-changes
|
||||
if: always()
|
||||
|
||||
clang-tidy-split:
|
||||
name: ${{ matrix.name }}
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- common
|
||||
- determine-jobs
|
||||
if: needs.determine-jobs.outputs.clang-tidy-mode == 'split'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 1
|
||||
matrix:
|
||||
include:
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ESP32 Arduino 1/4
|
||||
options: --environment esp32-arduino-tidy --split-num 4 --split-at 1
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ESP32 Arduino 2/4
|
||||
options: --environment esp32-arduino-tidy --split-num 4 --split-at 2
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ESP32 Arduino 3/4
|
||||
options: --environment esp32-arduino-tidy --split-num 4 --split-at 3
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ESP32 Arduino 4/4
|
||||
options: --environment esp32-arduino-tidy --split-num 4 --split-at 4
|
||||
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
# Need history for HEAD~1 to work for checking changed files
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref != 'refs/heads/dev'
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Register problem matchers
|
||||
run: |
|
||||
echo "::add-matcher::.github/workflows/matchers/gcc.json"
|
||||
echo "::add-matcher::.github/workflows/matchers/clang-tidy.json"
|
||||
|
||||
- name: Check if full clang-tidy scan needed
|
||||
id: check_full_scan
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
if python script/clang_tidy_hash.py --check; then
|
||||
echo "full_scan=true" >> $GITHUB_OUTPUT
|
||||
echo "reason=hash_changed" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "full_scan=false" >> $GITHUB_OUTPUT
|
||||
echo "reason=normal" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Run clang-tidy
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
if [ "${{ steps.check_full_scan.outputs.full_scan }}" = "true" ]; then
|
||||
echo "Running FULL clang-tidy scan (hash changed)"
|
||||
script/clang-tidy --all-headers --fix ${{ matrix.options }}
|
||||
else
|
||||
echo "Running clang-tidy on changed files only"
|
||||
script/clang-tidy --all-headers --fix --changed ${{ matrix.options }}
|
||||
fi
|
||||
env:
|
||||
# Also cache libdeps, store them in a ~/.platformio subfolder
|
||||
PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps
|
||||
|
||||
- name: Suggested changes
|
||||
run: script/ci-suggest-changes
|
||||
if: always()
|
||||
|
||||
test-build-components-splitter:
|
||||
name: Split components for intelligent grouping (40 weighted per batch)
|
||||
runs-on: ubuntu-24.04
|
||||
@@ -641,6 +789,12 @@ jobs:
|
||||
--output-env \
|
||||
--output-json memory-analysis-target.json
|
||||
|
||||
# Add metadata to JSON before caching
|
||||
python script/ci_add_metadata_to_json.py \
|
||||
--json-file memory-analysis-target.json \
|
||||
--components "$components" \
|
||||
--platform "$platform"
|
||||
|
||||
- name: Save memory analysis to cache
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success'
|
||||
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
@@ -720,6 +874,13 @@ jobs:
|
||||
python script/ci_memory_impact_extract.py \
|
||||
--output-env \
|
||||
--output-json memory-analysis-pr.json
|
||||
|
||||
# Add metadata to JSON (components and platform are in shell variables above)
|
||||
python script/ci_add_metadata_to_json.py \
|
||||
--json-file memory-analysis-pr.json \
|
||||
--components "$components" \
|
||||
--platform "$platform"
|
||||
|
||||
- name: Upload memory analysis JSON
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
@@ -736,10 +897,12 @@ jobs:
|
||||
- determine-jobs
|
||||
- memory-impact-target-branch
|
||||
- memory-impact-pr-branch
|
||||
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.memory_impact).should_run == 'true' && needs.memory-impact-target-branch.outputs.skip != 'true'
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && fromJSON(needs.determine-jobs.outputs.memory_impact).should_run == 'true' && needs.memory-impact-target-branch.outputs.skip != 'true'
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
@@ -749,65 +912,29 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Download target analysis JSON
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
with:
|
||||
name: memory-analysis-target
|
||||
path: ./memory-analysis
|
||||
continue-on-error: true
|
||||
- name: Download PR analysis JSON
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
with:
|
||||
name: memory-analysis-pr
|
||||
path: ./memory-analysis
|
||||
continue-on-error: true
|
||||
- name: Post or update PR comment
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
COMPONENTS: ${{ toJSON(fromJSON(needs.determine-jobs.outputs.memory_impact).components) }}
|
||||
PLATFORM: ${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}
|
||||
TARGET_RAM: ${{ needs.memory-impact-target-branch.outputs.ram_usage }}
|
||||
TARGET_FLASH: ${{ needs.memory-impact-target-branch.outputs.flash_usage }}
|
||||
PR_RAM: ${{ needs.memory-impact-pr-branch.outputs.ram_usage }}
|
||||
PR_FLASH: ${{ needs.memory-impact-pr-branch.outputs.flash_usage }}
|
||||
TARGET_CACHE_HIT: ${{ needs.memory-impact-target-branch.outputs.cache_hit }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
|
||||
# Check if analysis JSON files exist
|
||||
target_json_arg=""
|
||||
pr_json_arg=""
|
||||
|
||||
if [ -f ./memory-analysis/memory-analysis-target.json ]; then
|
||||
echo "Found target analysis JSON"
|
||||
target_json_arg="--target-json ./memory-analysis/memory-analysis-target.json"
|
||||
else
|
||||
echo "No target analysis JSON found"
|
||||
fi
|
||||
|
||||
if [ -f ./memory-analysis/memory-analysis-pr.json ]; then
|
||||
echo "Found PR analysis JSON"
|
||||
pr_json_arg="--pr-json ./memory-analysis/memory-analysis-pr.json"
|
||||
else
|
||||
echo "No PR analysis JSON found"
|
||||
fi
|
||||
|
||||
# Add cache flag if target was cached
|
||||
cache_flag=""
|
||||
if [ "$TARGET_CACHE_HIT" == "true" ]; then
|
||||
cache_flag="--target-cache-hit"
|
||||
fi
|
||||
|
||||
# Pass JSON file paths directly to Python script
|
||||
# All data is extracted from JSON files for security
|
||||
python script/ci_memory_impact_comment.py \
|
||||
--pr-number "${{ github.event.pull_request.number }}" \
|
||||
--components "$COMPONENTS" \
|
||||
--platform "$PLATFORM" \
|
||||
--target-ram "$TARGET_RAM" \
|
||||
--target-flash "$TARGET_FLASH" \
|
||||
--pr-ram "$PR_RAM" \
|
||||
--pr-flash "$PR_FLASH" \
|
||||
$target_json_arg \
|
||||
$pr_json_arg \
|
||||
$cache_flag
|
||||
--pr-number "$PR_NUMBER" \
|
||||
--target-json ./memory-analysis/memory-analysis-target.json \
|
||||
--pr-json ./memory-analysis/memory-analysis-pr.json
|
||||
|
||||
ci-status:
|
||||
name: CI Status
|
||||
@@ -818,7 +945,9 @@ jobs:
|
||||
- pylint
|
||||
- pytest
|
||||
- integration-tests
|
||||
- clang-tidy
|
||||
- clang-tidy-single
|
||||
- clang-tidy-nosplit
|
||||
- clang-tidy-split
|
||||
- determine-jobs
|
||||
- test-build-components-splitter
|
||||
- test-build-components-split
|
||||
|
||||
@@ -70,6 +70,7 @@ esphome/components/bl0939/* @ziceva
|
||||
esphome/components/bl0940/* @dan-s-github @tobias-
|
||||
esphome/components/bl0942/* @dbuezas @dwmw2
|
||||
esphome/components/ble_client/* @buxtronix @clydebarrow
|
||||
esphome/components/ble_nus/* @tomaszduda23
|
||||
esphome/components/bluetooth_proxy/* @bdraco @jesserockz
|
||||
esphome/components/bme280_base/* @esphome/core
|
||||
esphome/components/bme280_spi/* @apbodrov
|
||||
@@ -160,6 +161,7 @@ esphome/components/esp32_rmt_led_strip/* @jesserockz
|
||||
esphome/components/esp8266/* @esphome/core
|
||||
esphome/components/esp_ldo/* @clydebarrow
|
||||
esphome/components/espnow/* @jesserockz
|
||||
esphome/components/espnow/packet_transport/* @EasilyBoredEngineer
|
||||
esphome/components/ethernet_info/* @gtjadsonsantos
|
||||
esphome/components/event/* @nohat
|
||||
esphome/components/exposure_notifications/* @OttoWinter
|
||||
|
||||
@@ -62,6 +62,40 @@ from esphome.util import (
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Special non-component keys that appear in configs
|
||||
_NON_COMPONENT_KEYS = frozenset(
|
||||
{
|
||||
CONF_ESPHOME,
|
||||
"substitutions",
|
||||
"packages",
|
||||
"globals",
|
||||
"external_components",
|
||||
"<<",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def detect_external_components(config: ConfigType) -> set[str]:
|
||||
"""Detect external/custom components in the configuration.
|
||||
|
||||
External components are those that appear in the config but are not
|
||||
part of ESPHome's built-in components and are not special config keys.
|
||||
|
||||
Args:
|
||||
config: The ESPHome configuration dictionary
|
||||
|
||||
Returns:
|
||||
A set of external component names
|
||||
"""
|
||||
from esphome.analyze_memory.helpers import get_esphome_components
|
||||
|
||||
builtin_components = get_esphome_components()
|
||||
return {
|
||||
key
|
||||
for key in config
|
||||
if key not in builtin_components and key not in _NON_COMPONENT_KEYS
|
||||
}
|
||||
|
||||
|
||||
class ArgsProtocol(Protocol):
|
||||
device: list[str] | None
|
||||
@@ -185,7 +219,9 @@ def choose_upload_log_host(
|
||||
else:
|
||||
resolved.append(device)
|
||||
if not resolved:
|
||||
_LOGGER.error("All specified devices: %s could not be resolved.", defaults)
|
||||
raise EsphomeError(
|
||||
f"All specified devices {defaults} could not be resolved. Is the device connected to the network?"
|
||||
)
|
||||
return resolved
|
||||
|
||||
# No devices specified, show interactive chooser
|
||||
@@ -890,6 +926,54 @@ def command_idedata(args: ArgsProtocol, config: ConfigType) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
|
||||
"""Analyze memory usage by component.
|
||||
|
||||
This command compiles the configuration and performs memory analysis.
|
||||
Compilation is fast if sources haven't changed (just relinking).
|
||||
"""
|
||||
from esphome import platformio_api
|
||||
from esphome.analyze_memory.cli import MemoryAnalyzerCLI
|
||||
|
||||
# Always compile to ensure fresh data (fast if no changes - just relinks)
|
||||
exit_code = write_cpp(config)
|
||||
if exit_code != 0:
|
||||
return exit_code
|
||||
exit_code = compile_program(args, config)
|
||||
if exit_code != 0:
|
||||
return exit_code
|
||||
_LOGGER.info("Successfully compiled program.")
|
||||
|
||||
# Get idedata for analysis
|
||||
idedata = platformio_api.get_idedata(config)
|
||||
if idedata is None:
|
||||
_LOGGER.error("Failed to get IDE data for memory analysis")
|
||||
return 1
|
||||
|
||||
firmware_elf = Path(idedata.firmware_elf_path)
|
||||
|
||||
# Extract external components from config
|
||||
external_components = detect_external_components(config)
|
||||
_LOGGER.debug("Detected external components: %s", external_components)
|
||||
|
||||
# Perform memory analysis
|
||||
_LOGGER.info("Analyzing memory usage...")
|
||||
analyzer = MemoryAnalyzerCLI(
|
||||
str(firmware_elf),
|
||||
idedata.objdump_path,
|
||||
idedata.readelf_path,
|
||||
external_components,
|
||||
)
|
||||
analyzer.analyze()
|
||||
|
||||
# Generate and display report
|
||||
report = analyzer.generate_report()
|
||||
print()
|
||||
print(report)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def command_rename(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
new_name = args.name
|
||||
for c in new_name:
|
||||
@@ -1005,6 +1089,7 @@ POST_CONFIG_ACTIONS = {
|
||||
"idedata": command_idedata,
|
||||
"rename": command_rename,
|
||||
"discover": command_discover,
|
||||
"analyze-memory": command_analyze_memory,
|
||||
}
|
||||
|
||||
SIMPLE_CONFIG_ACTIONS = [
|
||||
@@ -1290,6 +1375,14 @@ def parse_args(argv):
|
||||
)
|
||||
parser_rename.add_argument("name", help="The new name for the device.", type=str)
|
||||
|
||||
parser_analyze_memory = subparsers.add_parser(
|
||||
"analyze-memory",
|
||||
help="Analyze memory usage by component.",
|
||||
)
|
||||
parser_analyze_memory.add_argument(
|
||||
"configuration", help="Your YAML configuration file(s).", nargs="+"
|
||||
)
|
||||
|
||||
# Keep backward compatibility with the old command line format of
|
||||
# esphome <config> <command>.
|
||||
#
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "absolute_humidity.h"
|
||||
|
||||
// test
|
||||
namespace esphome {
|
||||
namespace absolute_humidity {
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ enum SaturationVaporPressureEquation {
|
||||
};
|
||||
|
||||
/// This class implements calculation of absolute humidity from temperature and relative humidity.
|
||||
// Test change for clang-tidy split logic
|
||||
class AbsoluteHumidityComponent : public sensor::Sensor, public Component {
|
||||
public:
|
||||
AbsoluteHumidityComponent() = default;
|
||||
|
||||
@@ -28,7 +28,7 @@ class Anova : public climate::Climate, public esphome::ble_client::BLEClientNode
|
||||
void dump_config() override;
|
||||
climate::ClimateTraits traits() override {
|
||||
auto traits = climate::ClimateTraits();
|
||||
traits.set_supports_current_temperature(true);
|
||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
|
||||
traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::ClimateMode::CLIMATE_MODE_HEAT});
|
||||
traits.set_visual_min_temperature(25.0);
|
||||
traits.set_visual_max_temperature(100.0);
|
||||
|
||||
@@ -155,6 +155,17 @@ def _validate_api_config(config: ConfigType) -> ConfigType:
|
||||
return config
|
||||
|
||||
|
||||
def _consume_api_sockets(config: ConfigType) -> ConfigType:
|
||||
"""Register socket needs for API component."""
|
||||
from esphome.components import socket
|
||||
|
||||
# API needs 1 listening socket + typically 3 concurrent client connections
|
||||
# (not max_connections, which is the upper limit rarely reached)
|
||||
sockets_needed = 1 + 3
|
||||
socket.consume_sockets(sockets_needed, "api")(config)
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
@@ -222,6 +233,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
cv.rename_key(CONF_SERVICES, CONF_ACTIONS),
|
||||
_validate_api_config,
|
||||
_consume_api_sockets,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -506,7 +506,7 @@ message ListEntitiesLightResponse {
|
||||
string name = 3;
|
||||
reserved 4; // Deprecated: was string unique_id
|
||||
|
||||
repeated ColorMode supported_color_modes = 12 [(container_pointer) = "std::set<light::ColorMode>"];
|
||||
repeated ColorMode supported_color_modes = 12 [(container_pointer_no_template) = "light::ColorModeMask"];
|
||||
// next four supports_* are for legacy clients, newer clients should use color modes
|
||||
// Deprecated in API version 1.6
|
||||
bool legacy_supports_brightness = 5 [deprecated=true];
|
||||
|
||||
@@ -453,7 +453,6 @@ uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection *
|
||||
bool is_single) {
|
||||
auto *light = static_cast<light::LightState *>(entity);
|
||||
LightStateResponse resp;
|
||||
auto traits = light->get_traits();
|
||||
auto values = light->remote_values;
|
||||
auto color_mode = values.get_color_mode();
|
||||
resp.state = values.is_on();
|
||||
@@ -477,7 +476,8 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c
|
||||
auto *light = static_cast<light::LightState *>(entity);
|
||||
ListEntitiesLightResponse msg;
|
||||
auto traits = light->get_traits();
|
||||
msg.supported_color_modes = &traits.get_supported_color_modes_for_api_();
|
||||
// Pass pointer to ColorModeMask so the iterator can encode actual ColorMode enum values
|
||||
msg.supported_color_modes = &traits.get_supported_color_modes();
|
||||
if (traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) ||
|
||||
traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE)) {
|
||||
msg.min_mireds = traits.get_min_mireds();
|
||||
@@ -661,11 +661,12 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection
|
||||
ListEntitiesClimateResponse msg;
|
||||
auto traits = climate->get_traits();
|
||||
// Flags set for backward compatibility, deprecated in 2025.11.0
|
||||
msg.supports_current_temperature = traits.get_supports_current_temperature();
|
||||
msg.supports_current_humidity = traits.get_supports_current_humidity();
|
||||
msg.supports_two_point_target_temperature = traits.get_supports_two_point_target_temperature();
|
||||
msg.supports_target_humidity = traits.get_supports_target_humidity();
|
||||
msg.supports_action = traits.get_supports_action();
|
||||
msg.supports_current_temperature = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
|
||||
msg.supports_current_humidity = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY);
|
||||
msg.supports_two_point_target_temperature = traits.has_feature_flags(
|
||||
climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE);
|
||||
msg.supports_target_humidity = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY);
|
||||
msg.supports_action = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION);
|
||||
// Current feature flags and other supported parameters
|
||||
msg.feature_flags = traits.get_feature_flags();
|
||||
msg.supported_modes = &traits.get_supported_modes_for_api_();
|
||||
@@ -1081,13 +1082,8 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) {
|
||||
homeassistant::global_homeassistant_time->set_epoch_time(value.epoch_seconds);
|
||||
#ifdef USE_TIME_TIMEZONE
|
||||
if (value.timezone_len > 0) {
|
||||
const std::string ¤t_tz = homeassistant::global_homeassistant_time->get_timezone();
|
||||
// Compare without allocating a string
|
||||
if (current_tz.length() != value.timezone_len ||
|
||||
memcmp(current_tz.c_str(), value.timezone, value.timezone_len) != 0) {
|
||||
homeassistant::global_homeassistant_time->set_timezone(
|
||||
std::string(reinterpret_cast<const char *>(value.timezone), value.timezone_len));
|
||||
}
|
||||
homeassistant::global_homeassistant_time->set_timezone(reinterpret_cast<const char *>(value.timezone),
|
||||
value.timezone_len);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -70,4 +70,14 @@ extend google.protobuf.FieldOptions {
|
||||
// init(size) before adding elements. This eliminates std::vector template overhead
|
||||
// and is ideal when the exact size is known before populating the array.
|
||||
optional bool fixed_vector = 50013 [default=false];
|
||||
|
||||
// container_pointer_no_template: Use a non-template container type for repeated fields
|
||||
// Similar to container_pointer, but for containers that don't take template parameters.
|
||||
// The container type is used as-is without appending element type.
|
||||
// The container must have:
|
||||
// - begin() and end() methods returning iterators
|
||||
// - empty() method
|
||||
// Example: [(container_pointer_no_template) = "light::ColorModeMask"]
|
||||
// generates: const light::ColorModeMask *supported_color_modes{};
|
||||
optional string container_pointer_no_template = 50014;
|
||||
}
|
||||
|
||||
@@ -790,7 +790,7 @@ class ListEntitiesLightResponse final : public InfoResponseProtoMessage {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "list_entities_light_response"; }
|
||||
#endif
|
||||
const std::set<light::ColorMode> *supported_color_modes{};
|
||||
const light::ColorModeMask *supported_color_modes{};
|
||||
float min_mireds{0.0f};
|
||||
float max_mireds{0.0f};
|
||||
std::vector<std::string> effects{};
|
||||
|
||||
@@ -6,6 +6,9 @@ namespace bang_bang {
|
||||
|
||||
static const char *const TAG = "bang_bang.climate";
|
||||
|
||||
BangBangClimate::BangBangClimate()
|
||||
: idle_trigger_(new Trigger<>()), cool_trigger_(new Trigger<>()), heat_trigger_(new Trigger<>()) {}
|
||||
|
||||
void BangBangClimate::setup() {
|
||||
this->sensor_->add_on_state_callback([this](float state) {
|
||||
this->current_temperature = state;
|
||||
@@ -31,53 +34,63 @@ void BangBangClimate::setup() {
|
||||
restore->to_call(this).perform();
|
||||
} else {
|
||||
// restore from defaults, change_away handles those for us
|
||||
if (supports_cool_ && supports_heat_) {
|
||||
if (this->supports_cool_ && this->supports_heat_) {
|
||||
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
|
||||
} else if (supports_cool_) {
|
||||
} else if (this->supports_cool_) {
|
||||
this->mode = climate::CLIMATE_MODE_COOL;
|
||||
} else if (supports_heat_) {
|
||||
} else if (this->supports_heat_) {
|
||||
this->mode = climate::CLIMATE_MODE_HEAT;
|
||||
}
|
||||
this->change_away_(false);
|
||||
}
|
||||
}
|
||||
|
||||
void BangBangClimate::control(const climate::ClimateCall &call) {
|
||||
if (call.get_mode().has_value())
|
||||
if (call.get_mode().has_value()) {
|
||||
this->mode = *call.get_mode();
|
||||
if (call.get_target_temperature_low().has_value())
|
||||
}
|
||||
if (call.get_target_temperature_low().has_value()) {
|
||||
this->target_temperature_low = *call.get_target_temperature_low();
|
||||
if (call.get_target_temperature_high().has_value())
|
||||
}
|
||||
if (call.get_target_temperature_high().has_value()) {
|
||||
this->target_temperature_high = *call.get_target_temperature_high();
|
||||
if (call.get_preset().has_value())
|
||||
}
|
||||
if (call.get_preset().has_value()) {
|
||||
this->change_away_(*call.get_preset() == climate::CLIMATE_PRESET_AWAY);
|
||||
}
|
||||
|
||||
this->compute_state_();
|
||||
this->publish_state();
|
||||
}
|
||||
|
||||
climate::ClimateTraits BangBangClimate::traits() {
|
||||
auto traits = climate::ClimateTraits();
|
||||
traits.set_supports_current_temperature(true);
|
||||
if (this->humidity_sensor_ != nullptr)
|
||||
traits.set_supports_current_humidity(true);
|
||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE |
|
||||
climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE | climate::CLIMATE_SUPPORTS_ACTION);
|
||||
if (this->humidity_sensor_ != nullptr) {
|
||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY);
|
||||
}
|
||||
traits.set_supported_modes({
|
||||
climate::CLIMATE_MODE_OFF,
|
||||
});
|
||||
if (supports_cool_)
|
||||
if (this->supports_cool_) {
|
||||
traits.add_supported_mode(climate::CLIMATE_MODE_COOL);
|
||||
if (supports_heat_)
|
||||
}
|
||||
if (this->supports_heat_) {
|
||||
traits.add_supported_mode(climate::CLIMATE_MODE_HEAT);
|
||||
if (supports_cool_ && supports_heat_)
|
||||
}
|
||||
if (this->supports_cool_ && this->supports_heat_) {
|
||||
traits.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL);
|
||||
traits.set_supports_two_point_target_temperature(true);
|
||||
if (supports_away_) {
|
||||
}
|
||||
if (this->supports_away_) {
|
||||
traits.set_supported_presets({
|
||||
climate::CLIMATE_PRESET_HOME,
|
||||
climate::CLIMATE_PRESET_AWAY,
|
||||
});
|
||||
}
|
||||
traits.set_supports_action(true);
|
||||
return traits;
|
||||
}
|
||||
|
||||
void BangBangClimate::compute_state_() {
|
||||
if (this->mode == climate::CLIMATE_MODE_OFF) {
|
||||
this->switch_to_action_(climate::CLIMATE_ACTION_OFF);
|
||||
@@ -122,6 +135,7 @@ void BangBangClimate::compute_state_() {
|
||||
|
||||
this->switch_to_action_(target_action);
|
||||
}
|
||||
|
||||
void BangBangClimate::switch_to_action_(climate::ClimateAction action) {
|
||||
if (action == this->action) {
|
||||
// already in target mode
|
||||
@@ -166,6 +180,7 @@ void BangBangClimate::switch_to_action_(climate::ClimateAction action) {
|
||||
this->prev_trigger_ = trig;
|
||||
this->publish_state();
|
||||
}
|
||||
|
||||
void BangBangClimate::change_away_(bool away) {
|
||||
if (!away) {
|
||||
this->target_temperature_low = this->normal_config_.default_temperature_low;
|
||||
@@ -176,22 +191,26 @@ void BangBangClimate::change_away_(bool away) {
|
||||
}
|
||||
this->preset = away ? climate::CLIMATE_PRESET_AWAY : climate::CLIMATE_PRESET_HOME;
|
||||
}
|
||||
|
||||
void BangBangClimate::set_normal_config(const BangBangClimateTargetTempConfig &normal_config) {
|
||||
this->normal_config_ = normal_config;
|
||||
}
|
||||
|
||||
void BangBangClimate::set_away_config(const BangBangClimateTargetTempConfig &away_config) {
|
||||
this->supports_away_ = true;
|
||||
this->away_config_ = away_config;
|
||||
}
|
||||
BangBangClimate::BangBangClimate()
|
||||
: idle_trigger_(new Trigger<>()), cool_trigger_(new Trigger<>()), heat_trigger_(new Trigger<>()) {}
|
||||
|
||||
void BangBangClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; }
|
||||
void BangBangClimate::set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
|
||||
|
||||
Trigger<> *BangBangClimate::get_idle_trigger() const { return this->idle_trigger_; }
|
||||
Trigger<> *BangBangClimate::get_cool_trigger() const { return this->cool_trigger_; }
|
||||
void BangBangClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
|
||||
Trigger<> *BangBangClimate::get_heat_trigger() const { return this->heat_trigger_; }
|
||||
|
||||
void BangBangClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
|
||||
void BangBangClimate::set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; }
|
||||
|
||||
void BangBangClimate::dump_config() {
|
||||
LOG_CLIMATE("", "Bang Bang Climate", this);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
|
||||
@@ -25,14 +25,15 @@ class BangBangClimate : public climate::Climate, public Component {
|
||||
|
||||
void set_sensor(sensor::Sensor *sensor);
|
||||
void set_humidity_sensor(sensor::Sensor *humidity_sensor);
|
||||
Trigger<> *get_idle_trigger() const;
|
||||
Trigger<> *get_cool_trigger() const;
|
||||
void set_supports_cool(bool supports_cool);
|
||||
Trigger<> *get_heat_trigger() const;
|
||||
void set_supports_heat(bool supports_heat);
|
||||
void set_normal_config(const BangBangClimateTargetTempConfig &normal_config);
|
||||
void set_away_config(const BangBangClimateTargetTempConfig &away_config);
|
||||
|
||||
Trigger<> *get_idle_trigger() const;
|
||||
Trigger<> *get_cool_trigger() const;
|
||||
Trigger<> *get_heat_trigger() const;
|
||||
|
||||
protected:
|
||||
/// Override control to change settings of the climate device.
|
||||
void control(const climate::ClimateCall &call) override;
|
||||
@@ -56,16 +57,10 @@ class BangBangClimate : public climate::Climate, public Component {
|
||||
*
|
||||
* In idle mode, the controller is assumed to have both heating and cooling disabled.
|
||||
*/
|
||||
Trigger<> *idle_trigger_;
|
||||
Trigger<> *idle_trigger_{nullptr};
|
||||
/** The trigger to call when the controller should switch to cooling mode.
|
||||
*/
|
||||
Trigger<> *cool_trigger_;
|
||||
/** Whether the controller supports cooling.
|
||||
*
|
||||
* A false value for this attribute means that the controller has no cooling action
|
||||
* (for example a thermostat, where only heating and not-heating is possible).
|
||||
*/
|
||||
bool supports_cool_{false};
|
||||
Trigger<> *cool_trigger_{nullptr};
|
||||
/** The trigger to call when the controller should switch to heating mode.
|
||||
*
|
||||
* A null value for this attribute means that the controller has no heating action
|
||||
@@ -73,15 +68,23 @@ class BangBangClimate : public climate::Climate, public Component {
|
||||
* (blinds open) is possible.
|
||||
*/
|
||||
Trigger<> *heat_trigger_{nullptr};
|
||||
bool supports_heat_{false};
|
||||
/** A reference to the trigger that was previously active.
|
||||
*
|
||||
* This is so that the previous trigger can be stopped before enabling a new one.
|
||||
*/
|
||||
Trigger<> *prev_trigger_{nullptr};
|
||||
|
||||
BangBangClimateTargetTempConfig normal_config_{};
|
||||
/** Whether the controller supports cooling/heating
|
||||
*
|
||||
* A false value for this attribute means that the controller has no respective action
|
||||
* (for example a thermostat, where only heating and not-heating is possible).
|
||||
*/
|
||||
bool supports_cool_{false};
|
||||
bool supports_heat_{false};
|
||||
|
||||
bool supports_away_{false};
|
||||
|
||||
BangBangClimateTargetTempConfig normal_config_{};
|
||||
BangBangClimateTargetTempConfig away_config_{};
|
||||
};
|
||||
|
||||
|
||||
@@ -33,8 +33,7 @@ class BedJetClimate : public climate::Climate, public BedJetClient, public Polli
|
||||
|
||||
climate::ClimateTraits traits() override {
|
||||
auto traits = climate::ClimateTraits();
|
||||
traits.set_supports_action(true);
|
||||
traits.set_supports_current_temperature(true);
|
||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_ACTION | climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
|
||||
traits.set_supported_modes({
|
||||
climate::CLIMATE_MODE_OFF,
|
||||
climate::CLIMATE_MODE_HEAT,
|
||||
|
||||
29
esphome/components/ble_nus/__init__.py
Normal file
29
esphome/components/ble_nus/__init__.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.zephyr import zephyr_add_prj_conf
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_LOGS, CONF_TYPE
|
||||
|
||||
AUTO_LOAD = ["zephyr_ble_server"]
|
||||
CODEOWNERS = ["@tomaszduda23"]
|
||||
|
||||
ble_nus_ns = cg.esphome_ns.namespace("ble_nus")
|
||||
BLENUS = ble_nus_ns.class_("BLENUS", cg.Component)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(BLENUS),
|
||||
cv.Optional(CONF_TYPE, default=CONF_LOGS): cv.one_of(
|
||||
*[CONF_LOGS], lower=True
|
||||
),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
cv.only_with_framework("zephyr"),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
zephyr_add_prj_conf("BT_NUS", True)
|
||||
cg.add(var.set_expose_log(config[CONF_TYPE] == CONF_LOGS))
|
||||
await cg.register_component(var, config)
|
||||
157
esphome/components/ble_nus/ble_nus.cpp
Normal file
157
esphome/components/ble_nus/ble_nus.cpp
Normal file
@@ -0,0 +1,157 @@
|
||||
#ifdef USE_ZEPHYR
|
||||
#include "ble_nus.h"
|
||||
#include <zephyr/kernel.h>
|
||||
#include <bluetooth/services/nus.h>
|
||||
#include "esphome/core/log.h"
|
||||
#ifdef USE_LOGGER
|
||||
#include "esphome/components/logger/logger.h"
|
||||
#include "esphome/core/application.h"
|
||||
#endif
|
||||
#include <zephyr/sys/ring_buffer.h>
|
||||
|
||||
namespace esphome::ble_nus {
|
||||
|
||||
constexpr size_t BLE_TX_BUF_SIZE = 2048;
|
||||
|
||||
// NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
BLENUS *global_ble_nus;
|
||||
RING_BUF_DECLARE(global_ble_tx_ring_buf, BLE_TX_BUF_SIZE);
|
||||
// NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
static const char *const TAG = "ble_nus";
|
||||
|
||||
size_t BLENUS::write_array(const uint8_t *data, size_t len) {
|
||||
if (atomic_get(&this->tx_status_) == TX_DISABLED) {
|
||||
return 0;
|
||||
}
|
||||
return ring_buf_put(&global_ble_tx_ring_buf, data, len);
|
||||
}
|
||||
|
||||
void BLENUS::connected(bt_conn *conn, uint8_t err) {
|
||||
if (err == 0) {
|
||||
global_ble_nus->conn_.store(bt_conn_ref(conn));
|
||||
}
|
||||
}
|
||||
|
||||
void BLENUS::disconnected(bt_conn *conn, uint8_t reason) {
|
||||
if (global_ble_nus->conn_) {
|
||||
bt_conn_unref(global_ble_nus->conn_.load());
|
||||
// Connection array is global static.
|
||||
// Reference can be kept even if disconnected.
|
||||
}
|
||||
}
|
||||
|
||||
void BLENUS::tx_callback(bt_conn *conn) {
|
||||
atomic_cas(&global_ble_nus->tx_status_, TX_BUSY, TX_ENABLED);
|
||||
ESP_LOGVV(TAG, "Sent operation completed");
|
||||
}
|
||||
|
||||
void BLENUS::send_enabled_callback(bt_nus_send_status status) {
|
||||
switch (status) {
|
||||
case BT_NUS_SEND_STATUS_ENABLED:
|
||||
atomic_set(&global_ble_nus->tx_status_, TX_ENABLED);
|
||||
#ifdef USE_LOGGER
|
||||
if (global_ble_nus->expose_log_) {
|
||||
App.schedule_dump_config();
|
||||
}
|
||||
#endif
|
||||
ESP_LOGD(TAG, "NUS notification has been enabled");
|
||||
break;
|
||||
case BT_NUS_SEND_STATUS_DISABLED:
|
||||
atomic_set(&global_ble_nus->tx_status_, TX_DISABLED);
|
||||
ESP_LOGD(TAG, "NUS notification has been disabled");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void BLENUS::rx_callback(bt_conn *conn, const uint8_t *const data, uint16_t len) {
|
||||
ESP_LOGD(TAG, "Received %d bytes.", len);
|
||||
}
|
||||
|
||||
void BLENUS::setup() {
|
||||
bt_nus_cb callbacks = {
|
||||
.received = rx_callback,
|
||||
.sent = tx_callback,
|
||||
.send_enabled = send_enabled_callback,
|
||||
};
|
||||
|
||||
bt_nus_init(&callbacks);
|
||||
|
||||
static bt_conn_cb conn_callbacks = {
|
||||
.connected = BLENUS::connected,
|
||||
.disconnected = BLENUS::disconnected,
|
||||
};
|
||||
|
||||
bt_conn_cb_register(&conn_callbacks);
|
||||
|
||||
global_ble_nus = this;
|
||||
#ifdef USE_LOGGER
|
||||
if (logger::global_logger != nullptr && this->expose_log_) {
|
||||
logger::global_logger->add_on_log_callback(
|
||||
[this](int level, const char *tag, const char *message, size_t message_len) {
|
||||
this->write_array(reinterpret_cast<const uint8_t *>(message), message_len);
|
||||
const char c = '\n';
|
||||
this->write_array(reinterpret_cast<const uint8_t *>(&c), 1);
|
||||
});
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
void BLENUS::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "ble nus:");
|
||||
ESP_LOGCONFIG(TAG, " log: %s", YESNO(this->expose_log_));
|
||||
uint32_t mtu = 0;
|
||||
bt_conn *conn = this->conn_.load();
|
||||
if (conn) {
|
||||
mtu = bt_nus_get_mtu(conn);
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " MTU: %u", mtu);
|
||||
}
|
||||
|
||||
void BLENUS::loop() {
|
||||
if (ring_buf_is_empty(&global_ble_tx_ring_buf)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!atomic_cas(&this->tx_status_, TX_ENABLED, TX_BUSY)) {
|
||||
if (atomic_get(&this->tx_status_) == TX_DISABLED) {
|
||||
ring_buf_reset(&global_ble_tx_ring_buf);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
bt_conn *conn = this->conn_.load();
|
||||
if (conn) {
|
||||
conn = bt_conn_ref(conn);
|
||||
}
|
||||
|
||||
if (nullptr == conn) {
|
||||
atomic_cas(&this->tx_status_, TX_BUSY, TX_ENABLED);
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t req_len = bt_nus_get_mtu(conn);
|
||||
|
||||
uint8_t *buf;
|
||||
uint32_t size = ring_buf_get_claim(&global_ble_tx_ring_buf, &buf, req_len);
|
||||
|
||||
int err, err2;
|
||||
|
||||
err = bt_nus_send(conn, buf, size);
|
||||
err2 = ring_buf_get_finish(&global_ble_tx_ring_buf, size);
|
||||
if (err2) {
|
||||
// It should no happen.
|
||||
ESP_LOGE(TAG, "Size %u exceeds valid bytes in the ring buffer (%d error)", size, err2);
|
||||
}
|
||||
if (err == 0) {
|
||||
ESP_LOGVV(TAG, "Sent %d bytes", size);
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed to send %d bytes (%d error)", size, err);
|
||||
atomic_cas(&this->tx_status_, TX_BUSY, TX_ENABLED);
|
||||
}
|
||||
bt_conn_unref(conn);
|
||||
}
|
||||
|
||||
} // namespace esphome::ble_nus
|
||||
#endif
|
||||
37
esphome/components/ble_nus/ble_nus.h
Normal file
37
esphome/components/ble_nus/ble_nus.h
Normal file
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
#ifdef USE_ZEPHYR
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include <shell/shell_bt_nus.h>
|
||||
#include <atomic>
|
||||
|
||||
namespace esphome::ble_nus {
|
||||
|
||||
class BLENUS : public Component {
|
||||
enum TxStatus {
|
||||
TX_DISABLED,
|
||||
TX_ENABLED,
|
||||
TX_BUSY,
|
||||
};
|
||||
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void loop() override;
|
||||
size_t write_array(const uint8_t *data, size_t len);
|
||||
void set_expose_log(bool expose_log) { this->expose_log_ = expose_log; }
|
||||
|
||||
protected:
|
||||
static void send_enabled_callback(bt_nus_send_status status);
|
||||
static void tx_callback(bt_conn *conn);
|
||||
static void rx_callback(bt_conn *conn, const uint8_t *data, uint16_t len);
|
||||
static void connected(bt_conn *conn, uint8_t err);
|
||||
static void disconnected(bt_conn *conn, uint8_t reason);
|
||||
|
||||
std::atomic<bt_conn *> conn_ = nullptr;
|
||||
bool expose_log_ = false;
|
||||
atomic_t tx_status_ = ATOMIC_INIT(TX_DISABLED);
|
||||
};
|
||||
|
||||
} // namespace esphome::ble_nus
|
||||
#endif
|
||||
@@ -155,16 +155,12 @@ esp32_ble_tracker::AdvertisementParserType BluetoothProxy::get_advertisement_par
|
||||
BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool reserve) {
|
||||
for (uint8_t i = 0; i < this->connection_count_; i++) {
|
||||
auto *connection = this->connections_[i];
|
||||
if (connection->get_address() == address)
|
||||
uint64_t conn_addr = connection->get_address();
|
||||
|
||||
if (conn_addr == address)
|
||||
return connection;
|
||||
}
|
||||
|
||||
if (!reserve)
|
||||
return nullptr;
|
||||
|
||||
for (uint8_t i = 0; i < this->connection_count_; i++) {
|
||||
auto *connection = this->connections_[i];
|
||||
if (connection->get_address() == 0) {
|
||||
if (reserve && conn_addr == 0) {
|
||||
connection->send_service_ = INIT_SENDING_SERVICES;
|
||||
connection->set_address(address);
|
||||
// All connections must start at INIT
|
||||
@@ -175,7 +171,6 @@ BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool rese
|
||||
return connection;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(BME680BSECComponent),
|
||||
cv.Optional(CONF_TEMPERATURE_OFFSET, default=0): cv.temperature,
|
||||
cv.Optional(CONF_TEMPERATURE_OFFSET, default=0): cv.temperature_delta,
|
||||
cv.Optional(CONF_IAQ_MODE, default="STATIC"): cv.enum(
|
||||
IAQ_MODE_OPTIONS, upper=True
|
||||
),
|
||||
|
||||
@@ -139,7 +139,7 @@ CONFIG_SCHEMA_BASE = (
|
||||
cv.Optional(CONF_SUPPLY_VOLTAGE, default="3.3V"): cv.enum(
|
||||
VOLTAGE_OPTIONS, upper=True
|
||||
),
|
||||
cv.Optional(CONF_TEMPERATURE_OFFSET, default=0): cv.temperature,
|
||||
cv.Optional(CONF_TEMPERATURE_OFFSET, default=0): cv.temperature_delta,
|
||||
cv.Optional(
|
||||
CONF_STATE_SAVE_INTERVAL, default="6hours"
|
||||
): cv.positive_time_period_minutes,
|
||||
|
||||
@@ -6,6 +6,42 @@ namespace climate {
|
||||
|
||||
static const char *const TAG = "climate";
|
||||
|
||||
// Memory-efficient lookup tables
|
||||
struct StringToUint8 {
|
||||
const char *str;
|
||||
const uint8_t value;
|
||||
};
|
||||
|
||||
constexpr StringToUint8 CLIMATE_MODES_BY_STR[] = {
|
||||
{"OFF", CLIMATE_MODE_OFF},
|
||||
{"AUTO", CLIMATE_MODE_AUTO},
|
||||
{"COOL", CLIMATE_MODE_COOL},
|
||||
{"HEAT", CLIMATE_MODE_HEAT},
|
||||
{"FAN_ONLY", CLIMATE_MODE_FAN_ONLY},
|
||||
{"DRY", CLIMATE_MODE_DRY},
|
||||
{"HEAT_COOL", CLIMATE_MODE_HEAT_COOL},
|
||||
};
|
||||
|
||||
constexpr StringToUint8 CLIMATE_FAN_MODES_BY_STR[] = {
|
||||
{"ON", CLIMATE_FAN_ON}, {"OFF", CLIMATE_FAN_OFF}, {"AUTO", CLIMATE_FAN_AUTO},
|
||||
{"LOW", CLIMATE_FAN_LOW}, {"MEDIUM", CLIMATE_FAN_MEDIUM}, {"HIGH", CLIMATE_FAN_HIGH},
|
||||
{"MIDDLE", CLIMATE_FAN_MIDDLE}, {"FOCUS", CLIMATE_FAN_FOCUS}, {"DIFFUSE", CLIMATE_FAN_DIFFUSE},
|
||||
{"QUIET", CLIMATE_FAN_QUIET},
|
||||
};
|
||||
|
||||
constexpr StringToUint8 CLIMATE_PRESETS_BY_STR[] = {
|
||||
{"ECO", CLIMATE_PRESET_ECO}, {"AWAY", CLIMATE_PRESET_AWAY}, {"BOOST", CLIMATE_PRESET_BOOST},
|
||||
{"COMFORT", CLIMATE_PRESET_COMFORT}, {"HOME", CLIMATE_PRESET_HOME}, {"SLEEP", CLIMATE_PRESET_SLEEP},
|
||||
{"ACTIVITY", CLIMATE_PRESET_ACTIVITY}, {"NONE", CLIMATE_PRESET_NONE},
|
||||
};
|
||||
|
||||
constexpr StringToUint8 CLIMATE_SWING_MODES_BY_STR[] = {
|
||||
{"OFF", CLIMATE_SWING_OFF},
|
||||
{"BOTH", CLIMATE_SWING_BOTH},
|
||||
{"VERTICAL", CLIMATE_SWING_VERTICAL},
|
||||
{"HORIZONTAL", CLIMATE_SWING_HORIZONTAL},
|
||||
};
|
||||
|
||||
void ClimateCall::perform() {
|
||||
this->parent_->control_callback_.call(*this);
|
||||
ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str());
|
||||
@@ -50,47 +86,46 @@ void ClimateCall::perform() {
|
||||
}
|
||||
this->parent_->control(*this);
|
||||
}
|
||||
|
||||
void ClimateCall::validate_() {
|
||||
auto traits = this->parent_->get_traits();
|
||||
if (this->mode_.has_value()) {
|
||||
auto mode = *this->mode_;
|
||||
if (!traits.supports_mode(mode)) {
|
||||
ESP_LOGW(TAG, " Mode %s is not supported by this device!", LOG_STR_ARG(climate_mode_to_string(mode)));
|
||||
ESP_LOGW(TAG, " Mode %s not supported", LOG_STR_ARG(climate_mode_to_string(mode)));
|
||||
this->mode_.reset();
|
||||
}
|
||||
}
|
||||
if (this->custom_fan_mode_.has_value()) {
|
||||
auto custom_fan_mode = *this->custom_fan_mode_;
|
||||
if (!traits.supports_custom_fan_mode(custom_fan_mode)) {
|
||||
ESP_LOGW(TAG, " Fan Mode %s is not supported by this device!", custom_fan_mode.c_str());
|
||||
ESP_LOGW(TAG, " Fan Mode %s not supported", custom_fan_mode.c_str());
|
||||
this->custom_fan_mode_.reset();
|
||||
}
|
||||
} else if (this->fan_mode_.has_value()) {
|
||||
auto fan_mode = *this->fan_mode_;
|
||||
if (!traits.supports_fan_mode(fan_mode)) {
|
||||
ESP_LOGW(TAG, " Fan Mode %s is not supported by this device!",
|
||||
LOG_STR_ARG(climate_fan_mode_to_string(fan_mode)));
|
||||
ESP_LOGW(TAG, " Fan Mode %s not supported", LOG_STR_ARG(climate_fan_mode_to_string(fan_mode)));
|
||||
this->fan_mode_.reset();
|
||||
}
|
||||
}
|
||||
if (this->custom_preset_.has_value()) {
|
||||
auto custom_preset = *this->custom_preset_;
|
||||
if (!traits.supports_custom_preset(custom_preset)) {
|
||||
ESP_LOGW(TAG, " Preset %s is not supported by this device!", custom_preset.c_str());
|
||||
ESP_LOGW(TAG, " Preset %s not supported", custom_preset.c_str());
|
||||
this->custom_preset_.reset();
|
||||
}
|
||||
} else if (this->preset_.has_value()) {
|
||||
auto preset = *this->preset_;
|
||||
if (!traits.supports_preset(preset)) {
|
||||
ESP_LOGW(TAG, " Preset %s is not supported by this device!", LOG_STR_ARG(climate_preset_to_string(preset)));
|
||||
ESP_LOGW(TAG, " Preset %s not supported", LOG_STR_ARG(climate_preset_to_string(preset)));
|
||||
this->preset_.reset();
|
||||
}
|
||||
}
|
||||
if (this->swing_mode_.has_value()) {
|
||||
auto swing_mode = *this->swing_mode_;
|
||||
if (!traits.supports_swing_mode(swing_mode)) {
|
||||
ESP_LOGW(TAG, " Swing Mode %s is not supported by this device!",
|
||||
LOG_STR_ARG(climate_swing_mode_to_string(swing_mode)));
|
||||
ESP_LOGW(TAG, " Swing Mode %s not supported", LOG_STR_ARG(climate_swing_mode_to_string(swing_mode)));
|
||||
this->swing_mode_.reset();
|
||||
}
|
||||
}
|
||||
@@ -99,159 +134,127 @@ void ClimateCall::validate_() {
|
||||
if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
|
||||
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
|
||||
ESP_LOGW(TAG, " Cannot set target temperature for climate device "
|
||||
"with two-point target temperature!");
|
||||
"with two-point target temperature");
|
||||
this->target_temperature_.reset();
|
||||
} else if (std::isnan(target)) {
|
||||
ESP_LOGW(TAG, " Target temperature must not be NAN!");
|
||||
ESP_LOGW(TAG, " Target temperature must not be NAN");
|
||||
this->target_temperature_.reset();
|
||||
}
|
||||
}
|
||||
if (this->target_temperature_low_.has_value() || this->target_temperature_high_.has_value()) {
|
||||
if (!traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
|
||||
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
|
||||
ESP_LOGW(TAG, " Cannot set low/high target temperature for this device!");
|
||||
ESP_LOGW(TAG, " Cannot set low/high target temperature");
|
||||
this->target_temperature_low_.reset();
|
||||
this->target_temperature_high_.reset();
|
||||
}
|
||||
}
|
||||
if (this->target_temperature_low_.has_value() && std::isnan(*this->target_temperature_low_)) {
|
||||
ESP_LOGW(TAG, " Target temperature low must not be NAN!");
|
||||
ESP_LOGW(TAG, " Target temperature low must not be NAN");
|
||||
this->target_temperature_low_.reset();
|
||||
}
|
||||
if (this->target_temperature_high_.has_value() && std::isnan(*this->target_temperature_high_)) {
|
||||
ESP_LOGW(TAG, " Target temperature low must not be NAN!");
|
||||
ESP_LOGW(TAG, " Target temperature high must not be NAN");
|
||||
this->target_temperature_high_.reset();
|
||||
}
|
||||
if (this->target_temperature_low_.has_value() && this->target_temperature_high_.has_value()) {
|
||||
float low = *this->target_temperature_low_;
|
||||
float high = *this->target_temperature_high_;
|
||||
if (low > high) {
|
||||
ESP_LOGW(TAG, " Target temperature low %.2f must be smaller than target temperature high %.2f!", low, high);
|
||||
ESP_LOGW(TAG, " Target temperature low %.2f must be less than target temperature high %.2f", low, high);
|
||||
this->target_temperature_low_.reset();
|
||||
this->target_temperature_high_.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ClimateCall &ClimateCall::set_mode(ClimateMode mode) {
|
||||
this->mode_ = mode;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ClimateCall &ClimateCall::set_mode(const std::string &mode) {
|
||||
if (str_equals_case_insensitive(mode, "OFF")) {
|
||||
this->set_mode(CLIMATE_MODE_OFF);
|
||||
} else if (str_equals_case_insensitive(mode, "AUTO")) {
|
||||
this->set_mode(CLIMATE_MODE_AUTO);
|
||||
} else if (str_equals_case_insensitive(mode, "COOL")) {
|
||||
this->set_mode(CLIMATE_MODE_COOL);
|
||||
} else if (str_equals_case_insensitive(mode, "HEAT")) {
|
||||
this->set_mode(CLIMATE_MODE_HEAT);
|
||||
} else if (str_equals_case_insensitive(mode, "FAN_ONLY")) {
|
||||
this->set_mode(CLIMATE_MODE_FAN_ONLY);
|
||||
} else if (str_equals_case_insensitive(mode, "DRY")) {
|
||||
this->set_mode(CLIMATE_MODE_DRY);
|
||||
} else if (str_equals_case_insensitive(mode, "HEAT_COOL")) {
|
||||
this->set_mode(CLIMATE_MODE_HEAT_COOL);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "'%s' - Unrecognized mode %s", this->parent_->get_name().c_str(), mode.c_str());
|
||||
for (const auto &mode_entry : CLIMATE_MODES_BY_STR) {
|
||||
if (str_equals_case_insensitive(mode, mode_entry.str)) {
|
||||
this->set_mode(static_cast<ClimateMode>(mode_entry.value));
|
||||
return *this;
|
||||
}
|
||||
}
|
||||
ESP_LOGW(TAG, "'%s' - Unrecognized mode %s", this->parent_->get_name().c_str(), mode.c_str());
|
||||
return *this;
|
||||
}
|
||||
|
||||
ClimateCall &ClimateCall::set_fan_mode(ClimateFanMode fan_mode) {
|
||||
this->fan_mode_ = fan_mode;
|
||||
this->custom_fan_mode_.reset();
|
||||
return *this;
|
||||
}
|
||||
|
||||
ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) {
|
||||
if (str_equals_case_insensitive(fan_mode, "ON")) {
|
||||
this->set_fan_mode(CLIMATE_FAN_ON);
|
||||
} else if (str_equals_case_insensitive(fan_mode, "OFF")) {
|
||||
this->set_fan_mode(CLIMATE_FAN_OFF);
|
||||
} else if (str_equals_case_insensitive(fan_mode, "AUTO")) {
|
||||
this->set_fan_mode(CLIMATE_FAN_AUTO);
|
||||
} else if (str_equals_case_insensitive(fan_mode, "LOW")) {
|
||||
this->set_fan_mode(CLIMATE_FAN_LOW);
|
||||
} else if (str_equals_case_insensitive(fan_mode, "MEDIUM")) {
|
||||
this->set_fan_mode(CLIMATE_FAN_MEDIUM);
|
||||
} else if (str_equals_case_insensitive(fan_mode, "HIGH")) {
|
||||
this->set_fan_mode(CLIMATE_FAN_HIGH);
|
||||
} else if (str_equals_case_insensitive(fan_mode, "MIDDLE")) {
|
||||
this->set_fan_mode(CLIMATE_FAN_MIDDLE);
|
||||
} else if (str_equals_case_insensitive(fan_mode, "FOCUS")) {
|
||||
this->set_fan_mode(CLIMATE_FAN_FOCUS);
|
||||
} else if (str_equals_case_insensitive(fan_mode, "DIFFUSE")) {
|
||||
this->set_fan_mode(CLIMATE_FAN_DIFFUSE);
|
||||
} else if (str_equals_case_insensitive(fan_mode, "QUIET")) {
|
||||
this->set_fan_mode(CLIMATE_FAN_QUIET);
|
||||
} else {
|
||||
if (this->parent_->get_traits().supports_custom_fan_mode(fan_mode)) {
|
||||
this->custom_fan_mode_ = fan_mode;
|
||||
this->fan_mode_.reset();
|
||||
} else {
|
||||
ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), fan_mode.c_str());
|
||||
for (const auto &mode_entry : CLIMATE_FAN_MODES_BY_STR) {
|
||||
if (str_equals_case_insensitive(fan_mode, mode_entry.str)) {
|
||||
this->set_fan_mode(static_cast<ClimateFanMode>(mode_entry.value));
|
||||
return *this;
|
||||
}
|
||||
}
|
||||
if (this->parent_->get_traits().supports_custom_fan_mode(fan_mode)) {
|
||||
this->custom_fan_mode_ = fan_mode;
|
||||
this->fan_mode_.reset();
|
||||
} else {
|
||||
ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), fan_mode.c_str());
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
ClimateCall &ClimateCall::set_fan_mode(optional<std::string> fan_mode) {
|
||||
if (fan_mode.has_value()) {
|
||||
this->set_fan_mode(fan_mode.value());
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
ClimateCall &ClimateCall::set_preset(ClimatePreset preset) {
|
||||
this->preset_ = preset;
|
||||
this->custom_preset_.reset();
|
||||
return *this;
|
||||
}
|
||||
|
||||
ClimateCall &ClimateCall::set_preset(const std::string &preset) {
|
||||
if (str_equals_case_insensitive(preset, "ECO")) {
|
||||
this->set_preset(CLIMATE_PRESET_ECO);
|
||||
} else if (str_equals_case_insensitive(preset, "AWAY")) {
|
||||
this->set_preset(CLIMATE_PRESET_AWAY);
|
||||
} else if (str_equals_case_insensitive(preset, "BOOST")) {
|
||||
this->set_preset(CLIMATE_PRESET_BOOST);
|
||||
} else if (str_equals_case_insensitive(preset, "COMFORT")) {
|
||||
this->set_preset(CLIMATE_PRESET_COMFORT);
|
||||
} else if (str_equals_case_insensitive(preset, "HOME")) {
|
||||
this->set_preset(CLIMATE_PRESET_HOME);
|
||||
} else if (str_equals_case_insensitive(preset, "SLEEP")) {
|
||||
this->set_preset(CLIMATE_PRESET_SLEEP);
|
||||
} else if (str_equals_case_insensitive(preset, "ACTIVITY")) {
|
||||
this->set_preset(CLIMATE_PRESET_ACTIVITY);
|
||||
} else if (str_equals_case_insensitive(preset, "NONE")) {
|
||||
this->set_preset(CLIMATE_PRESET_NONE);
|
||||
} else {
|
||||
if (this->parent_->get_traits().supports_custom_preset(preset)) {
|
||||
this->custom_preset_ = preset;
|
||||
this->preset_.reset();
|
||||
} else {
|
||||
ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), preset.c_str());
|
||||
for (const auto &preset_entry : CLIMATE_PRESETS_BY_STR) {
|
||||
if (str_equals_case_insensitive(preset, preset_entry.str)) {
|
||||
this->set_preset(static_cast<ClimatePreset>(preset_entry.value));
|
||||
return *this;
|
||||
}
|
||||
}
|
||||
if (this->parent_->get_traits().supports_custom_preset(preset)) {
|
||||
this->custom_preset_ = preset;
|
||||
this->preset_.reset();
|
||||
} else {
|
||||
ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), preset.c_str());
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
ClimateCall &ClimateCall::set_preset(optional<std::string> preset) {
|
||||
if (preset.has_value()) {
|
||||
this->set_preset(preset.value());
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
ClimateCall &ClimateCall::set_swing_mode(ClimateSwingMode swing_mode) {
|
||||
this->swing_mode_ = swing_mode;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ClimateCall &ClimateCall::set_swing_mode(const std::string &swing_mode) {
|
||||
if (str_equals_case_insensitive(swing_mode, "OFF")) {
|
||||
this->set_swing_mode(CLIMATE_SWING_OFF);
|
||||
} else if (str_equals_case_insensitive(swing_mode, "BOTH")) {
|
||||
this->set_swing_mode(CLIMATE_SWING_BOTH);
|
||||
} else if (str_equals_case_insensitive(swing_mode, "VERTICAL")) {
|
||||
this->set_swing_mode(CLIMATE_SWING_VERTICAL);
|
||||
} else if (str_equals_case_insensitive(swing_mode, "HORIZONTAL")) {
|
||||
this->set_swing_mode(CLIMATE_SWING_HORIZONTAL);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "'%s' - Unrecognized swing mode %s", this->parent_->get_name().c_str(), swing_mode.c_str());
|
||||
for (const auto &mode_entry : CLIMATE_SWING_MODES_BY_STR) {
|
||||
if (str_equals_case_insensitive(swing_mode, mode_entry.str)) {
|
||||
this->set_swing_mode(static_cast<ClimateSwingMode>(mode_entry.value));
|
||||
return *this;
|
||||
}
|
||||
}
|
||||
ESP_LOGW(TAG, "'%s' - Unrecognized swing mode %s", this->parent_->get_name().c_str(), swing_mode.c_str());
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -259,59 +262,71 @@ ClimateCall &ClimateCall::set_target_temperature(float target_temperature) {
|
||||
this->target_temperature_ = target_temperature;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ClimateCall &ClimateCall::set_target_temperature_low(float target_temperature_low) {
|
||||
this->target_temperature_low_ = target_temperature_low;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ClimateCall &ClimateCall::set_target_temperature_high(float target_temperature_high) {
|
||||
this->target_temperature_high_ = target_temperature_high;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ClimateCall &ClimateCall::set_target_humidity(float target_humidity) {
|
||||
this->target_humidity_ = target_humidity;
|
||||
return *this;
|
||||
}
|
||||
|
||||
const optional<ClimateMode> &ClimateCall::get_mode() const { return this->mode_; }
|
||||
const optional<float> &ClimateCall::get_target_temperature() const { return this->target_temperature_; }
|
||||
const optional<float> &ClimateCall::get_target_temperature_low() const { return this->target_temperature_low_; }
|
||||
const optional<float> &ClimateCall::get_target_temperature_high() const { return this->target_temperature_high_; }
|
||||
const optional<float> &ClimateCall::get_target_humidity() const { return this->target_humidity_; }
|
||||
|
||||
const optional<ClimateMode> &ClimateCall::get_mode() const { return this->mode_; }
|
||||
const optional<ClimateFanMode> &ClimateCall::get_fan_mode() const { return this->fan_mode_; }
|
||||
const optional<std::string> &ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; }
|
||||
const optional<ClimatePreset> &ClimateCall::get_preset() const { return this->preset_; }
|
||||
const optional<std::string> &ClimateCall::get_custom_preset() const { return this->custom_preset_; }
|
||||
const optional<ClimateSwingMode> &ClimateCall::get_swing_mode() const { return this->swing_mode_; }
|
||||
const optional<ClimatePreset> &ClimateCall::get_preset() const { return this->preset_; }
|
||||
const optional<std::string> &ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; }
|
||||
const optional<std::string> &ClimateCall::get_custom_preset() const { return this->custom_preset_; }
|
||||
|
||||
ClimateCall &ClimateCall::set_target_temperature_high(optional<float> target_temperature_high) {
|
||||
this->target_temperature_high_ = target_temperature_high;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ClimateCall &ClimateCall::set_target_temperature_low(optional<float> target_temperature_low) {
|
||||
this->target_temperature_low_ = target_temperature_low;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ClimateCall &ClimateCall::set_target_temperature(optional<float> target_temperature) {
|
||||
this->target_temperature_ = target_temperature;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ClimateCall &ClimateCall::set_target_humidity(optional<float> target_humidity) {
|
||||
this->target_humidity_ = target_humidity;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ClimateCall &ClimateCall::set_mode(optional<ClimateMode> mode) {
|
||||
this->mode_ = mode;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ClimateCall &ClimateCall::set_fan_mode(optional<ClimateFanMode> fan_mode) {
|
||||
this->fan_mode_ = fan_mode;
|
||||
this->custom_fan_mode_.reset();
|
||||
return *this;
|
||||
}
|
||||
|
||||
ClimateCall &ClimateCall::set_preset(optional<ClimatePreset> preset) {
|
||||
this->preset_ = preset;
|
||||
this->custom_preset_.reset();
|
||||
return *this;
|
||||
}
|
||||
|
||||
ClimateCall &ClimateCall::set_swing_mode(optional<ClimateSwingMode> swing_mode) {
|
||||
this->swing_mode_ = swing_mode;
|
||||
return *this;
|
||||
@@ -336,6 +351,7 @@ optional<ClimateDeviceRestoreState> Climate::restore_state_() {
|
||||
return {};
|
||||
return recovered;
|
||||
}
|
||||
|
||||
void Climate::save_state_() {
|
||||
#if (defined(USE_ESP_IDF) || (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0))) && \
|
||||
!defined(CLANG_TIDY)
|
||||
@@ -398,6 +414,7 @@ void Climate::save_state_() {
|
||||
|
||||
this->rtc_.save(&state);
|
||||
}
|
||||
|
||||
void Climate::publish_state() {
|
||||
ESP_LOGD(TAG, "'%s' - Sending state:", this->name_.c_str());
|
||||
auto traits = this->get_traits();
|
||||
@@ -469,16 +486,20 @@ ClimateTraits Climate::get_traits() {
|
||||
void Climate::set_visual_min_temperature_override(float visual_min_temperature_override) {
|
||||
this->visual_min_temperature_override_ = visual_min_temperature_override;
|
||||
}
|
||||
|
||||
void Climate::set_visual_max_temperature_override(float visual_max_temperature_override) {
|
||||
this->visual_max_temperature_override_ = visual_max_temperature_override;
|
||||
}
|
||||
|
||||
void Climate::set_visual_temperature_step_override(float target, float current) {
|
||||
this->visual_target_temperature_step_override_ = target;
|
||||
this->visual_current_temperature_step_override_ = current;
|
||||
}
|
||||
|
||||
void Climate::set_visual_min_humidity_override(float visual_min_humidity_override) {
|
||||
this->visual_min_humidity_override_ = visual_min_humidity_override;
|
||||
}
|
||||
|
||||
void Climate::set_visual_max_humidity_override(float visual_max_humidity_override) {
|
||||
this->visual_max_humidity_override_ = visual_max_humidity_override;
|
||||
}
|
||||
@@ -510,6 +531,7 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) {
|
||||
}
|
||||
return call;
|
||||
}
|
||||
|
||||
void ClimateDeviceRestoreState::apply(Climate *climate) {
|
||||
auto traits = climate->get_traits();
|
||||
climate->mode = this->mode;
|
||||
@@ -579,68 +601,68 @@ void Climate::dump_traits_(const char *tag) {
|
||||
auto traits = this->get_traits();
|
||||
ESP_LOGCONFIG(tag, "ClimateTraits:");
|
||||
ESP_LOGCONFIG(tag,
|
||||
" [x] Visual settings:\n"
|
||||
" - Min temperature: %.1f\n"
|
||||
" - Max temperature: %.1f\n"
|
||||
" - Temperature step:\n"
|
||||
" Target: %.1f",
|
||||
" Visual settings:\n"
|
||||
" - Min temperature: %.1f\n"
|
||||
" - Max temperature: %.1f\n"
|
||||
" - Temperature step:\n"
|
||||
" Target: %.1f",
|
||||
traits.get_visual_min_temperature(), traits.get_visual_max_temperature(),
|
||||
traits.get_visual_target_temperature_step());
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) {
|
||||
ESP_LOGCONFIG(tag, " Current: %.1f", traits.get_visual_current_temperature_step());
|
||||
ESP_LOGCONFIG(tag, " Current: %.1f", traits.get_visual_current_temperature_step());
|
||||
}
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY |
|
||||
climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) {
|
||||
ESP_LOGCONFIG(tag,
|
||||
" - Min humidity: %.0f\n"
|
||||
" - Max humidity: %.0f",
|
||||
" - Min humidity: %.0f\n"
|
||||
" - Max humidity: %.0f",
|
||||
traits.get_visual_min_humidity(), traits.get_visual_max_humidity());
|
||||
}
|
||||
if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
|
||||
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
|
||||
ESP_LOGCONFIG(tag, " [x] Supports two-point target temperature");
|
||||
ESP_LOGCONFIG(tag, " Supports two-point target temperature");
|
||||
}
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) {
|
||||
ESP_LOGCONFIG(tag, " [x] Supports current temperature");
|
||||
ESP_LOGCONFIG(tag, " Supports current temperature");
|
||||
}
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
|
||||
ESP_LOGCONFIG(tag, " [x] Supports target humidity");
|
||||
ESP_LOGCONFIG(tag, " Supports target humidity");
|
||||
}
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) {
|
||||
ESP_LOGCONFIG(tag, " [x] Supports current humidity");
|
||||
ESP_LOGCONFIG(tag, " Supports current humidity");
|
||||
}
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) {
|
||||
ESP_LOGCONFIG(tag, " [x] Supports action");
|
||||
ESP_LOGCONFIG(tag, " Supports action");
|
||||
}
|
||||
if (!traits.get_supported_modes().empty()) {
|
||||
ESP_LOGCONFIG(tag, " [x] Supported modes:");
|
||||
ESP_LOGCONFIG(tag, " Supported modes:");
|
||||
for (ClimateMode m : traits.get_supported_modes())
|
||||
ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_mode_to_string(m)));
|
||||
ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_mode_to_string(m)));
|
||||
}
|
||||
if (!traits.get_supported_fan_modes().empty()) {
|
||||
ESP_LOGCONFIG(tag, " [x] Supported fan modes:");
|
||||
ESP_LOGCONFIG(tag, " Supported fan modes:");
|
||||
for (ClimateFanMode m : traits.get_supported_fan_modes())
|
||||
ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_fan_mode_to_string(m)));
|
||||
ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_fan_mode_to_string(m)));
|
||||
}
|
||||
if (!traits.get_supported_custom_fan_modes().empty()) {
|
||||
ESP_LOGCONFIG(tag, " [x] Supported custom fan modes:");
|
||||
ESP_LOGCONFIG(tag, " Supported custom fan modes:");
|
||||
for (const std::string &s : traits.get_supported_custom_fan_modes())
|
||||
ESP_LOGCONFIG(tag, " - %s", s.c_str());
|
||||
ESP_LOGCONFIG(tag, " - %s", s.c_str());
|
||||
}
|
||||
if (!traits.get_supported_presets().empty()) {
|
||||
ESP_LOGCONFIG(tag, " [x] Supported presets:");
|
||||
ESP_LOGCONFIG(tag, " Supported presets:");
|
||||
for (ClimatePreset p : traits.get_supported_presets())
|
||||
ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_preset_to_string(p)));
|
||||
ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_preset_to_string(p)));
|
||||
}
|
||||
if (!traits.get_supported_custom_presets().empty()) {
|
||||
ESP_LOGCONFIG(tag, " [x] Supported custom presets:");
|
||||
ESP_LOGCONFIG(tag, " Supported custom presets:");
|
||||
for (const std::string &s : traits.get_supported_custom_presets())
|
||||
ESP_LOGCONFIG(tag, " - %s", s.c_str());
|
||||
ESP_LOGCONFIG(tag, " - %s", s.c_str());
|
||||
}
|
||||
if (!traits.get_supported_swing_modes().empty()) {
|
||||
ESP_LOGCONFIG(tag, " [x] Supported swing modes:");
|
||||
ESP_LOGCONFIG(tag, " Supported swing modes:");
|
||||
for (ClimateSwingMode m : traits.get_supported_swing_modes())
|
||||
ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_swing_mode_to_string(m)));
|
||||
ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_swing_mode_to_string(m)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -93,30 +93,31 @@ class ClimateCall {
|
||||
|
||||
void perform();
|
||||
|
||||
const optional<ClimateMode> &get_mode() const;
|
||||
const optional<float> &get_target_temperature() const;
|
||||
const optional<float> &get_target_temperature_low() const;
|
||||
const optional<float> &get_target_temperature_high() const;
|
||||
const optional<float> &get_target_humidity() const;
|
||||
|
||||
const optional<ClimateMode> &get_mode() const;
|
||||
const optional<ClimateFanMode> &get_fan_mode() const;
|
||||
const optional<ClimateSwingMode> &get_swing_mode() const;
|
||||
const optional<std::string> &get_custom_fan_mode() const;
|
||||
const optional<ClimatePreset> &get_preset() const;
|
||||
const optional<std::string> &get_custom_fan_mode() const;
|
||||
const optional<std::string> &get_custom_preset() const;
|
||||
|
||||
protected:
|
||||
void validate_();
|
||||
|
||||
Climate *const parent_;
|
||||
optional<ClimateMode> mode_;
|
||||
optional<float> target_temperature_;
|
||||
optional<float> target_temperature_low_;
|
||||
optional<float> target_temperature_high_;
|
||||
optional<float> target_humidity_;
|
||||
optional<ClimateMode> mode_;
|
||||
optional<ClimateFanMode> fan_mode_;
|
||||
optional<ClimateSwingMode> swing_mode_;
|
||||
optional<std::string> custom_fan_mode_;
|
||||
optional<ClimatePreset> preset_;
|
||||
optional<std::string> custom_fan_mode_;
|
||||
optional<std::string> custom_preset_;
|
||||
};
|
||||
|
||||
@@ -169,47 +170,6 @@ class Climate : public EntityBase {
|
||||
public:
|
||||
Climate() {}
|
||||
|
||||
/// The active mode of the climate device.
|
||||
ClimateMode mode{CLIMATE_MODE_OFF};
|
||||
|
||||
/// The active state of the climate device.
|
||||
ClimateAction action{CLIMATE_ACTION_OFF};
|
||||
|
||||
/// The current temperature of the climate device, as reported from the integration.
|
||||
float current_temperature{NAN};
|
||||
|
||||
/// The current humidity of the climate device, as reported from the integration.
|
||||
float current_humidity{NAN};
|
||||
|
||||
union {
|
||||
/// The target temperature of the climate device.
|
||||
float target_temperature;
|
||||
struct {
|
||||
/// The minimum target temperature of the climate device, for climate devices with split target temperature.
|
||||
float target_temperature_low{NAN};
|
||||
/// The maximum target temperature of the climate device, for climate devices with split target temperature.
|
||||
float target_temperature_high{NAN};
|
||||
};
|
||||
};
|
||||
|
||||
/// The target humidity of the climate device.
|
||||
float target_humidity;
|
||||
|
||||
/// The active fan mode of the climate device.
|
||||
optional<ClimateFanMode> fan_mode;
|
||||
|
||||
/// The active swing mode of the climate device.
|
||||
ClimateSwingMode swing_mode;
|
||||
|
||||
/// The active custom fan mode of the climate device.
|
||||
optional<std::string> custom_fan_mode;
|
||||
|
||||
/// The active preset of the climate device.
|
||||
optional<ClimatePreset> preset;
|
||||
|
||||
/// The active custom preset mode of the climate device.
|
||||
optional<std::string> custom_preset;
|
||||
|
||||
/** Add a callback for the climate device state, each time the state of the climate device is updated
|
||||
* (using publish_state), this callback will be called.
|
||||
*
|
||||
@@ -251,6 +211,47 @@ class Climate : public EntityBase {
|
||||
void set_visual_min_humidity_override(float visual_min_humidity_override);
|
||||
void set_visual_max_humidity_override(float visual_max_humidity_override);
|
||||
|
||||
/// The current temperature of the climate device, as reported from the integration.
|
||||
float current_temperature{NAN};
|
||||
|
||||
/// The current humidity of the climate device, as reported from the integration.
|
||||
float current_humidity{NAN};
|
||||
|
||||
union {
|
||||
/// The target temperature of the climate device.
|
||||
float target_temperature;
|
||||
struct {
|
||||
/// The minimum target temperature of the climate device, for climate devices with split target temperature.
|
||||
float target_temperature_low{NAN};
|
||||
/// The maximum target temperature of the climate device, for climate devices with split target temperature.
|
||||
float target_temperature_high{NAN};
|
||||
};
|
||||
};
|
||||
|
||||
/// The target humidity of the climate device.
|
||||
float target_humidity;
|
||||
|
||||
/// The active fan mode of the climate device.
|
||||
optional<ClimateFanMode> fan_mode;
|
||||
|
||||
/// The active preset of the climate device.
|
||||
optional<ClimatePreset> preset;
|
||||
|
||||
/// The active custom fan mode of the climate device.
|
||||
optional<std::string> custom_fan_mode;
|
||||
|
||||
/// The active custom preset mode of the climate device.
|
||||
optional<std::string> custom_preset;
|
||||
|
||||
/// The active mode of the climate device.
|
||||
ClimateMode mode{CLIMATE_MODE_OFF};
|
||||
|
||||
/// The active state of the climate device.
|
||||
ClimateAction action{CLIMATE_ACTION_OFF};
|
||||
|
||||
/// The active swing mode of the climate device.
|
||||
ClimateSwingMode swing_mode{CLIMATE_SWING_OFF};
|
||||
|
||||
protected:
|
||||
friend ClimateCall;
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "climate_mode.h"
|
||||
#include <set>
|
||||
#include "climate_mode.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
|
||||
@@ -109,44 +109,12 @@ class ClimateTraits {
|
||||
|
||||
void set_supported_modes(std::set<ClimateMode> modes) { this->supported_modes_ = std::move(modes); }
|
||||
void add_supported_mode(ClimateMode mode) { this->supported_modes_.insert(mode); }
|
||||
ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
|
||||
void set_supports_auto_mode(bool supports_auto_mode) { set_mode_support_(CLIMATE_MODE_AUTO, supports_auto_mode); }
|
||||
ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
|
||||
void set_supports_cool_mode(bool supports_cool_mode) { set_mode_support_(CLIMATE_MODE_COOL, supports_cool_mode); }
|
||||
ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
|
||||
void set_supports_heat_mode(bool supports_heat_mode) { set_mode_support_(CLIMATE_MODE_HEAT, supports_heat_mode); }
|
||||
ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
|
||||
void set_supports_heat_cool_mode(bool supported) { set_mode_support_(CLIMATE_MODE_HEAT_COOL, supported); }
|
||||
ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
|
||||
void set_supports_fan_only_mode(bool supports_fan_only_mode) {
|
||||
set_mode_support_(CLIMATE_MODE_FAN_ONLY, supports_fan_only_mode);
|
||||
}
|
||||
ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
|
||||
void set_supports_dry_mode(bool supports_dry_mode) { set_mode_support_(CLIMATE_MODE_DRY, supports_dry_mode); }
|
||||
bool supports_mode(ClimateMode mode) const { return this->supported_modes_.count(mode); }
|
||||
const std::set<ClimateMode> &get_supported_modes() const { return this->supported_modes_; }
|
||||
|
||||
void set_supported_fan_modes(std::set<ClimateFanMode> modes) { this->supported_fan_modes_ = std::move(modes); }
|
||||
void add_supported_fan_mode(ClimateFanMode mode) { this->supported_fan_modes_.insert(mode); }
|
||||
void add_supported_custom_fan_mode(const std::string &mode) { this->supported_custom_fan_modes_.insert(mode); }
|
||||
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
|
||||
void set_supports_fan_mode_on(bool supported) { set_fan_mode_support_(CLIMATE_FAN_ON, supported); }
|
||||
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
|
||||
void set_supports_fan_mode_off(bool supported) { set_fan_mode_support_(CLIMATE_FAN_OFF, supported); }
|
||||
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
|
||||
void set_supports_fan_mode_auto(bool supported) { set_fan_mode_support_(CLIMATE_FAN_AUTO, supported); }
|
||||
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
|
||||
void set_supports_fan_mode_low(bool supported) { set_fan_mode_support_(CLIMATE_FAN_LOW, supported); }
|
||||
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
|
||||
void set_supports_fan_mode_medium(bool supported) { set_fan_mode_support_(CLIMATE_FAN_MEDIUM, supported); }
|
||||
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
|
||||
void set_supports_fan_mode_high(bool supported) { set_fan_mode_support_(CLIMATE_FAN_HIGH, supported); }
|
||||
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
|
||||
void set_supports_fan_mode_middle(bool supported) { set_fan_mode_support_(CLIMATE_FAN_MIDDLE, supported); }
|
||||
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
|
||||
void set_supports_fan_mode_focus(bool supported) { set_fan_mode_support_(CLIMATE_FAN_FOCUS, supported); }
|
||||
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
|
||||
void set_supports_fan_mode_diffuse(bool supported) { set_fan_mode_support_(CLIMATE_FAN_DIFFUSE, supported); }
|
||||
bool supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.count(fan_mode); }
|
||||
bool get_supports_fan_modes() const {
|
||||
return !this->supported_fan_modes_.empty() || !this->supported_custom_fan_modes_.empty();
|
||||
@@ -178,16 +146,6 @@ class ClimateTraits {
|
||||
|
||||
void set_supported_swing_modes(std::set<ClimateSwingMode> modes) { this->supported_swing_modes_ = std::move(modes); }
|
||||
void add_supported_swing_mode(ClimateSwingMode mode) { this->supported_swing_modes_.insert(mode); }
|
||||
ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20")
|
||||
void set_supports_swing_mode_off(bool supported) { set_swing_mode_support_(CLIMATE_SWING_OFF, supported); }
|
||||
ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20")
|
||||
void set_supports_swing_mode_both(bool supported) { set_swing_mode_support_(CLIMATE_SWING_BOTH, supported); }
|
||||
ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20")
|
||||
void set_supports_swing_mode_vertical(bool supported) { set_swing_mode_support_(CLIMATE_SWING_VERTICAL, supported); }
|
||||
ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20")
|
||||
void set_supports_swing_mode_horizontal(bool supported) {
|
||||
set_swing_mode_support_(CLIMATE_SWING_HORIZONTAL, supported);
|
||||
}
|
||||
bool supports_swing_mode(ClimateSwingMode swing_mode) const { return this->supported_swing_modes_.count(swing_mode); }
|
||||
bool get_supports_swing_modes() const { return !this->supported_swing_modes_.empty(); }
|
||||
const std::set<ClimateSwingMode> &get_supported_swing_modes() const { return this->supported_swing_modes_; }
|
||||
|
||||
@@ -8,7 +8,10 @@ static const char *const TAG = "climate_ir";
|
||||
|
||||
climate::ClimateTraits ClimateIR::traits() {
|
||||
auto traits = climate::ClimateTraits();
|
||||
traits.set_supports_current_temperature(this->sensor_ != nullptr);
|
||||
if (this->sensor_ != nullptr) {
|
||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
|
||||
}
|
||||
|
||||
traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_HEAT_COOL});
|
||||
if (this->supports_cool_)
|
||||
traits.add_supported_mode(climate::CLIMATE_MODE_COOL);
|
||||
@@ -19,7 +22,6 @@ climate::ClimateTraits ClimateIR::traits() {
|
||||
if (this->supports_fan_only_)
|
||||
traits.add_supported_mode(climate::CLIMATE_MODE_FAN_ONLY);
|
||||
|
||||
traits.set_supports_two_point_target_temperature(false);
|
||||
traits.set_visual_min_temperature(this->minimum_temperature_);
|
||||
traits.set_visual_max_temperature(this->maximum_temperature_);
|
||||
traits.set_visual_temperature_step(this->temperature_step_);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "cover.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <strings.h>
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace cover {
|
||||
@@ -144,21 +144,7 @@ CoverCall &CoverCall::set_stop(bool stop) {
|
||||
bool CoverCall::get_stop() const { return this->stop_; }
|
||||
|
||||
CoverCall Cover::make_call() { return {this}; }
|
||||
void Cover::open() {
|
||||
auto call = this->make_call();
|
||||
call.set_command_open();
|
||||
call.perform();
|
||||
}
|
||||
void Cover::close() {
|
||||
auto call = this->make_call();
|
||||
call.set_command_close();
|
||||
call.perform();
|
||||
}
|
||||
void Cover::stop() {
|
||||
auto call = this->make_call();
|
||||
call.set_command_stop();
|
||||
call.perform();
|
||||
}
|
||||
|
||||
void Cover::add_on_state_callback(std::function<void()> &&f) { this->state_callback_.add(std::move(f)); }
|
||||
void Cover::publish_state(bool save) {
|
||||
this->position = clamp(this->position, 0.0f, 1.0f);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "esphome/core/entity_base.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
|
||||
#include "cover_traits.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -125,25 +126,6 @@ class Cover : public EntityBase, public EntityBase_DeviceClass {
|
||||
|
||||
/// Construct a new cover call used to control the cover.
|
||||
CoverCall make_call();
|
||||
/** Open the cover.
|
||||
*
|
||||
* This is a legacy method and may be removed later, please use `.make_call()` instead.
|
||||
*/
|
||||
ESPDEPRECATED("open() is deprecated, use make_call().set_command_open().perform() instead.", "2021.9")
|
||||
void open();
|
||||
/** Close the cover.
|
||||
*
|
||||
* This is a legacy method and may be removed later, please use `.make_call()` instead.
|
||||
*/
|
||||
ESPDEPRECATED("close() is deprecated, use make_call().set_command_close().perform() instead.", "2021.9")
|
||||
void close();
|
||||
/** Stop the cover.
|
||||
*
|
||||
* This is a legacy method and may be removed later, please use `.make_call()` instead.
|
||||
* As per solution from issue #2885 the call should include perform()
|
||||
*/
|
||||
ESPDEPRECATED("stop() is deprecated, use make_call().set_command_stop().perform() instead.", "2021.9")
|
||||
void stop();
|
||||
|
||||
void add_on_state_callback(std::function<void()> &&f);
|
||||
|
||||
|
||||
@@ -241,9 +241,7 @@ uint8_t DaikinArcClimate::humidity_() {
|
||||
|
||||
climate::ClimateTraits DaikinArcClimate::traits() {
|
||||
climate::ClimateTraits traits = climate_ir::ClimateIR::traits();
|
||||
traits.set_supports_current_temperature(true);
|
||||
traits.set_supports_current_humidity(false);
|
||||
traits.set_supports_target_humidity(true);
|
||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE | climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY);
|
||||
traits.set_visual_min_humidity(38);
|
||||
traits.set_visual_max_humidity(52);
|
||||
return traits;
|
||||
|
||||
@@ -82,16 +82,14 @@ class DemoClimate : public climate::Climate, public Component {
|
||||
climate::ClimateTraits traits{};
|
||||
switch (type_) {
|
||||
case DemoClimateType::TYPE_1:
|
||||
traits.set_supports_current_temperature(true);
|
||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE | climate::CLIMATE_SUPPORTS_ACTION);
|
||||
traits.set_supported_modes({
|
||||
climate::CLIMATE_MODE_OFF,
|
||||
climate::CLIMATE_MODE_HEAT,
|
||||
});
|
||||
traits.set_supports_action(true);
|
||||
traits.set_visual_temperature_step(0.5);
|
||||
break;
|
||||
case DemoClimateType::TYPE_2:
|
||||
traits.set_supports_current_temperature(false);
|
||||
traits.set_supported_modes({
|
||||
climate::CLIMATE_MODE_OFF,
|
||||
climate::CLIMATE_MODE_HEAT,
|
||||
@@ -100,7 +98,7 @@ class DemoClimate : public climate::Climate, public Component {
|
||||
climate::CLIMATE_MODE_DRY,
|
||||
climate::CLIMATE_MODE_FAN_ONLY,
|
||||
});
|
||||
traits.set_supports_action(true);
|
||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_ACTION);
|
||||
traits.set_supported_fan_modes({
|
||||
climate::CLIMATE_FAN_ON,
|
||||
climate::CLIMATE_FAN_OFF,
|
||||
@@ -123,8 +121,8 @@ class DemoClimate : public climate::Climate, public Component {
|
||||
traits.set_supported_custom_presets({"My Preset"});
|
||||
break;
|
||||
case DemoClimateType::TYPE_3:
|
||||
traits.set_supports_current_temperature(true);
|
||||
traits.set_supports_two_point_target_temperature(true);
|
||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE |
|
||||
climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE);
|
||||
traits.set_supported_modes({
|
||||
climate::CLIMATE_MODE_OFF,
|
||||
climate::CLIMATE_MODE_COOL,
|
||||
|
||||
@@ -103,7 +103,7 @@ bool EPaperBase::is_idle_() {
|
||||
if (this->busy_pin_ == nullptr) {
|
||||
return true;
|
||||
}
|
||||
return !this->busy_pin_->digital_read();
|
||||
return this->busy_pin_->digital_read();
|
||||
}
|
||||
|
||||
void EPaperBase::reset() {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import contextlib
|
||||
from dataclasses import dataclass
|
||||
import itertools
|
||||
import logging
|
||||
@@ -102,6 +103,10 @@ COMPILER_OPTIMIZATIONS = {
|
||||
"SIZE": "CONFIG_COMPILER_OPTIMIZATION_SIZE",
|
||||
}
|
||||
|
||||
# Socket limit configuration for ESP-IDF
|
||||
# ESP-IDF CONFIG_LWIP_MAX_SOCKETS has range 1-253, default 10
|
||||
DEFAULT_MAX_SOCKETS = 10 # ESP-IDF default
|
||||
|
||||
ARDUINO_ALLOWED_VARIANTS = [
|
||||
VARIANT_ESP32,
|
||||
VARIANT_ESP32C3,
|
||||
@@ -746,6 +751,72 @@ CONFIG_SCHEMA = cv.All(
|
||||
FINAL_VALIDATE_SCHEMA = cv.Schema(final_validate)
|
||||
|
||||
|
||||
def _configure_lwip_max_sockets(conf: dict) -> None:
|
||||
"""Calculate and set CONFIG_LWIP_MAX_SOCKETS based on component needs.
|
||||
|
||||
Socket component tracks consumer needs via consume_sockets() called during config validation.
|
||||
This function runs in to_code() after all components have registered their socket needs.
|
||||
User-provided sdkconfig_options take precedence.
|
||||
"""
|
||||
from esphome.components.socket import KEY_SOCKET_CONSUMERS
|
||||
|
||||
# Check if user manually specified CONFIG_LWIP_MAX_SOCKETS
|
||||
user_max_sockets = conf.get(CONF_SDKCONFIG_OPTIONS, {}).get(
|
||||
"CONFIG_LWIP_MAX_SOCKETS"
|
||||
)
|
||||
|
||||
socket_consumers: dict[str, int] = CORE.data.get(KEY_SOCKET_CONSUMERS, {})
|
||||
total_sockets = sum(socket_consumers.values())
|
||||
|
||||
# Early return if no sockets registered and no user override
|
||||
if total_sockets == 0 and user_max_sockets is None:
|
||||
return
|
||||
|
||||
components_list = ", ".join(
|
||||
f"{name}={count}" for name, count in sorted(socket_consumers.items())
|
||||
)
|
||||
|
||||
# User specified their own value - respect it but warn if insufficient
|
||||
if user_max_sockets is not None:
|
||||
_LOGGER.info(
|
||||
"Using user-provided CONFIG_LWIP_MAX_SOCKETS: %s",
|
||||
user_max_sockets,
|
||||
)
|
||||
|
||||
# Warn if user's value is less than what components need
|
||||
if total_sockets > 0:
|
||||
user_sockets_int = 0
|
||||
with contextlib.suppress(ValueError, TypeError):
|
||||
user_sockets_int = int(user_max_sockets)
|
||||
|
||||
if user_sockets_int < total_sockets:
|
||||
_LOGGER.warning(
|
||||
"CONFIG_LWIP_MAX_SOCKETS is set to %d but your configuration "
|
||||
"needs %d sockets (registered: %s). You may experience socket "
|
||||
"exhaustion errors. Consider increasing to at least %d.",
|
||||
user_sockets_int,
|
||||
total_sockets,
|
||||
components_list,
|
||||
total_sockets,
|
||||
)
|
||||
# User's value already added via sdkconfig_options processing
|
||||
return
|
||||
|
||||
# Auto-calculate based on component needs
|
||||
# Use at least the ESP-IDF default (10), or the total needed by components
|
||||
max_sockets = max(DEFAULT_MAX_SOCKETS, total_sockets)
|
||||
|
||||
log_level = logging.INFO if max_sockets > DEFAULT_MAX_SOCKETS else logging.DEBUG
|
||||
_LOGGER.log(
|
||||
log_level,
|
||||
"Setting CONFIG_LWIP_MAX_SOCKETS to %d (registered: %s)",
|
||||
max_sockets,
|
||||
components_list,
|
||||
)
|
||||
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_MAX_SOCKETS", max_sockets)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
cg.add_platformio_option("board", config[CONF_BOARD])
|
||||
cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE])
|
||||
@@ -779,6 +850,16 @@ async def to_code(config):
|
||||
Path(__file__).parent / "post_build.py.script",
|
||||
)
|
||||
|
||||
# In testing mode, add IRAM fix script to allow linking grouped component tests
|
||||
# Similar to ESP8266's approach but for ESP-IDF
|
||||
if CORE.testing_mode:
|
||||
cg.add_build_flag("-DESPHOME_TESTING_MODE")
|
||||
add_extra_script(
|
||||
"pre",
|
||||
"iram_fix.py",
|
||||
Path(__file__).parent / "iram_fix.py.script",
|
||||
)
|
||||
|
||||
if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
|
||||
cg.add_platformio_option("framework", "espidf")
|
||||
cg.add_build_flag("-DUSE_ESP_IDF")
|
||||
@@ -805,6 +886,7 @@ async def to_code(config):
|
||||
add_idf_sdkconfig_option("CONFIG_AUTOSTART_ARDUINO", True)
|
||||
add_idf_sdkconfig_option("CONFIG_MBEDTLS_PSK_MODES", True)
|
||||
add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True)
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_PHY_REDUCE_TX_POWER", True)
|
||||
|
||||
cg.add_build_flag("-Wno-nonnull-compare")
|
||||
|
||||
@@ -855,6 +937,9 @@ async def to_code(config):
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES", False)
|
||||
if not advanced.get(CONF_ENABLE_LWIP_BRIDGE_INTERFACE, False):
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_BRIDGEIF_MAX_PORTS", 0)
|
||||
|
||||
_configure_lwip_max_sockets(conf)
|
||||
|
||||
if advanced.get(CONF_EXECUTE_FROM_PSRAM, False):
|
||||
add_idf_sdkconfig_option("CONFIG_SPIRAM_FETCH_INSTRUCTIONS", True)
|
||||
add_idf_sdkconfig_option("CONFIG_SPIRAM_RODATA", True)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
#include <esp_idf_version.h>
|
||||
#include <esp_ota_ops.h>
|
||||
#include <esp_task_wdt.h>
|
||||
#include <esp_timer.h>
|
||||
#include <soc/rtc.h>
|
||||
@@ -52,6 +53,16 @@ void arch_init() {
|
||||
disableCore1WDT();
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// If the bootloader was compiled with CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE the current
|
||||
// partition will get rolled back unless it is marked as valid.
|
||||
esp_ota_img_states_t state;
|
||||
const esp_partition_t *running = esp_ota_get_running_partition();
|
||||
if (esp_ota_get_state_partition(running, &state) == ESP_OK) {
|
||||
if (state == ESP_OTA_IMG_PENDING_VERIFY) {
|
||||
esp_ota_mark_app_valid_cancel_rollback();
|
||||
}
|
||||
}
|
||||
}
|
||||
void IRAM_ATTR HOT arch_feed_wdt() { esp_task_wdt_reset(); }
|
||||
|
||||
|
||||
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)
|
||||
@@ -61,12 +61,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
|
||||
this->address_str_ = "";
|
||||
} else {
|
||||
char buf[18];
|
||||
uint8_t mac[6] = {
|
||||
(uint8_t) ((this->address_ >> 40) & 0xff), (uint8_t) ((this->address_ >> 32) & 0xff),
|
||||
(uint8_t) ((this->address_ >> 24) & 0xff), (uint8_t) ((this->address_ >> 16) & 0xff),
|
||||
(uint8_t) ((this->address_ >> 8) & 0xff), (uint8_t) ((this->address_ >> 0) & 0xff),
|
||||
};
|
||||
format_mac_addr_upper(mac, buf);
|
||||
format_mac_addr_upper(this->remote_bda_, buf);
|
||||
this->address_str_ = buf;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_MODE, CONF_PORT
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@ayufan"]
|
||||
AUTO_LOAD = ["camera"]
|
||||
@@ -13,13 +14,27 @@ Mode = esp32_camera_web_server_ns.enum("Mode")
|
||||
|
||||
MODES = {"STREAM": Mode.STREAM, "SNAPSHOT": Mode.SNAPSHOT}
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(CameraWebServer),
|
||||
cv.Required(CONF_PORT): cv.port,
|
||||
cv.Required(CONF_MODE): cv.enum(MODES, upper=True),
|
||||
},
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
def _consume_camera_web_server_sockets(config: ConfigType) -> ConfigType:
|
||||
"""Register socket needs for camera web server."""
|
||||
from esphome.components import socket
|
||||
|
||||
# Each camera web server instance needs 1 listening socket + 2 client connections
|
||||
sockets_needed = 3
|
||||
socket.consume_sockets(sockets_needed, "esp32_camera_web_server")(config)
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(CameraWebServer),
|
||||
cv.Required(CONF_PORT): cv.port,
|
||||
cv.Required(CONF_MODE): cv.enum(MODES, upper=True),
|
||||
},
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
_consume_camera_web_server_sockets,
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
|
||||
@@ -95,7 +95,7 @@ async def to_code(config):
|
||||
if framework_ver >= cv.Version(5, 5, 0):
|
||||
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.1.5")
|
||||
esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.3")
|
||||
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.5.11")
|
||||
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.6.1")
|
||||
else:
|
||||
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.13.0")
|
||||
esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0")
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import binary_sensor, esp32_ble, output
|
||||
from esphome.components import binary_sensor, esp32_ble, improv_base, output
|
||||
from esphome.components.esp32_ble import BTLoggers
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_ON_STATE, CONF_TRIGGER_ID
|
||||
|
||||
AUTO_LOAD = ["esp32_ble_server"]
|
||||
AUTO_LOAD = ["esp32_ble_server", "improv_base"]
|
||||
CODEOWNERS = ["@jesserockz"]
|
||||
DEPENDENCIES = ["wifi", "esp32"]
|
||||
|
||||
@@ -20,6 +20,7 @@ CONF_ON_STOP = "on_stop"
|
||||
CONF_STATUS_INDICATOR = "status_indicator"
|
||||
CONF_WIFI_TIMEOUT = "wifi_timeout"
|
||||
|
||||
|
||||
improv_ns = cg.esphome_ns.namespace("improv")
|
||||
Error = improv_ns.enum("Error")
|
||||
State = improv_ns.enum("State")
|
||||
@@ -43,55 +44,63 @@ ESP32ImprovStoppedTrigger = esp32_improv_ns.class_(
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(ESP32ImprovComponent),
|
||||
cv.Required(CONF_AUTHORIZER): cv.Any(
|
||||
cv.none, cv.use_id(binary_sensor.BinarySensor)
|
||||
),
|
||||
cv.Optional(CONF_STATUS_INDICATOR): cv.use_id(output.BinaryOutput),
|
||||
cv.Optional(
|
||||
CONF_IDENTIFY_DURATION, default="10s"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(
|
||||
CONF_AUTHORIZED_DURATION, default="1min"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(
|
||||
CONF_WIFI_TIMEOUT, default="1min"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_ON_PROVISIONED): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
ESP32ImprovProvisionedTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_PROVISIONING): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
ESP32ImprovProvisioningTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_START): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESP32ImprovStartTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_STATE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESP32ImprovStateTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_STOP): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
ESP32ImprovStoppedTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(ESP32ImprovComponent),
|
||||
cv.Required(CONF_AUTHORIZER): cv.Any(
|
||||
cv.none, cv.use_id(binary_sensor.BinarySensor)
|
||||
),
|
||||
cv.Optional(CONF_STATUS_INDICATOR): cv.use_id(output.BinaryOutput),
|
||||
cv.Optional(
|
||||
CONF_IDENTIFY_DURATION, default="10s"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(
|
||||
CONF_AUTHORIZED_DURATION, default="1min"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(
|
||||
CONF_WIFI_TIMEOUT, default="1min"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_ON_PROVISIONED): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
ESP32ImprovProvisionedTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_PROVISIONING): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
ESP32ImprovProvisioningTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_START): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
ESP32ImprovStartTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_STATE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
ESP32ImprovStateTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_STOP): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
ESP32ImprovStoppedTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(improv_base.IMPROV_SCHEMA)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
@@ -102,7 +111,8 @@ async def to_code(config):
|
||||
await cg.register_component(var, config)
|
||||
|
||||
cg.add_define("USE_IMPROV")
|
||||
cg.add_library("improv/Improv", "1.2.4")
|
||||
|
||||
await improv_base.setup_improv_core(var, config)
|
||||
|
||||
cg.add(var.set_identify_duration(config[CONF_IDENTIFY_DURATION]))
|
||||
cg.add(var.set_authorized_duration(config[CONF_AUTHORIZED_DURATION]))
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
#include "esp32_improv_component.h"
|
||||
|
||||
#include "esphome/components/bytebuffer/bytebuffer.h"
|
||||
#include "esphome/components/esp32_ble/ble.h"
|
||||
#include "esphome/components/esp32_ble_server/ble_2902.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/components/bytebuffer/bytebuffer.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
@@ -384,17 +384,32 @@ void ESP32ImprovComponent::check_wifi_connection_() {
|
||||
this->connecting_sta_ = {};
|
||||
this->cancel_timeout("wifi-connect-timeout");
|
||||
|
||||
std::vector<std::string> urls = {ESPHOME_MY_LINK};
|
||||
// Build URL list with minimal allocations
|
||||
// Maximum 3 URLs: custom next_url + ESPHOME_MY_LINK + webserver URL
|
||||
std::string url_strings[3];
|
||||
size_t url_count = 0;
|
||||
|
||||
// Add next_url if configured (should be first per Improv BLE spec)
|
||||
std::string next_url = this->get_formatted_next_url_();
|
||||
if (!next_url.empty()) {
|
||||
url_strings[url_count++] = std::move(next_url);
|
||||
}
|
||||
|
||||
// Add default URLs for backward compatibility
|
||||
url_strings[url_count++] = ESPHOME_MY_LINK;
|
||||
#ifdef USE_WEBSERVER
|
||||
for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) {
|
||||
if (ip.is_ip4()) {
|
||||
std::string webserver_url = "http://" + ip.str() + ":" + to_string(USE_WEBSERVER_PORT);
|
||||
urls.push_back(webserver_url);
|
||||
char url_buffer[64];
|
||||
snprintf(url_buffer, sizeof(url_buffer), "http://%s:%d", ip.str().c_str(), USE_WEBSERVER_PORT);
|
||||
url_strings[url_count++] = url_buffer;
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
std::vector<uint8_t> data = improv::build_rpc_response(improv::WIFI_SETTINGS, urls);
|
||||
// Pass to build_rpc_response using vector constructor from iterators to avoid extra copies
|
||||
std::vector<uint8_t> data = improv::build_rpc_response(
|
||||
improv::WIFI_SETTINGS, std::vector<std::string>(url_strings, url_strings + url_count));
|
||||
this->send_response_(data);
|
||||
} else if (this->is_active() && this->state_ != improv::STATE_PROVISIONED) {
|
||||
ESP_LOGD(TAG, "WiFi provisioned externally");
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#include "esphome/components/esp32_ble_server/ble_characteristic.h"
|
||||
#include "esphome/components/esp32_ble_server/ble_server.h"
|
||||
#include "esphome/components/improv_base/improv_base.h"
|
||||
#include "esphome/components/wifi/wifi_component.h"
|
||||
|
||||
#ifdef USE_ESP32_IMPROV_STATE_CALLBACK
|
||||
@@ -32,7 +33,7 @@ namespace esp32_improv {
|
||||
|
||||
using namespace esp32_ble_server;
|
||||
|
||||
class ESP32ImprovComponent : public Component {
|
||||
class ESP32ImprovComponent : public Component, public improv_base::ImprovBase {
|
||||
public:
|
||||
ESP32ImprovComponent();
|
||||
void dump_config() override;
|
||||
|
||||
@@ -103,7 +103,16 @@ def ota_esphome_final_validate(config):
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
def _consume_ota_sockets(config: ConfigType) -> ConfigType:
|
||||
"""Register socket needs for OTA component."""
|
||||
from esphome.components import socket
|
||||
|
||||
# OTA needs 1 listening socket (client connections are temporary during updates)
|
||||
socket.consume_sockets(1, "ota")(config)
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(ESPHomeOTAComponent),
|
||||
@@ -130,7 +139,8 @@ CONFIG_SCHEMA = (
|
||||
}
|
||||
)
|
||||
.extend(BASE_OTA_SCHEMA)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(cv.COMPONENT_SCHEMA),
|
||||
_consume_ota_sockets,
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = ota_esphome_final_validate
|
||||
|
||||
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 = fan_ns.class_("Fan", cg.EntityBase)
|
||||
FanState = fan_ns.class_("Fan", Fan, cg.Component)
|
||||
|
||||
FanDirection = fan_ns.enum("FanDirection", is_class=True)
|
||||
FAN_DIRECTION_ENUM = {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "fan_state.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "fan.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace fan {
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
#include "fan_state.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace fan {
|
||||
|
||||
static const char *const TAG = "fan";
|
||||
|
||||
void FanState::setup() {
|
||||
auto restore = this->restore_state_();
|
||||
if (restore)
|
||||
restore->to_call(*this).perform();
|
||||
}
|
||||
float FanState::get_setup_priority() const { return setup_priority::DATA - 1.0f; }
|
||||
|
||||
} // namespace fan
|
||||
} // namespace esphome
|
||||
@@ -1,34 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "fan.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace fan {
|
||||
|
||||
enum ESPDEPRECATED("LegacyFanDirection members are deprecated, use FanDirection instead.",
|
||||
"2022.2") LegacyFanDirection {
|
||||
FAN_DIRECTION_FORWARD = 0,
|
||||
FAN_DIRECTION_REVERSE = 1
|
||||
};
|
||||
|
||||
class ESPDEPRECATED("FanState is deprecated, use Fan instead.", "2022.2") FanState : public Fan, public Component {
|
||||
public:
|
||||
FanState() = default;
|
||||
|
||||
/// Get the traits of this fan.
|
||||
FanTraits get_traits() override { return this->traits_; }
|
||||
/// Set the traits of this fan (i.e. what features it supports).
|
||||
void set_traits(const FanTraits &traits) { this->traits_ = traits; }
|
||||
|
||||
void setup() override;
|
||||
float get_setup_priority() const override;
|
||||
|
||||
protected:
|
||||
void control(const FanCall &call) override { this->publish_state(); }
|
||||
|
||||
FanTraits traits_{};
|
||||
};
|
||||
|
||||
} // namespace fan
|
||||
} // namespace esphome
|
||||
@@ -65,7 +65,7 @@ HaierClimateBase::HaierClimateBase()
|
||||
{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH});
|
||||
this->traits_.set_supported_swing_modes({climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH,
|
||||
climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL});
|
||||
this->traits_.set_supports_current_temperature(true);
|
||||
this->traits_.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
|
||||
}
|
||||
|
||||
HaierClimateBase::~HaierClimateBase() {}
|
||||
|
||||
@@ -16,7 +16,8 @@ void HDC1080Component::setup() {
|
||||
|
||||
// if configuration fails - there is a problem
|
||||
if (this->write_register(HDC1080_CMD_CONFIGURATION, config, 2) != i2c::ERROR_OK) {
|
||||
this->mark_failed();
|
||||
ESP_LOGW(TAG, "Failed to configure HDC1080");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,31 +6,42 @@
|
||||
namespace esphome {
|
||||
namespace improv_base {
|
||||
|
||||
static constexpr const char DEVICE_NAME_PLACEHOLDER[] = "{{device_name}}";
|
||||
static constexpr size_t DEVICE_NAME_PLACEHOLDER_LEN = sizeof(DEVICE_NAME_PLACEHOLDER) - 1;
|
||||
static constexpr const char IP_ADDRESS_PLACEHOLDER[] = "{{ip_address}}";
|
||||
static constexpr size_t IP_ADDRESS_PLACEHOLDER_LEN = sizeof(IP_ADDRESS_PLACEHOLDER) - 1;
|
||||
|
||||
static void replace_all_in_place(std::string &str, const char *placeholder, size_t placeholder_len,
|
||||
const std::string &replacement) {
|
||||
size_t pos = 0;
|
||||
const size_t replacement_len = replacement.length();
|
||||
while ((pos = str.find(placeholder, pos)) != std::string::npos) {
|
||||
str.replace(pos, placeholder_len, replacement);
|
||||
pos += replacement_len;
|
||||
}
|
||||
}
|
||||
|
||||
std::string ImprovBase::get_formatted_next_url_() {
|
||||
if (this->next_url_.empty()) {
|
||||
return "";
|
||||
}
|
||||
std::string copy = this->next_url_;
|
||||
// Device name
|
||||
std::size_t pos = this->next_url_.find("{{device_name}}");
|
||||
if (pos != std::string::npos) {
|
||||
const std::string &device_name = App.get_name();
|
||||
copy.replace(pos, 15, device_name);
|
||||
}
|
||||
|
||||
// Ip address
|
||||
pos = this->next_url_.find("{{ip_address}}");
|
||||
if (pos != std::string::npos) {
|
||||
for (auto &ip : network::get_ip_addresses()) {
|
||||
if (ip.is_ip4()) {
|
||||
std::string ipa = ip.str();
|
||||
copy.replace(pos, 14, ipa);
|
||||
break;
|
||||
}
|
||||
std::string formatted_url = this->next_url_;
|
||||
|
||||
// Replace all occurrences of {{device_name}}
|
||||
replace_all_in_place(formatted_url, DEVICE_NAME_PLACEHOLDER, DEVICE_NAME_PLACEHOLDER_LEN, App.get_name());
|
||||
|
||||
// Replace all occurrences of {{ip_address}}
|
||||
for (auto &ip : network::get_ip_addresses()) {
|
||||
if (ip.is_ip4()) {
|
||||
replace_all_in_place(formatted_url, IP_ADDRESS_PLACEHOLDER, IP_ADDRESS_PLACEHOLDER_LEN, ip.str());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return copy;
|
||||
// Note: {{esphome_version}} is replaced at code generation time in Python
|
||||
|
||||
return formatted_url;
|
||||
}
|
||||
|
||||
} // namespace improv_base
|
||||
|
||||
@@ -14,7 +14,7 @@ void Kuntze::on_modbus_data(const std::vector<uint8_t> &data) {
|
||||
auto get_16bit = [&](int i) -> uint16_t { return (uint16_t(data[i * 2]) << 8) | uint16_t(data[i * 2 + 1]); };
|
||||
|
||||
this->waiting_ = false;
|
||||
ESP_LOGV(TAG, "Data: %s", hexencode(data).c_str());
|
||||
ESP_LOGV(TAG, "Data: %s", format_hex_pretty(data).c_str());
|
||||
|
||||
float value = (float) get_16bit(0);
|
||||
for (int i = 0; i < data[3]; i++)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/color.h"
|
||||
#include "esp_color_correction.h"
|
||||
#include "esp_color_view.h"
|
||||
#include "esp_range_view.h"
|
||||
#include "esphome/core/color.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "light_output.h"
|
||||
#include "light_state.h"
|
||||
#include "transformers.h"
|
||||
@@ -17,8 +17,6 @@
|
||||
namespace esphome {
|
||||
namespace light {
|
||||
|
||||
using ESPColor ESPDEPRECATED("esphome::light::ESPColor is deprecated, use esphome::Color instead.", "v1.21") = Color;
|
||||
|
||||
/// Convert the color information from a `LightColorValues` object to a `Color` object (does not apply brightness).
|
||||
Color color_from_light_color_values(LightColorValues val);
|
||||
|
||||
|
||||
@@ -104,5 +104,200 @@ constexpr ColorModeHelper operator|(ColorModeHelper lhs, ColorMode rhs) {
|
||||
return static_cast<ColorMode>(static_cast<uint8_t>(lhs) | static_cast<uint8_t>(rhs));
|
||||
}
|
||||
|
||||
// Type alias for raw color mode bitmask values
|
||||
using color_mode_bitmask_t = uint16_t;
|
||||
|
||||
// Constants for ColorMode count and bit range
|
||||
static constexpr int COLOR_MODE_COUNT = 10; // UNKNOWN through RGB_COLD_WARM_WHITE
|
||||
static constexpr int MAX_BIT_INDEX = sizeof(color_mode_bitmask_t) * 8; // Number of bits in bitmask type
|
||||
|
||||
// Compile-time array of all ColorMode values in declaration order
|
||||
// Bit positions (0-9) map directly to enum declaration order
|
||||
static constexpr ColorMode COLOR_MODES[COLOR_MODE_COUNT] = {
|
||||
ColorMode::UNKNOWN, // bit 0
|
||||
ColorMode::ON_OFF, // bit 1
|
||||
ColorMode::BRIGHTNESS, // bit 2
|
||||
ColorMode::WHITE, // bit 3
|
||||
ColorMode::COLOR_TEMPERATURE, // bit 4
|
||||
ColorMode::COLD_WARM_WHITE, // bit 5
|
||||
ColorMode::RGB, // bit 6
|
||||
ColorMode::RGB_WHITE, // bit 7
|
||||
ColorMode::RGB_COLOR_TEMPERATURE, // bit 8
|
||||
ColorMode::RGB_COLD_WARM_WHITE, // bit 9
|
||||
};
|
||||
|
||||
/// Map ColorMode enum values to bit positions (0-9)
|
||||
/// Bit positions follow the enum declaration order
|
||||
static constexpr int mode_to_bit(ColorMode mode) {
|
||||
// Linear search through COLOR_MODES array
|
||||
// Compiler optimizes this to efficient code since array is constexpr
|
||||
for (int i = 0; i < COLOR_MODE_COUNT; ++i) {
|
||||
if (COLOR_MODES[i] == mode)
|
||||
return i;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Map bit positions (0-9) to ColorMode enum values
|
||||
/// Bit positions follow the enum declaration order
|
||||
static constexpr ColorMode bit_to_mode(int bit) {
|
||||
// Direct lookup in COLOR_MODES array
|
||||
return (bit >= 0 && bit < COLOR_MODE_COUNT) ? COLOR_MODES[bit] : ColorMode::UNKNOWN;
|
||||
}
|
||||
|
||||
/// Helper to compute capability bitmask at compile time
|
||||
static constexpr color_mode_bitmask_t compute_capability_bitmask(ColorCapability capability) {
|
||||
color_mode_bitmask_t mask = 0;
|
||||
uint8_t cap_bit = static_cast<uint8_t>(capability);
|
||||
|
||||
// Check each ColorMode to see if it has this capability
|
||||
for (int bit = 0; bit < COLOR_MODE_COUNT; ++bit) {
|
||||
uint8_t mode_val = static_cast<uint8_t>(bit_to_mode(bit));
|
||||
if ((mode_val & cap_bit) != 0) {
|
||||
mask |= (1 << bit);
|
||||
}
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
|
||||
// Number of ColorCapability enum values
|
||||
static constexpr int COLOR_CAPABILITY_COUNT = 6;
|
||||
|
||||
/// Compile-time lookup table mapping ColorCapability to bitmask
|
||||
/// This array is computed at compile time using constexpr
|
||||
static constexpr color_mode_bitmask_t CAPABILITY_BITMASKS[] = {
|
||||
compute_capability_bitmask(ColorCapability::ON_OFF), // 1 << 0
|
||||
compute_capability_bitmask(ColorCapability::BRIGHTNESS), // 1 << 1
|
||||
compute_capability_bitmask(ColorCapability::WHITE), // 1 << 2
|
||||
compute_capability_bitmask(ColorCapability::COLOR_TEMPERATURE), // 1 << 3
|
||||
compute_capability_bitmask(ColorCapability::COLD_WARM_WHITE), // 1 << 4
|
||||
compute_capability_bitmask(ColorCapability::RGB), // 1 << 5
|
||||
};
|
||||
|
||||
/// Bitmask for storing a set of ColorMode values efficiently.
|
||||
/// Replaces std::set<ColorMode> to eliminate red-black tree overhead (~586 bytes).
|
||||
class ColorModeMask {
|
||||
public:
|
||||
constexpr ColorModeMask() = default;
|
||||
|
||||
/// Support initializer list syntax: {ColorMode::RGB, ColorMode::WHITE}
|
||||
constexpr ColorModeMask(std::initializer_list<ColorMode> modes) {
|
||||
for (auto mode : modes) {
|
||||
this->add(mode);
|
||||
}
|
||||
}
|
||||
|
||||
constexpr void add(ColorMode mode) { this->mask_ |= (1 << mode_to_bit(mode)); }
|
||||
|
||||
/// Add multiple modes at once using initializer list
|
||||
constexpr void add(std::initializer_list<ColorMode> modes) {
|
||||
for (auto mode : modes) {
|
||||
this->add(mode);
|
||||
}
|
||||
}
|
||||
|
||||
constexpr bool contains(ColorMode mode) const { return (this->mask_ & (1 << mode_to_bit(mode))) != 0; }
|
||||
|
||||
constexpr size_t size() const {
|
||||
// Count set bits using Brian Kernighan's algorithm
|
||||
// More efficient for sparse bitmasks (typical case: 2-4 modes out of 10)
|
||||
uint16_t n = this->mask_;
|
||||
size_t count = 0;
|
||||
while (n) {
|
||||
n &= n - 1; // Clear the least significant set bit
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
constexpr bool empty() const { return this->mask_ == 0; }
|
||||
|
||||
/// Iterator support for API encoding
|
||||
class Iterator {
|
||||
public:
|
||||
using iterator_category = std::forward_iterator_tag;
|
||||
using value_type = ColorMode;
|
||||
using difference_type = std::ptrdiff_t;
|
||||
using pointer = const ColorMode *;
|
||||
using reference = ColorMode;
|
||||
|
||||
constexpr Iterator(color_mode_bitmask_t mask, int bit) : mask_(mask), bit_(bit) { advance_to_next_set_bit_(); }
|
||||
|
||||
constexpr ColorMode operator*() const { return bit_to_mode(bit_); }
|
||||
|
||||
constexpr Iterator &operator++() {
|
||||
++bit_;
|
||||
advance_to_next_set_bit_();
|
||||
return *this;
|
||||
}
|
||||
|
||||
constexpr bool operator==(const Iterator &other) const { return bit_ == other.bit_; }
|
||||
|
||||
constexpr bool operator!=(const Iterator &other) const { return !(*this == other); }
|
||||
|
||||
private:
|
||||
constexpr void advance_to_next_set_bit_() { bit_ = ColorModeMask::find_next_set_bit(mask_, bit_); }
|
||||
|
||||
color_mode_bitmask_t mask_;
|
||||
int bit_;
|
||||
};
|
||||
|
||||
constexpr Iterator begin() const { return Iterator(mask_, 0); }
|
||||
constexpr Iterator end() const { return Iterator(mask_, MAX_BIT_INDEX); }
|
||||
|
||||
/// Get the raw bitmask value for API encoding
|
||||
constexpr color_mode_bitmask_t get_mask() const { return this->mask_; }
|
||||
|
||||
/// Find the next set bit in a bitmask starting from a given position
|
||||
/// Returns the bit position, or MAX_BIT_INDEX if no more bits are set
|
||||
static constexpr int find_next_set_bit(color_mode_bitmask_t mask, int start_bit) {
|
||||
int bit = start_bit;
|
||||
while (bit < MAX_BIT_INDEX && !(mask & (1 << bit))) {
|
||||
++bit;
|
||||
}
|
||||
return bit;
|
||||
}
|
||||
|
||||
/// Find the first set bit in a bitmask and return the corresponding ColorMode
|
||||
/// Used for optimizing compute_color_mode_() intersection logic
|
||||
static constexpr ColorMode first_mode_from_mask(color_mode_bitmask_t mask) {
|
||||
return bit_to_mode(find_next_set_bit(mask, 0));
|
||||
}
|
||||
|
||||
/// Check if a ColorMode is present in a raw bitmask value
|
||||
/// Useful for checking intersection results without creating a temporary ColorModeMask
|
||||
static constexpr bool mask_contains(color_mode_bitmask_t mask, ColorMode mode) {
|
||||
return (mask & (1 << mode_to_bit(mode))) != 0;
|
||||
}
|
||||
|
||||
/// Check if any mode in the bitmask has a specific capability
|
||||
/// Used for checking if a light supports a capability (e.g., BRIGHTNESS, RGB)
|
||||
bool has_capability(ColorCapability capability) const {
|
||||
// Lookup the pre-computed bitmask for this capability and check intersection with our mask
|
||||
// ColorCapability values: 1, 2, 4, 8, 16, 32 -> array indices: 0, 1, 2, 3, 4, 5
|
||||
// We need to convert the power-of-2 value to an index
|
||||
uint8_t cap_val = static_cast<uint8_t>(capability);
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
// Use compiler intrinsic for efficient bit position lookup (O(1) vs O(log n))
|
||||
int index = __builtin_ctz(cap_val);
|
||||
#else
|
||||
// Fallback for compilers without __builtin_ctz
|
||||
int index = 0;
|
||||
while (cap_val > 1) {
|
||||
cap_val >>= 1;
|
||||
++index;
|
||||
}
|
||||
#endif
|
||||
return (this->mask_ & CAPABILITY_BITMASKS[index]) != 0;
|
||||
}
|
||||
|
||||
private:
|
||||
// Using uint16_t instead of uint32_t for more efficient iteration (fewer bits to scan).
|
||||
// Currently only 10 ColorMode values exist, so 16 bits is sufficient.
|
||||
// Can be changed to uint32_t if more than 16 color modes are needed in the future.
|
||||
// Note: Due to struct padding, uint16_t and uint32_t result in same LightTraits size (12 bytes).
|
||||
color_mode_bitmask_t mask_{0};
|
||||
};
|
||||
|
||||
} // namespace light
|
||||
} // namespace esphome
|
||||
|
||||
@@ -406,7 +406,7 @@ void LightCall::transform_parameters_() {
|
||||
}
|
||||
}
|
||||
ColorMode LightCall::compute_color_mode_() {
|
||||
auto supported_modes = this->parent_->get_traits().get_supported_color_modes();
|
||||
const auto &supported_modes = this->parent_->get_traits().get_supported_color_modes();
|
||||
int supported_count = supported_modes.size();
|
||||
|
||||
// Some lights don't support any color modes (e.g. monochromatic light), leave it at unknown.
|
||||
@@ -425,20 +425,19 @@ ColorMode LightCall::compute_color_mode_() {
|
||||
// If no color mode is specified, we try to guess the color mode. This is needed for backward compatibility to
|
||||
// pre-colormode clients and automations, but also for the MQTT API, where HA doesn't let us know which color mode
|
||||
// was used for some reason.
|
||||
std::set<ColorMode> suitable_modes = this->get_suitable_color_modes_();
|
||||
// Compute intersection of suitable and supported modes using bitwise AND
|
||||
color_mode_bitmask_t intersection = this->get_suitable_color_modes_mask_() & supported_modes.get_mask();
|
||||
|
||||
// Don't change if the current mode is suitable.
|
||||
if (suitable_modes.count(current_mode) > 0) {
|
||||
// Don't change if the current mode is in the intersection (suitable AND supported)
|
||||
if (ColorModeMask::mask_contains(intersection, current_mode)) {
|
||||
ESP_LOGI(TAG, "'%s': color mode not specified; retaining %s", this->parent_->get_name().c_str(),
|
||||
LOG_STR_ARG(color_mode_to_human(current_mode)));
|
||||
return current_mode;
|
||||
}
|
||||
|
||||
// Use the preferred suitable mode.
|
||||
for (auto mode : suitable_modes) {
|
||||
if (supported_modes.count(mode) == 0)
|
||||
continue;
|
||||
|
||||
if (intersection != 0) {
|
||||
ColorMode mode = ColorModeMask::first_mode_from_mask(intersection);
|
||||
ESP_LOGI(TAG, "'%s': color mode not specified; using %s", this->parent_->get_name().c_str(),
|
||||
LOG_STR_ARG(color_mode_to_human(mode)));
|
||||
return mode;
|
||||
@@ -451,7 +450,7 @@ ColorMode LightCall::compute_color_mode_() {
|
||||
LOG_STR_ARG(color_mode_to_human(color_mode)));
|
||||
return color_mode;
|
||||
}
|
||||
std::set<ColorMode> LightCall::get_suitable_color_modes_() {
|
||||
color_mode_bitmask_t LightCall::get_suitable_color_modes_mask_() {
|
||||
bool has_white = this->has_white() && this->white_ > 0.0f;
|
||||
bool has_ct = this->has_color_temperature();
|
||||
bool has_cwww =
|
||||
@@ -459,36 +458,44 @@ std::set<ColorMode> LightCall::get_suitable_color_modes_() {
|
||||
bool has_rgb = (this->has_color_brightness() && this->color_brightness_ > 0.0f) ||
|
||||
(this->has_red() || this->has_green() || this->has_blue());
|
||||
|
||||
// Build key from flags: [rgb][cwww][ct][white]
|
||||
// Build key from flags: [rgb][cwww][ct][white]
|
||||
#define KEY(white, ct, cwww, rgb) ((white) << 0 | (ct) << 1 | (cwww) << 2 | (rgb) << 3)
|
||||
|
||||
uint8_t key = KEY(has_white, has_ct, has_cwww, has_rgb);
|
||||
|
||||
switch (key) {
|
||||
case KEY(true, false, false, false): // white only
|
||||
return {ColorMode::WHITE, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE,
|
||||
ColorMode::RGB_COLD_WARM_WHITE};
|
||||
return ColorModeMask({ColorMode::WHITE, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE,
|
||||
ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE})
|
||||
.get_mask();
|
||||
case KEY(false, true, false, false): // ct only
|
||||
return {ColorMode::COLOR_TEMPERATURE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE,
|
||||
ColorMode::RGB_COLD_WARM_WHITE};
|
||||
return ColorModeMask({ColorMode::COLOR_TEMPERATURE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE,
|
||||
ColorMode::RGB_COLD_WARM_WHITE})
|
||||
.get_mask();
|
||||
case KEY(true, true, false, false): // white + ct
|
||||
return {ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE};
|
||||
return ColorModeMask(
|
||||
{ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE})
|
||||
.get_mask();
|
||||
case KEY(false, false, true, false): // cwww only
|
||||
return {ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE};
|
||||
return ColorModeMask({ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE}).get_mask();
|
||||
case KEY(false, false, false, false): // none
|
||||
return {ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE, ColorMode::RGB,
|
||||
ColorMode::WHITE, ColorMode::COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE};
|
||||
return ColorModeMask({ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE,
|
||||
ColorMode::RGB, ColorMode::WHITE, ColorMode::COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE})
|
||||
.get_mask();
|
||||
case KEY(true, false, false, true): // rgb + white
|
||||
return {ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE};
|
||||
return ColorModeMask({ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE})
|
||||
.get_mask();
|
||||
case KEY(false, true, false, true): // rgb + ct
|
||||
case KEY(true, true, false, true): // rgb + white + ct
|
||||
return {ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE};
|
||||
return ColorModeMask({ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}).get_mask();
|
||||
case KEY(false, false, true, true): // rgb + cwww
|
||||
return {ColorMode::RGB_COLD_WARM_WHITE};
|
||||
return ColorModeMask({ColorMode::RGB_COLD_WARM_WHITE}).get_mask();
|
||||
case KEY(false, false, false, true): // rgb only
|
||||
return {ColorMode::RGB, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE};
|
||||
return ColorModeMask({ColorMode::RGB, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE,
|
||||
ColorMode::RGB_COLD_WARM_WHITE})
|
||||
.get_mask();
|
||||
default:
|
||||
return {}; // conflicting flags
|
||||
return 0; // conflicting flags
|
||||
}
|
||||
|
||||
#undef KEY
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "light_color_values.h"
|
||||
#include <set>
|
||||
|
||||
namespace esphome {
|
||||
|
||||
@@ -186,8 +185,8 @@ class LightCall {
|
||||
|
||||
//// Compute the color mode that should be used for this call.
|
||||
ColorMode compute_color_mode_();
|
||||
/// Get potential color modes for this light call.
|
||||
std::set<ColorMode> get_suitable_color_modes_();
|
||||
/// Get potential color modes bitmask for this light call.
|
||||
color_mode_bitmask_t get_suitable_color_modes_mask_();
|
||||
/// Some color modes also can be set using non-native parameters, transform those calls.
|
||||
void transform_parameters_();
|
||||
|
||||
|
||||
@@ -43,7 +43,6 @@ void LightJSONSchema::dump_json(LightState &state, JsonObject root) {
|
||||
}
|
||||
|
||||
auto values = state.remote_values;
|
||||
auto traits = state.get_output()->get_traits();
|
||||
|
||||
const auto color_mode = values.get_color_mode();
|
||||
const char *mode_str = get_color_mode_json_str(color_mode);
|
||||
|
||||
@@ -191,11 +191,9 @@ void LightState::current_values_as_brightness(float *brightness) {
|
||||
this->current_values.as_brightness(brightness, this->gamma_correct_);
|
||||
}
|
||||
void LightState::current_values_as_rgb(float *red, float *green, float *blue, bool color_interlock) {
|
||||
auto traits = this->get_traits();
|
||||
this->current_values.as_rgb(red, green, blue, this->gamma_correct_, false);
|
||||
}
|
||||
void LightState::current_values_as_rgbw(float *red, float *green, float *blue, float *white, bool color_interlock) {
|
||||
auto traits = this->get_traits();
|
||||
this->current_values.as_rgbw(red, green, blue, white, this->gamma_correct_, false);
|
||||
}
|
||||
void LightState::current_values_as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white,
|
||||
@@ -209,7 +207,6 @@ void LightState::current_values_as_rgbct(float *red, float *green, float *blue,
|
||||
white_brightness, this->gamma_correct_);
|
||||
}
|
||||
void LightState::current_values_as_cwww(float *cold_white, float *warm_white, bool constant_brightness) {
|
||||
auto traits = this->get_traits();
|
||||
this->current_values.as_cwww(cold_white, warm_white, this->gamma_correct_, constant_brightness);
|
||||
}
|
||||
void LightState::current_values_as_ct(float *color_temperature, float *white_brightness) {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "color_mode.h"
|
||||
#include <set>
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
|
||||
@@ -19,38 +18,17 @@ class LightTraits {
|
||||
public:
|
||||
LightTraits() = default;
|
||||
|
||||
const std::set<ColorMode> &get_supported_color_modes() const { return this->supported_color_modes_; }
|
||||
void set_supported_color_modes(std::set<ColorMode> supported_color_modes) {
|
||||
this->supported_color_modes_ = std::move(supported_color_modes);
|
||||
const ColorModeMask &get_supported_color_modes() const { return this->supported_color_modes_; }
|
||||
void set_supported_color_modes(ColorModeMask supported_color_modes) {
|
||||
this->supported_color_modes_ = supported_color_modes;
|
||||
}
|
||||
void set_supported_color_modes(std::initializer_list<ColorMode> modes) {
|
||||
this->supported_color_modes_ = ColorModeMask(modes);
|
||||
}
|
||||
|
||||
bool supports_color_mode(ColorMode color_mode) const { return this->supported_color_modes_.count(color_mode); }
|
||||
bool supports_color_mode(ColorMode color_mode) const { return this->supported_color_modes_.contains(color_mode); }
|
||||
bool supports_color_capability(ColorCapability color_capability) const {
|
||||
for (auto mode : this->supported_color_modes_) {
|
||||
if (mode & color_capability)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
ESPDEPRECATED("get_supports_brightness() is deprecated, use color modes instead.", "v1.21")
|
||||
bool get_supports_brightness() const { return this->supports_color_capability(ColorCapability::BRIGHTNESS); }
|
||||
ESPDEPRECATED("get_supports_rgb() is deprecated, use color modes instead.", "v1.21")
|
||||
bool get_supports_rgb() const { return this->supports_color_capability(ColorCapability::RGB); }
|
||||
ESPDEPRECATED("get_supports_rgb_white_value() is deprecated, use color modes instead.", "v1.21")
|
||||
bool get_supports_rgb_white_value() const {
|
||||
return this->supports_color_mode(ColorMode::RGB_WHITE) ||
|
||||
this->supports_color_mode(ColorMode::RGB_COLOR_TEMPERATURE);
|
||||
}
|
||||
ESPDEPRECATED("get_supports_color_temperature() is deprecated, use color modes instead.", "v1.21")
|
||||
bool get_supports_color_temperature() const {
|
||||
return this->supports_color_capability(ColorCapability::COLOR_TEMPERATURE);
|
||||
}
|
||||
ESPDEPRECATED("get_supports_color_interlock() is deprecated, use color modes instead.", "v1.21")
|
||||
bool get_supports_color_interlock() const {
|
||||
return this->supports_color_mode(ColorMode::RGB) &&
|
||||
(this->supports_color_mode(ColorMode::WHITE) || this->supports_color_mode(ColorMode::COLD_WARM_WHITE) ||
|
||||
this->supports_color_mode(ColorMode::COLOR_TEMPERATURE));
|
||||
return this->supported_color_modes_.has_capability(color_capability);
|
||||
}
|
||||
|
||||
float get_min_mireds() const { return this->min_mireds_; }
|
||||
@@ -59,19 +37,9 @@ class LightTraits {
|
||||
void set_max_mireds(float max_mireds) { this->max_mireds_ = max_mireds; }
|
||||
|
||||
protected:
|
||||
#ifdef USE_API
|
||||
// The API connection is a friend class to access internal methods
|
||||
friend class api::APIConnection;
|
||||
// This method returns a reference to the internal color modes set.
|
||||
// It is used by the API to avoid copying data when encoding messages.
|
||||
// Warning: Do not use this method outside of the API connection code.
|
||||
// It returns a reference to internal data that can be invalidated.
|
||||
const std::set<ColorMode> &get_supported_color_modes_for_api_() const { return this->supported_color_modes_; }
|
||||
#endif
|
||||
|
||||
std::set<ColorMode> supported_color_modes_{};
|
||||
float min_mireds_{0};
|
||||
float max_mireds_{0};
|
||||
ColorModeMask supported_color_modes_{};
|
||||
};
|
||||
|
||||
} // namespace light
|
||||
|
||||
@@ -68,6 +68,9 @@ static constexpr char LOG_LEVEL_LETTER_CHARS[] = {
|
||||
// Maximum header size: 35 bytes fixed + 32 bytes tag + 16 bytes thread name = 83 bytes (45 byte safety margin)
|
||||
static constexpr uint16_t MAX_HEADER_SIZE = 128;
|
||||
|
||||
// "0x" + 2 hex digits per byte + '\0'
|
||||
static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1;
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||
/** Enum for logging UART selection
|
||||
*
|
||||
@@ -177,8 +180,11 @@ class Logger : public Component {
|
||||
inline void HOT format_log_to_buffer_with_terminator_(uint8_t level, const char *tag, int line, const char *format,
|
||||
va_list args, char *buffer, uint16_t *buffer_at,
|
||||
uint16_t buffer_size) {
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
this->write_header_to_buffer_(level, tag, line, this->get_thread_name_(), buffer, buffer_at, buffer_size);
|
||||
#elif defined(USE_ZEPHYR)
|
||||
char buff[MAX_POINTER_REPRESENTATION];
|
||||
this->write_header_to_buffer_(level, tag, line, this->get_thread_name_(buff), buffer, buffer_at, buffer_size);
|
||||
#else
|
||||
this->write_header_to_buffer_(level, tag, line, nullptr, buffer, buffer_at, buffer_size);
|
||||
#endif
|
||||
@@ -277,7 +283,11 @@ class Logger : public Component {
|
||||
#endif
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||
const char *HOT get_thread_name_() {
|
||||
const char *HOT get_thread_name_(
|
||||
#ifdef USE_ZEPHYR
|
||||
char *buff
|
||||
#endif
|
||||
) {
|
||||
#ifdef USE_ZEPHYR
|
||||
k_tid_t current_task = k_current_get();
|
||||
#else
|
||||
@@ -291,7 +301,13 @@ class Logger : public Component {
|
||||
#elif defined(USE_LIBRETINY)
|
||||
return pcTaskGetTaskName(current_task);
|
||||
#elif defined(USE_ZEPHYR)
|
||||
return k_thread_name_get(current_task);
|
||||
const char *name = k_thread_name_get(current_task);
|
||||
if (name) {
|
||||
// zephyr print task names only if debug component is present
|
||||
return name;
|
||||
}
|
||||
std::snprintf(buff, MAX_POINTER_REPRESENTATION, "%p", current_task);
|
||||
return buff;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core import CORE, Lambda, coroutine_with_priority
|
||||
from esphome.coroutine import CoroPriority
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
DEPENDENCIES = ["network"]
|
||||
@@ -46,6 +47,19 @@ SERVICE_SCHEMA = cv.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _consume_mdns_sockets(config: ConfigType) -> ConfigType:
|
||||
"""Register socket needs for mDNS component."""
|
||||
if config.get(CONF_DISABLED):
|
||||
return config
|
||||
|
||||
from esphome.components import socket
|
||||
|
||||
# mDNS needs 2 sockets (IPv4 + IPv6 multicast)
|
||||
socket.consume_sockets(2, "mdns")(config)
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
@@ -55,6 +69,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
}
|
||||
),
|
||||
_remove_id_if_disabled,
|
||||
_consume_mdns_sockets,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -31,18 +31,17 @@ void MDNSComponent::setup() {
|
||||
mdns_instance_name_set(this->hostname_.c_str());
|
||||
|
||||
for (const auto &service : services) {
|
||||
std::vector<mdns_txt_item_t> txt_records;
|
||||
for (const auto &record : service.txt_records) {
|
||||
mdns_txt_item_t it{};
|
||||
auto txt_records = std::make_unique<mdns_txt_item_t[]>(service.txt_records.size());
|
||||
for (size_t i = 0; i < service.txt_records.size(); i++) {
|
||||
const auto &record = service.txt_records[i];
|
||||
// key and value are either compile-time string literals in flash or pointers to dynamic_txt_values_
|
||||
// Both remain valid for the lifetime of this function, and ESP-IDF makes internal copies
|
||||
it.key = MDNS_STR_ARG(record.key);
|
||||
it.value = MDNS_STR_ARG(record.value);
|
||||
txt_records.push_back(it);
|
||||
txt_records[i].key = MDNS_STR_ARG(record.key);
|
||||
txt_records[i].value = MDNS_STR_ARG(record.value);
|
||||
}
|
||||
uint16_t port = const_cast<TemplatableValue<uint16_t> &>(service.port).value();
|
||||
err = mdns_service_add(nullptr, MDNS_STR_ARG(service.service_type), MDNS_STR_ARG(service.proto), port,
|
||||
txt_records.data(), txt_records.size());
|
||||
txt_records.get(), service.txt_records.size());
|
||||
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Failed to register service %s: %s", MDNS_STR_ARG(service.service_type), esp_err_to_name(err));
|
||||
|
||||
@@ -77,7 +77,7 @@ void AirConditioner::control(const ClimateCall &call) {
|
||||
|
||||
ClimateTraits AirConditioner::traits() {
|
||||
auto traits = ClimateTraits();
|
||||
traits.set_supports_current_temperature(true);
|
||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
|
||||
traits.set_visual_min_temperature(17);
|
||||
traits.set_visual_max_temperature(30);
|
||||
traits.set_visual_temperature_step(0.5);
|
||||
|
||||
@@ -30,6 +30,19 @@ wave_4_3 = DriverChip(
|
||||
"blue": [14, 38, 18, 17, 10],
|
||||
},
|
||||
)
|
||||
|
||||
wave_4_3.extend(
|
||||
"WAVESHARE-5-1024X600",
|
||||
width=1024,
|
||||
height=600,
|
||||
hsync_back_porch=145,
|
||||
hsync_front_porch=170,
|
||||
hsync_pulse_width=30,
|
||||
vsync_back_porch=23,
|
||||
vsync_front_porch=12,
|
||||
vsync_pulse_width=2,
|
||||
)
|
||||
|
||||
wave_4_3.extend(
|
||||
"ESP32-S3-TOUCH-LCD-7-800X480",
|
||||
enable_pin=[{"ch422g": None, "number": 2}, {"ch422g": None, "number": 6}],
|
||||
|
||||
@@ -52,8 +52,9 @@ const uint8_t MITSUBISHI_BYTE16 = 0x00;
|
||||
|
||||
climate::ClimateTraits MitsubishiClimate::traits() {
|
||||
auto traits = climate::ClimateTraits();
|
||||
traits.set_supports_current_temperature(this->sensor_ != nullptr);
|
||||
traits.set_supports_action(false);
|
||||
if (this->sensor_ != nullptr) {
|
||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
|
||||
}
|
||||
traits.set_visual_min_temperature(MITSUBISHI_TEMP_MIN);
|
||||
traits.set_visual_max_temperature(MITSUBISHI_TEMP_MAX);
|
||||
traits.set_visual_temperature_step(1.0f);
|
||||
|
||||
@@ -58,6 +58,7 @@ from esphome.const import (
|
||||
PlatformFramework,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DEPENDENCIES = ["network"]
|
||||
|
||||
@@ -210,6 +211,15 @@ def validate_fingerprint(value):
|
||||
return value
|
||||
|
||||
|
||||
def _consume_mqtt_sockets(config: ConfigType) -> ConfigType:
|
||||
"""Register socket needs for MQTT component."""
|
||||
from esphome.components import socket
|
||||
|
||||
# MQTT needs 1 socket for the broker connection
|
||||
socket.consume_sockets(1, "mqtt")(config)
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
@@ -306,6 +316,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
),
|
||||
validate_config,
|
||||
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX]),
|
||||
_consume_mqtt_sockets,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -140,11 +140,8 @@ void MQTTClientComponent::send_device_info_() {
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
if (api::global_api_server->get_noise_ctx()->has_psk()) {
|
||||
root["api_encryption"] = "Noise_NNpsk0_25519_ChaChaPoly_SHA256";
|
||||
} else {
|
||||
root["api_encryption_supported"] = "Noise_NNpsk0_25519_ChaChaPoly_SHA256";
|
||||
}
|
||||
root[api::global_api_server->get_noise_ctx()->has_psk() ? "api_encryption" : "api_encryption_supported"] =
|
||||
"Noise_NNpsk0_25519_ChaChaPoly_SHA256";
|
||||
#endif
|
||||
},
|
||||
2, this->discovery_info_.retain);
|
||||
|
||||
@@ -17,11 +17,11 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
auto traits = this->device_->get_traits();
|
||||
// current_temperature_topic
|
||||
if (traits.get_supports_current_temperature()) {
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) {
|
||||
root[MQTT_CURRENT_TEMPERATURE_TOPIC] = this->get_current_temperature_state_topic();
|
||||
}
|
||||
// current_humidity_topic
|
||||
if (traits.get_supports_current_humidity()) {
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) {
|
||||
root[MQTT_CURRENT_HUMIDITY_TOPIC] = this->get_current_humidity_state_topic();
|
||||
}
|
||||
// mode_command_topic
|
||||
@@ -45,7 +45,8 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
|
||||
if (traits.supports_mode(CLIMATE_MODE_HEAT_COOL))
|
||||
modes.add("heat_cool");
|
||||
|
||||
if (traits.get_supports_two_point_target_temperature()) {
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
|
||||
climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
|
||||
// temperature_low_command_topic
|
||||
root[MQTT_TEMPERATURE_LOW_COMMAND_TOPIC] = this->get_target_temperature_low_command_topic();
|
||||
// temperature_low_state_topic
|
||||
@@ -61,7 +62,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
|
||||
root[MQTT_TEMPERATURE_STATE_TOPIC] = this->get_target_temperature_state_topic();
|
||||
}
|
||||
|
||||
if (traits.get_supports_target_humidity()) {
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
|
||||
// target_humidity_command_topic
|
||||
root[MQTT_TARGET_HUMIDITY_COMMAND_TOPIC] = this->get_target_humidity_command_topic();
|
||||
// target_humidity_state_topic
|
||||
@@ -109,7 +110,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
|
||||
presets.add(preset);
|
||||
}
|
||||
|
||||
if (traits.get_supports_action()) {
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) {
|
||||
// action_topic
|
||||
root[MQTT_ACTION_TOPIC] = this->get_action_state_topic();
|
||||
}
|
||||
@@ -174,7 +175,8 @@ void MQTTClimateComponent::setup() {
|
||||
call.perform();
|
||||
});
|
||||
|
||||
if (traits.get_supports_two_point_target_temperature()) {
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
|
||||
climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
|
||||
this->subscribe(this->get_target_temperature_low_command_topic(),
|
||||
[this](const std::string &topic, const std::string &payload) {
|
||||
auto val = parse_number<float>(payload);
|
||||
@@ -211,7 +213,7 @@ void MQTTClimateComponent::setup() {
|
||||
});
|
||||
}
|
||||
|
||||
if (traits.get_supports_target_humidity()) {
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
|
||||
this->subscribe(this->get_target_humidity_command_topic(),
|
||||
[this](const std::string &topic, const std::string &payload) {
|
||||
auto val = parse_number<float>(payload);
|
||||
@@ -290,12 +292,14 @@ bool MQTTClimateComponent::publish_state_() {
|
||||
success = false;
|
||||
int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals();
|
||||
int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals();
|
||||
if (traits.get_supports_current_temperature() && !std::isnan(this->device_->current_temperature)) {
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE) &&
|
||||
!std::isnan(this->device_->current_temperature)) {
|
||||
std::string payload = value_accuracy_to_string(this->device_->current_temperature, current_accuracy);
|
||||
if (!this->publish(this->get_current_temperature_state_topic(), payload))
|
||||
success = false;
|
||||
}
|
||||
if (traits.get_supports_two_point_target_temperature()) {
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
|
||||
climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
|
||||
std::string payload = value_accuracy_to_string(this->device_->target_temperature_low, target_accuracy);
|
||||
if (!this->publish(this->get_target_temperature_low_state_topic(), payload))
|
||||
success = false;
|
||||
@@ -308,12 +312,14 @@ bool MQTTClimateComponent::publish_state_() {
|
||||
success = false;
|
||||
}
|
||||
|
||||
if (traits.get_supports_current_humidity() && !std::isnan(this->device_->current_humidity)) {
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY) &&
|
||||
!std::isnan(this->device_->current_humidity)) {
|
||||
std::string payload = value_accuracy_to_string(this->device_->current_humidity, 0);
|
||||
if (!this->publish(this->get_current_humidity_state_topic(), payload))
|
||||
success = false;
|
||||
}
|
||||
if (traits.get_supports_target_humidity() && !std::isnan(this->device_->target_humidity)) {
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY) &&
|
||||
!std::isnan(this->device_->target_humidity)) {
|
||||
std::string payload = value_accuracy_to_string(this->device_->target_humidity, 0);
|
||||
if (!this->publish(this->get_target_humidity_state_topic(), payload))
|
||||
success = false;
|
||||
@@ -357,7 +363,7 @@ bool MQTTClimateComponent::publish_state_() {
|
||||
success = false;
|
||||
}
|
||||
|
||||
if (traits.get_supports_action()) {
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) {
|
||||
const char *payload;
|
||||
switch (this->device_->action) {
|
||||
case CLIMATE_ACTION_OFF:
|
||||
|
||||
@@ -85,24 +85,20 @@ bool MQTTComponent::send_discovery_() {
|
||||
}
|
||||
|
||||
// Fields from EntityBase
|
||||
if (this->get_entity()->has_own_name()) {
|
||||
root[MQTT_NAME] = this->friendly_name();
|
||||
} else {
|
||||
root[MQTT_NAME] = "";
|
||||
}
|
||||
root[MQTT_NAME] = this->get_entity()->has_own_name() ? this->friendly_name() : "";
|
||||
|
||||
if (this->is_disabled_by_default())
|
||||
root[MQTT_ENABLED_BY_DEFAULT] = false;
|
||||
if (!this->get_icon().empty())
|
||||
root[MQTT_ICON] = this->get_icon();
|
||||
|
||||
switch (this->get_entity()->get_entity_category()) {
|
||||
const auto entity_category = this->get_entity()->get_entity_category();
|
||||
switch (entity_category) {
|
||||
case ENTITY_CATEGORY_NONE:
|
||||
break;
|
||||
case ENTITY_CATEGORY_CONFIG:
|
||||
root[MQTT_ENTITY_CATEGORY] = "config";
|
||||
break;
|
||||
case ENTITY_CATEGORY_DIAGNOSTIC:
|
||||
root[MQTT_ENTITY_CATEGORY] = "diagnostic";
|
||||
root[MQTT_ENTITY_CATEGORY] = entity_category == ENTITY_CATEGORY_CONFIG ? "config" : "diagnostic";
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -113,20 +109,14 @@ bool MQTTComponent::send_discovery_() {
|
||||
if (this->command_retain_)
|
||||
root[MQTT_COMMAND_RETAIN] = true;
|
||||
|
||||
if (this->availability_ == nullptr) {
|
||||
if (!global_mqtt_client->get_availability().topic.empty()) {
|
||||
root[MQTT_AVAILABILITY_TOPIC] = global_mqtt_client->get_availability().topic;
|
||||
if (global_mqtt_client->get_availability().payload_available != "online")
|
||||
root[MQTT_PAYLOAD_AVAILABLE] = global_mqtt_client->get_availability().payload_available;
|
||||
if (global_mqtt_client->get_availability().payload_not_available != "offline")
|
||||
root[MQTT_PAYLOAD_NOT_AVAILABLE] = global_mqtt_client->get_availability().payload_not_available;
|
||||
}
|
||||
} else if (!this->availability_->topic.empty()) {
|
||||
root[MQTT_AVAILABILITY_TOPIC] = this->availability_->topic;
|
||||
if (this->availability_->payload_available != "online")
|
||||
root[MQTT_PAYLOAD_AVAILABLE] = this->availability_->payload_available;
|
||||
if (this->availability_->payload_not_available != "offline")
|
||||
root[MQTT_PAYLOAD_NOT_AVAILABLE] = this->availability_->payload_not_available;
|
||||
const Availability &avail =
|
||||
this->availability_ == nullptr ? global_mqtt_client->get_availability() : *this->availability_;
|
||||
if (!avail.topic.empty()) {
|
||||
root[MQTT_AVAILABILITY_TOPIC] = avail.topic;
|
||||
if (avail.payload_available != "online")
|
||||
root[MQTT_PAYLOAD_AVAILABLE] = avail.payload_available;
|
||||
if (avail.payload_not_available != "offline")
|
||||
root[MQTT_PAYLOAD_NOT_AVAILABLE] = avail.payload_not_available;
|
||||
}
|
||||
|
||||
const MQTTDiscoveryInfo &discovery_info = global_mqtt_client->get_discovery_info();
|
||||
@@ -145,10 +135,8 @@ bool MQTTComponent::send_discovery_() {
|
||||
if (discovery_info.object_id_generator == MQTT_DEVICE_NAME_OBJECT_ID_GENERATOR)
|
||||
root[MQTT_OBJECT_ID] = node_name + "_" + this->get_default_object_id_();
|
||||
|
||||
std::string node_friendly_name = App.get_friendly_name();
|
||||
if (node_friendly_name.empty()) {
|
||||
node_friendly_name = node_name;
|
||||
}
|
||||
const std::string &friendly_name_ref = App.get_friendly_name();
|
||||
const std::string &node_friendly_name = friendly_name_ref.empty() ? node_name : friendly_name_ref;
|
||||
std::string node_area = App.get_area();
|
||||
|
||||
JsonObject device_info = root[MQTT_DEVICE].to<JsonObject>();
|
||||
@@ -158,13 +146,9 @@ bool MQTTComponent::send_discovery_() {
|
||||
#ifdef ESPHOME_PROJECT_NAME
|
||||
device_info[MQTT_DEVICE_SW_VERSION] = ESPHOME_PROJECT_VERSION " (ESPHome " ESPHOME_VERSION ")";
|
||||
const char *model = std::strchr(ESPHOME_PROJECT_NAME, '.');
|
||||
if (model == nullptr) { // must never happen but check anyway
|
||||
device_info[MQTT_DEVICE_MODEL] = ESPHOME_BOARD;
|
||||
device_info[MQTT_DEVICE_MANUFACTURER] = ESPHOME_PROJECT_NAME;
|
||||
} else {
|
||||
device_info[MQTT_DEVICE_MODEL] = model + 1;
|
||||
device_info[MQTT_DEVICE_MANUFACTURER] = std::string(ESPHOME_PROJECT_NAME, model - ESPHOME_PROJECT_NAME);
|
||||
}
|
||||
device_info[MQTT_DEVICE_MODEL] = model == nullptr ? ESPHOME_BOARD : model + 1;
|
||||
device_info[MQTT_DEVICE_MANUFACTURER] =
|
||||
model == nullptr ? ESPHOME_PROJECT_NAME : std::string(ESPHOME_PROJECT_NAME, model - ESPHOME_PROJECT_NAME);
|
||||
#else
|
||||
device_info[MQTT_DEVICE_SW_VERSION] = ESPHOME_VERSION " (" + App.get_compilation_time() + ")";
|
||||
device_info[MQTT_DEVICE_MODEL] = ESPHOME_BOARD;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
#ifdef USE_MQTT
|
||||
#ifdef USE_FAN
|
||||
|
||||
#include "esphome/components/fan/fan_state.h"
|
||||
#include "esphome/components/fan/fan.h"
|
||||
#include "mqtt_component.h"
|
||||
|
||||
namespace esphome {
|
||||
|
||||
@@ -69,6 +69,12 @@ void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscovery
|
||||
if (traits.supports_color_capability(ColorCapability::BRIGHTNESS))
|
||||
root["brightness"] = true;
|
||||
|
||||
if (traits.supports_color_mode(ColorMode::COLOR_TEMPERATURE) ||
|
||||
traits.supports_color_mode(ColorMode::COLD_WARM_WHITE)) {
|
||||
root[MQTT_MIN_MIREDS] = traits.get_min_mireds();
|
||||
root[MQTT_MAX_MIREDS] = traits.get_max_mireds();
|
||||
}
|
||||
|
||||
if (this->state_->supports_effects()) {
|
||||
root["effect"] = true;
|
||||
JsonArray effect_list = root[MQTT_EFFECT_LIST].to<JsonArray>();
|
||||
|
||||
@@ -1291,9 +1291,6 @@ void Nextion::check_pending_waveform_() {
|
||||
|
||||
void Nextion::set_writer(const nextion_writer_t &writer) { this->writer_ = writer; }
|
||||
|
||||
ESPDEPRECATED("set_wait_for_ack(bool) deprecated, no effect", "v1.20")
|
||||
void Nextion::set_wait_for_ack(bool wait_for_ack) { ESP_LOGE(TAG, "Deprecated"); }
|
||||
|
||||
bool Nextion::is_updating() { return this->connection_state_.is_updating_; }
|
||||
|
||||
} // namespace nextion
|
||||
|
||||
@@ -54,11 +54,10 @@ void PIDClimate::control(const climate::ClimateCall &call) {
|
||||
}
|
||||
climate::ClimateTraits PIDClimate::traits() {
|
||||
auto traits = climate::ClimateTraits();
|
||||
traits.set_supports_current_temperature(true);
|
||||
traits.set_supports_two_point_target_temperature(false);
|
||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE | climate::CLIMATE_SUPPORTS_ACTION);
|
||||
|
||||
if (this->humidity_sensor_ != nullptr)
|
||||
traits.set_supports_current_humidity(true);
|
||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY);
|
||||
|
||||
traits.set_supported_modes({climate::CLIMATE_MODE_OFF});
|
||||
if (supports_cool_())
|
||||
@@ -68,7 +67,6 @@ climate::ClimateTraits PIDClimate::traits() {
|
||||
if (supports_heat_() && supports_cool_())
|
||||
traits.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL);
|
||||
|
||||
traits.set_supports_action(true);
|
||||
return traits;
|
||||
}
|
||||
void PIDClimate::dump_config() {
|
||||
|
||||
@@ -62,7 +62,7 @@ CONF_WARNING_MPPT_OVERLOAD = "warning_mppt_overload"
|
||||
CONF_WARNING_BATTERY_TOO_LOW_TO_CHARGE = "warning_battery_too_low_to_charge"
|
||||
CONF_FAULT_DC_DC_OVER_CURRENT = "fault_dc_dc_over_current"
|
||||
CONF_FAULT_CODE = "fault_code"
|
||||
CONF_WARNUNG_LOW_PV_ENERGY = "warnung_low_pv_energy"
|
||||
CONF_WARNING_LOW_PV_ENERGY = "warning_low_pv_energy"
|
||||
CONF_WARNING_HIGH_AC_INPUT_DURING_BUS_SOFT_START = (
|
||||
"warning_high_ac_input_during_bus_soft_start"
|
||||
)
|
||||
@@ -122,7 +122,7 @@ TYPES = [
|
||||
CONF_WARNING_BATTERY_TOO_LOW_TO_CHARGE,
|
||||
CONF_FAULT_DC_DC_OVER_CURRENT,
|
||||
CONF_FAULT_CODE,
|
||||
CONF_WARNUNG_LOW_PV_ENERGY,
|
||||
CONF_WARNING_LOW_PV_ENERGY,
|
||||
CONF_WARNING_HIGH_AC_INPUT_DURING_BUS_SOFT_START,
|
||||
CONF_WARNING_BATTERY_EQUALIZATION,
|
||||
]
|
||||
|
||||
@@ -13,7 +13,7 @@ void PipsolarOutput::write_state(float state) {
|
||||
|
||||
if (std::find(this->possible_values_.begin(), this->possible_values_.end(), state) != this->possible_values_.end()) {
|
||||
ESP_LOGD(TAG, "Will write: %s out of value %f / %02.0f", tmp, state, state);
|
||||
this->parent_->switch_command(std::string(tmp));
|
||||
this->parent_->queue_command(std::string(tmp));
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Will not write: %s as it is not in list of allowed values", tmp);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,7 @@
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace pipsolar {
|
||||
@@ -28,10 +29,17 @@ struct PollingCommand {
|
||||
bool needs_update;
|
||||
};
|
||||
|
||||
#define PIPSOLAR_VALUED_ENTITY_(type, name, polling_command, value_type) \
|
||||
protected: \
|
||||
value_type value_##name##_; \
|
||||
PIPSOLAR_ENTITY_(type, name, polling_command)
|
||||
struct QFLAGValues {
|
||||
esphome::optional<bool> silence_buzzer_open_buzzer;
|
||||
esphome::optional<bool> overload_bypass_function;
|
||||
esphome::optional<bool> lcd_escape_to_default;
|
||||
esphome::optional<bool> overload_restart_function;
|
||||
esphome::optional<bool> over_temperature_restart_function;
|
||||
esphome::optional<bool> backlight_on;
|
||||
esphome::optional<bool> alarm_on_when_primary_source_interrupt;
|
||||
esphome::optional<bool> fault_code_record;
|
||||
esphome::optional<bool> power_saving;
|
||||
};
|
||||
|
||||
#define PIPSOLAR_ENTITY_(type, name, polling_command) \
|
||||
protected: \
|
||||
@@ -43,126 +51,123 @@ struct PollingCommand {
|
||||
this->add_polling_command_(#polling_command, POLLING_##polling_command); \
|
||||
}
|
||||
|
||||
#define PIPSOLAR_SENSOR(name, polling_command, value_type) \
|
||||
PIPSOLAR_VALUED_ENTITY_(sensor::Sensor, name, polling_command, value_type)
|
||||
#define PIPSOLAR_SENSOR(name, polling_command) PIPSOLAR_ENTITY_(sensor::Sensor, name, polling_command)
|
||||
#define PIPSOLAR_SWITCH(name, polling_command) PIPSOLAR_ENTITY_(switch_::Switch, name, polling_command)
|
||||
#define PIPSOLAR_BINARY_SENSOR(name, polling_command, value_type) \
|
||||
PIPSOLAR_VALUED_ENTITY_(binary_sensor::BinarySensor, name, polling_command, value_type)
|
||||
#define PIPSOLAR_VALUED_TEXT_SENSOR(name, polling_command, value_type) \
|
||||
PIPSOLAR_VALUED_ENTITY_(text_sensor::TextSensor, name, polling_command, value_type)
|
||||
#define PIPSOLAR_BINARY_SENSOR(name, polling_command) \
|
||||
PIPSOLAR_ENTITY_(binary_sensor::BinarySensor, name, polling_command)
|
||||
#define PIPSOLAR_TEXT_SENSOR(name, polling_command) PIPSOLAR_ENTITY_(text_sensor::TextSensor, name, polling_command)
|
||||
|
||||
class Pipsolar : public uart::UARTDevice, public PollingComponent {
|
||||
// QPIGS values
|
||||
PIPSOLAR_SENSOR(grid_voltage, QPIGS, float)
|
||||
PIPSOLAR_SENSOR(grid_frequency, QPIGS, float)
|
||||
PIPSOLAR_SENSOR(ac_output_voltage, QPIGS, float)
|
||||
PIPSOLAR_SENSOR(ac_output_frequency, QPIGS, float)
|
||||
PIPSOLAR_SENSOR(ac_output_apparent_power, QPIGS, int)
|
||||
PIPSOLAR_SENSOR(ac_output_active_power, QPIGS, int)
|
||||
PIPSOLAR_SENSOR(output_load_percent, QPIGS, int)
|
||||
PIPSOLAR_SENSOR(bus_voltage, QPIGS, int)
|
||||
PIPSOLAR_SENSOR(battery_voltage, QPIGS, float)
|
||||
PIPSOLAR_SENSOR(battery_charging_current, QPIGS, int)
|
||||
PIPSOLAR_SENSOR(battery_capacity_percent, QPIGS, int)
|
||||
PIPSOLAR_SENSOR(inverter_heat_sink_temperature, QPIGS, int)
|
||||
PIPSOLAR_SENSOR(pv_input_current_for_battery, QPIGS, float)
|
||||
PIPSOLAR_SENSOR(pv_input_voltage, QPIGS, float)
|
||||
PIPSOLAR_SENSOR(battery_voltage_scc, QPIGS, float)
|
||||
PIPSOLAR_SENSOR(battery_discharge_current, QPIGS, int)
|
||||
PIPSOLAR_BINARY_SENSOR(add_sbu_priority_version, QPIGS, int)
|
||||
PIPSOLAR_BINARY_SENSOR(configuration_status, QPIGS, int)
|
||||
PIPSOLAR_BINARY_SENSOR(scc_firmware_version, QPIGS, int)
|
||||
PIPSOLAR_BINARY_SENSOR(load_status, QPIGS, int)
|
||||
PIPSOLAR_BINARY_SENSOR(battery_voltage_to_steady_while_charging, QPIGS, int)
|
||||
PIPSOLAR_BINARY_SENSOR(charging_status, QPIGS, int)
|
||||
PIPSOLAR_BINARY_SENSOR(scc_charging_status, QPIGS, int)
|
||||
PIPSOLAR_BINARY_SENSOR(ac_charging_status, QPIGS, int)
|
||||
PIPSOLAR_SENSOR(battery_voltage_offset_for_fans_on, QPIGS, int) //.1 scale
|
||||
PIPSOLAR_SENSOR(eeprom_version, QPIGS, int)
|
||||
PIPSOLAR_SENSOR(pv_charging_power, QPIGS, int)
|
||||
PIPSOLAR_BINARY_SENSOR(charging_to_floating_mode, QPIGS, int)
|
||||
PIPSOLAR_BINARY_SENSOR(switch_on, QPIGS, int)
|
||||
PIPSOLAR_BINARY_SENSOR(dustproof_installed, QPIGS, int)
|
||||
PIPSOLAR_SENSOR(grid_voltage, QPIGS)
|
||||
PIPSOLAR_SENSOR(grid_frequency, QPIGS)
|
||||
PIPSOLAR_SENSOR(ac_output_voltage, QPIGS)
|
||||
PIPSOLAR_SENSOR(ac_output_frequency, QPIGS)
|
||||
PIPSOLAR_SENSOR(ac_output_apparent_power, QPIGS)
|
||||
PIPSOLAR_SENSOR(ac_output_active_power, QPIGS)
|
||||
PIPSOLAR_SENSOR(output_load_percent, QPIGS)
|
||||
PIPSOLAR_SENSOR(bus_voltage, QPIGS)
|
||||
PIPSOLAR_SENSOR(battery_voltage, QPIGS)
|
||||
PIPSOLAR_SENSOR(battery_charging_current, QPIGS)
|
||||
PIPSOLAR_SENSOR(battery_capacity_percent, QPIGS)
|
||||
PIPSOLAR_SENSOR(inverter_heat_sink_temperature, QPIGS)
|
||||
PIPSOLAR_SENSOR(pv_input_current_for_battery, QPIGS)
|
||||
PIPSOLAR_SENSOR(pv_input_voltage, QPIGS)
|
||||
PIPSOLAR_SENSOR(battery_voltage_scc, QPIGS)
|
||||
PIPSOLAR_SENSOR(battery_discharge_current, QPIGS)
|
||||
PIPSOLAR_BINARY_SENSOR(add_sbu_priority_version, QPIGS)
|
||||
PIPSOLAR_BINARY_SENSOR(configuration_status, QPIGS)
|
||||
PIPSOLAR_BINARY_SENSOR(scc_firmware_version, QPIGS)
|
||||
PIPSOLAR_BINARY_SENSOR(load_status, QPIGS)
|
||||
PIPSOLAR_BINARY_SENSOR(battery_voltage_to_steady_while_charging, QPIGS)
|
||||
PIPSOLAR_BINARY_SENSOR(charging_status, QPIGS)
|
||||
PIPSOLAR_BINARY_SENSOR(scc_charging_status, QPIGS)
|
||||
PIPSOLAR_BINARY_SENSOR(ac_charging_status, QPIGS)
|
||||
PIPSOLAR_SENSOR(battery_voltage_offset_for_fans_on, QPIGS) //.1 scale
|
||||
PIPSOLAR_SENSOR(eeprom_version, QPIGS)
|
||||
PIPSOLAR_SENSOR(pv_charging_power, QPIGS)
|
||||
PIPSOLAR_BINARY_SENSOR(charging_to_floating_mode, QPIGS)
|
||||
PIPSOLAR_BINARY_SENSOR(switch_on, QPIGS)
|
||||
PIPSOLAR_BINARY_SENSOR(dustproof_installed, QPIGS)
|
||||
|
||||
// QPIRI values
|
||||
PIPSOLAR_SENSOR(grid_rating_voltage, QPIRI, float)
|
||||
PIPSOLAR_SENSOR(grid_rating_current, QPIRI, float)
|
||||
PIPSOLAR_SENSOR(ac_output_rating_voltage, QPIRI, float)
|
||||
PIPSOLAR_SENSOR(ac_output_rating_frequency, QPIRI, float)
|
||||
PIPSOLAR_SENSOR(ac_output_rating_current, QPIRI, float)
|
||||
PIPSOLAR_SENSOR(ac_output_rating_apparent_power, QPIRI, int)
|
||||
PIPSOLAR_SENSOR(ac_output_rating_active_power, QPIRI, int)
|
||||
PIPSOLAR_SENSOR(battery_rating_voltage, QPIRI, float)
|
||||
PIPSOLAR_SENSOR(battery_recharge_voltage, QPIRI, float)
|
||||
PIPSOLAR_SENSOR(battery_under_voltage, QPIRI, float)
|
||||
PIPSOLAR_SENSOR(battery_bulk_voltage, QPIRI, float)
|
||||
PIPSOLAR_SENSOR(battery_float_voltage, QPIRI, float)
|
||||
PIPSOLAR_SENSOR(battery_type, QPIRI, int)
|
||||
PIPSOLAR_SENSOR(current_max_ac_charging_current, QPIRI, int)
|
||||
PIPSOLAR_SENSOR(current_max_charging_current, QPIRI, int)
|
||||
PIPSOLAR_SENSOR(input_voltage_range, QPIRI, int)
|
||||
PIPSOLAR_SENSOR(output_source_priority, QPIRI, int)
|
||||
PIPSOLAR_SENSOR(charger_source_priority, QPIRI, int)
|
||||
PIPSOLAR_SENSOR(parallel_max_num, QPIRI, int)
|
||||
PIPSOLAR_SENSOR(machine_type, QPIRI, int)
|
||||
PIPSOLAR_SENSOR(topology, QPIRI, int)
|
||||
PIPSOLAR_SENSOR(output_mode, QPIRI, int)
|
||||
PIPSOLAR_SENSOR(battery_redischarge_voltage, QPIRI, float)
|
||||
PIPSOLAR_SENSOR(pv_ok_condition_for_parallel, QPIRI, int)
|
||||
PIPSOLAR_SENSOR(pv_power_balance, QPIRI, int)
|
||||
PIPSOLAR_SENSOR(grid_rating_voltage, QPIRI)
|
||||
PIPSOLAR_SENSOR(grid_rating_current, QPIRI)
|
||||
PIPSOLAR_SENSOR(ac_output_rating_voltage, QPIRI)
|
||||
PIPSOLAR_SENSOR(ac_output_rating_frequency, QPIRI)
|
||||
PIPSOLAR_SENSOR(ac_output_rating_current, QPIRI)
|
||||
PIPSOLAR_SENSOR(ac_output_rating_apparent_power, QPIRI)
|
||||
PIPSOLAR_SENSOR(ac_output_rating_active_power, QPIRI)
|
||||
PIPSOLAR_SENSOR(battery_rating_voltage, QPIRI)
|
||||
PIPSOLAR_SENSOR(battery_recharge_voltage, QPIRI)
|
||||
PIPSOLAR_SENSOR(battery_under_voltage, QPIRI)
|
||||
PIPSOLAR_SENSOR(battery_bulk_voltage, QPIRI)
|
||||
PIPSOLAR_SENSOR(battery_float_voltage, QPIRI)
|
||||
PIPSOLAR_SENSOR(battery_type, QPIRI)
|
||||
PIPSOLAR_SENSOR(current_max_ac_charging_current, QPIRI)
|
||||
PIPSOLAR_SENSOR(current_max_charging_current, QPIRI)
|
||||
PIPSOLAR_SENSOR(input_voltage_range, QPIRI)
|
||||
PIPSOLAR_SENSOR(output_source_priority, QPIRI)
|
||||
PIPSOLAR_SENSOR(charger_source_priority, QPIRI)
|
||||
PIPSOLAR_SENSOR(parallel_max_num, QPIRI)
|
||||
PIPSOLAR_SENSOR(machine_type, QPIRI)
|
||||
PIPSOLAR_SENSOR(topology, QPIRI)
|
||||
PIPSOLAR_SENSOR(output_mode, QPIRI)
|
||||
PIPSOLAR_SENSOR(battery_redischarge_voltage, QPIRI)
|
||||
PIPSOLAR_SENSOR(pv_ok_condition_for_parallel, QPIRI)
|
||||
PIPSOLAR_SENSOR(pv_power_balance, QPIRI)
|
||||
|
||||
// QMOD values
|
||||
PIPSOLAR_VALUED_TEXT_SENSOR(device_mode, QMOD, char)
|
||||
PIPSOLAR_TEXT_SENSOR(device_mode, QMOD)
|
||||
|
||||
// QFLAG values
|
||||
PIPSOLAR_BINARY_SENSOR(silence_buzzer_open_buzzer, QFLAG, int)
|
||||
PIPSOLAR_BINARY_SENSOR(overload_bypass_function, QFLAG, int)
|
||||
PIPSOLAR_BINARY_SENSOR(lcd_escape_to_default, QFLAG, int)
|
||||
PIPSOLAR_BINARY_SENSOR(overload_restart_function, QFLAG, int)
|
||||
PIPSOLAR_BINARY_SENSOR(over_temperature_restart_function, QFLAG, int)
|
||||
PIPSOLAR_BINARY_SENSOR(backlight_on, QFLAG, int)
|
||||
PIPSOLAR_BINARY_SENSOR(alarm_on_when_primary_source_interrupt, QFLAG, int)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_code_record, QFLAG, int)
|
||||
PIPSOLAR_BINARY_SENSOR(power_saving, QFLAG, int)
|
||||
PIPSOLAR_BINARY_SENSOR(silence_buzzer_open_buzzer, QFLAG)
|
||||
PIPSOLAR_BINARY_SENSOR(overload_bypass_function, QFLAG)
|
||||
PIPSOLAR_BINARY_SENSOR(lcd_escape_to_default, QFLAG)
|
||||
PIPSOLAR_BINARY_SENSOR(overload_restart_function, QFLAG)
|
||||
PIPSOLAR_BINARY_SENSOR(over_temperature_restart_function, QFLAG)
|
||||
PIPSOLAR_BINARY_SENSOR(backlight_on, QFLAG)
|
||||
PIPSOLAR_BINARY_SENSOR(alarm_on_when_primary_source_interrupt, QFLAG)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_code_record, QFLAG)
|
||||
PIPSOLAR_BINARY_SENSOR(power_saving, QFLAG)
|
||||
|
||||
// QPIWS values
|
||||
PIPSOLAR_BINARY_SENSOR(warnings_present, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(faults_present, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_power_loss, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_inverter_fault, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_bus_over, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_bus_under, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_bus_soft_fail, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_line_fail, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_opvshort, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_inverter_voltage_too_low, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_inverter_voltage_too_high, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_over_temperature, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_fan_lock, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_battery_voltage_high, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_battery_low_alarm, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_battery_under_shutdown, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_battery_derating, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_over_load, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_eeprom_failed, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_inverter_over_current, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_inverter_soft_failed, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_self_test_failed, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_op_dc_voltage_over, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_battery_open, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_current_sensor_failed, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_battery_short, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_power_limit, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_pv_voltage_high, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_mppt_overload, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_mppt_overload, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_battery_too_low_to_charge, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_dc_dc_over_current, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_code, QPIWS, int)
|
||||
PIPSOLAR_BINARY_SENSOR(warnung_low_pv_energy, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_high_ac_input_during_bus_soft_start, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_battery_equalization, QPIWS, bool)
|
||||
PIPSOLAR_BINARY_SENSOR(warnings_present, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(faults_present, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_power_loss, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_inverter_fault, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_bus_over, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_bus_under, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_bus_soft_fail, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_line_fail, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_opvshort, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_inverter_voltage_too_low, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_inverter_voltage_too_high, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_over_temperature, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_fan_lock, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_battery_voltage_high, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_battery_low_alarm, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_battery_under_shutdown, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_battery_derating, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_over_load, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_eeprom_failed, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_inverter_over_current, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_inverter_soft_failed, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_self_test_failed, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_op_dc_voltage_over, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_battery_open, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_current_sensor_failed, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_battery_short, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_power_limit, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_pv_voltage_high, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_mppt_overload, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_mppt_overload, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_battery_too_low_to_charge, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_dc_dc_over_current, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(fault_code, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_low_pv_energy, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_high_ac_input_during_bus_soft_start, QPIWS)
|
||||
PIPSOLAR_BINARY_SENSOR(warning_battery_equalization, QPIWS)
|
||||
|
||||
PIPSOLAR_TEXT_SENSOR(last_qpigs, QPIGS)
|
||||
PIPSOLAR_TEXT_SENSOR(last_qpiri, QPIRI)
|
||||
@@ -180,14 +185,14 @@ class Pipsolar : public uart::UARTDevice, public PollingComponent {
|
||||
PIPSOLAR_SWITCH(pv_ok_condition_for_parallel_switch, QPIRI)
|
||||
PIPSOLAR_SWITCH(pv_power_balance_switch, QPIRI)
|
||||
|
||||
void switch_command(const std::string &command);
|
||||
void queue_command(const std::string &command);
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
void update() override;
|
||||
|
||||
protected:
|
||||
static const size_t PIPSOLAR_READ_BUFFER_LENGTH = 110; // maximum supported answer length
|
||||
static const size_t PIPSOLAR_READ_BUFFER_LENGTH = 128; // maximum supported answer length
|
||||
static const size_t COMMAND_QUEUE_LENGTH = 10;
|
||||
static const size_t COMMAND_TIMEOUT = 5000;
|
||||
static const size_t POLLING_COMMANDS_MAX = 15;
|
||||
@@ -198,7 +203,26 @@ class Pipsolar : public uart::UARTDevice, public PollingComponent {
|
||||
uint16_t pipsolar_crc_(uint8_t *msg, uint8_t len);
|
||||
bool send_next_command_();
|
||||
bool send_next_poll_();
|
||||
void queue_command_(const char *command, uint8_t length);
|
||||
|
||||
void handle_qpiri_(const char *message);
|
||||
void handle_qpigs_(const char *message);
|
||||
void handle_qmod_(const char *message);
|
||||
void handle_qflag_(const char *message);
|
||||
void handle_qpiws_(const char *message);
|
||||
void handle_qt_(const char *message);
|
||||
void handle_qmn_(const char *message);
|
||||
|
||||
void skip_start_(const char *message, size_t *pos);
|
||||
void skip_field_(const char *message, size_t *pos);
|
||||
std::string read_field_(const char *message, size_t *pos);
|
||||
|
||||
void read_float_sensor_(const char *message, size_t *pos, sensor::Sensor *sensor);
|
||||
void read_int_sensor_(const char *message, size_t *pos, sensor::Sensor *sensor);
|
||||
|
||||
void publish_binary_sensor_(esphome::optional<bool> b, binary_sensor::BinarySensor *sensor);
|
||||
|
||||
esphome::optional<bool> get_bit_(std::string bits, uint8_t bit_pos);
|
||||
|
||||
std::string command_queue_[COMMAND_QUEUE_LENGTH];
|
||||
uint8_t command_queue_position_ = 0;
|
||||
uint8_t read_buffer_[PIPSOLAR_READ_BUFFER_LENGTH];
|
||||
@@ -213,11 +237,10 @@ class Pipsolar : public uart::UARTDevice, public PollingComponent {
|
||||
STATE_POLL_COMPLETE = 3,
|
||||
STATE_COMMAND_COMPLETE = 4,
|
||||
STATE_POLL_CHECKED = 5,
|
||||
STATE_POLL_DECODED = 6,
|
||||
};
|
||||
|
||||
uint8_t last_polling_command_ = 0;
|
||||
PollingCommand used_polling_commands_[POLLING_COMMANDS_MAX];
|
||||
PollingCommand enabled_polling_commands_[POLLING_COMMANDS_MAX];
|
||||
};
|
||||
|
||||
} // namespace pipsolar
|
||||
|
||||
@@ -11,11 +11,11 @@ void PipsolarSwitch::dump_config() { LOG_SWITCH("", "Pipsolar Switch", this); }
|
||||
void PipsolarSwitch::write_state(bool state) {
|
||||
if (state) {
|
||||
if (!this->on_command_.empty()) {
|
||||
this->parent_->switch_command(this->on_command_);
|
||||
this->parent_->queue_command(this->on_command_);
|
||||
}
|
||||
} else {
|
||||
if (!this->off_command_.empty()) {
|
||||
this->parent_->switch_command(this->off_command_);
|
||||
this->parent_->queue_command(this->off_command_);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -916,7 +916,7 @@ void PrometheusHandler::climate_row_(AsyncResponseStream *stream, climate::Clima
|
||||
auto min_temp_value = value_accuracy_to_string(traits.get_visual_min_temperature(), target_accuracy);
|
||||
climate_value_row_(stream, obj, area, node, friendly_name, min_temp, min_temp_value);
|
||||
// now check optional traits
|
||||
if (traits.get_supports_current_temperature()) {
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) {
|
||||
std::string current_temp = "current_temperature";
|
||||
if (std::isnan(obj->current_temperature)) {
|
||||
climate_failed_row_(stream, obj, area, node, friendly_name, current_temp, true);
|
||||
@@ -927,7 +927,7 @@ void PrometheusHandler::climate_row_(AsyncResponseStream *stream, climate::Clima
|
||||
climate_failed_row_(stream, obj, area, node, friendly_name, current_temp, false);
|
||||
}
|
||||
}
|
||||
if (traits.get_supports_current_humidity()) {
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) {
|
||||
std::string current_humidity = "current_humidity";
|
||||
if (std::isnan(obj->current_humidity)) {
|
||||
climate_failed_row_(stream, obj, area, node, friendly_name, current_humidity, true);
|
||||
@@ -938,7 +938,7 @@ void PrometheusHandler::climate_row_(AsyncResponseStream *stream, climate::Clima
|
||||
climate_failed_row_(stream, obj, area, node, friendly_name, current_humidity, false);
|
||||
}
|
||||
}
|
||||
if (traits.get_supports_target_humidity()) {
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
|
||||
std::string target_humidity = "target_humidity";
|
||||
if (std::isnan(obj->target_humidity)) {
|
||||
climate_failed_row_(stream, obj, area, node, friendly_name, target_humidity, true);
|
||||
@@ -949,7 +949,8 @@ void PrometheusHandler::climate_row_(AsyncResponseStream *stream, climate::Clima
|
||||
climate_failed_row_(stream, obj, area, node, friendly_name, target_humidity, false);
|
||||
}
|
||||
}
|
||||
if (traits.get_supports_two_point_target_temperature()) {
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
|
||||
climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
|
||||
std::string target_temp_low = "target_temperature_low";
|
||||
auto target_temp_low_value = value_accuracy_to_string(obj->target_temperature_low, target_accuracy);
|
||||
climate_value_row_(stream, obj, area, node, friendly_name, target_temp_low, target_temp_low_value);
|
||||
@@ -961,7 +962,7 @@ void PrometheusHandler::climate_row_(AsyncResponseStream *stream, climate::Clima
|
||||
auto target_temp_value = value_accuracy_to_string(obj->target_temperature, target_accuracy);
|
||||
climate_value_row_(stream, obj, area, node, friendly_name, target_temp, target_temp_value);
|
||||
}
|
||||
if (traits.get_supports_action()) {
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) {
|
||||
std::string climate_trait_category = "action";
|
||||
const auto *climate_trait_value = climate::climate_action_to_string(obj->action);
|
||||
climate_setting_row_(stream, obj, area, node, friendly_name, climate_trait_category, climate_trait_value);
|
||||
|
||||
@@ -81,7 +81,7 @@ CONFIG_SCHEMA = (
|
||||
cv.int_range(min=0, max=0xFFFF, max_included=False),
|
||||
),
|
||||
cv.Optional(CONF_AMBIENT_PRESSURE_COMPENSATION): cv.pressure,
|
||||
cv.Optional(CONF_TEMPERATURE_OFFSET, default="4°C"): cv.temperature,
|
||||
cv.Optional(CONF_TEMPERATURE_OFFSET, default="4°C"): cv.temperature_delta,
|
||||
cv.Optional(CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE): cv.use_id(
|
||||
sensor.Sensor
|
||||
),
|
||||
|
||||
@@ -111,24 +111,26 @@ template<typename... Ts> class RestartScript : public Script<Ts...> {
|
||||
template<typename... Ts> class QueueingScript : public Script<Ts...>, public Component {
|
||||
public:
|
||||
void execute(Ts... x) override {
|
||||
this->lazy_init_queue_();
|
||||
|
||||
if (this->is_action_running() || this->num_queued_ > 0) {
|
||||
// num_queued_ is the number of *queued* instances (waiting, not including currently running)
|
||||
// max_runs_ is the maximum *total* instances (running + queued)
|
||||
// So we reject when num_queued_ + 1 >= max_runs_ (queued + running >= max)
|
||||
if (this->num_queued_ + 1 >= this->max_runs_) {
|
||||
this->esp_logw_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' maximum number of queued runs exceeded!"),
|
||||
this->esp_logw_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' max instances (running + queued) reached!"),
|
||||
LOG_STR_ARG(this->name_));
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize queue on first queued item (after capacity check)
|
||||
this->lazy_init_queue_();
|
||||
|
||||
this->esp_logd_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' queueing new instance (mode: queued)"),
|
||||
LOG_STR_ARG(this->name_));
|
||||
// Ring buffer: write to (queue_front_ + num_queued_) % (max_runs_ - 1)
|
||||
size_t write_pos = (this->queue_front_ + this->num_queued_) % (this->max_runs_ - 1);
|
||||
// Use reset() to replace the unique_ptr
|
||||
this->var_queue_[write_pos].reset(new std::tuple<Ts...>(std::make_tuple(x...)));
|
||||
// Ring buffer: write to (queue_front_ + num_queued_) % queue_capacity
|
||||
const size_t queue_capacity = static_cast<size_t>(this->max_runs_ - 1);
|
||||
size_t write_pos = (this->queue_front_ + this->num_queued_) % queue_capacity;
|
||||
// Use std::make_unique to replace the unique_ptr
|
||||
this->var_queue_[write_pos] = std::make_unique<std::tuple<Ts...>>(x...);
|
||||
this->num_queued_++;
|
||||
return;
|
||||
}
|
||||
@@ -140,9 +142,8 @@ template<typename... Ts> class QueueingScript : public Script<Ts...>, public Com
|
||||
|
||||
void stop() override {
|
||||
// Clear all queued items to free memory immediately
|
||||
for (int i = 0; i < this->max_runs_ - 1; i++) {
|
||||
this->var_queue_[i].reset();
|
||||
}
|
||||
// Resetting the array automatically destroys all unique_ptrs and their contents
|
||||
this->var_queue_.reset();
|
||||
this->num_queued_ = 0;
|
||||
this->queue_front_ = 0;
|
||||
Script<Ts...>::stop();
|
||||
@@ -152,8 +153,9 @@ template<typename... Ts> class QueueingScript : public Script<Ts...>, public Com
|
||||
if (this->num_queued_ != 0 && !this->is_action_running()) {
|
||||
// Dequeue: decrement count, move tuple out (frees slot), advance read position
|
||||
this->num_queued_--;
|
||||
const size_t queue_capacity = static_cast<size_t>(this->max_runs_ - 1);
|
||||
auto tuple_ptr = std::move(this->var_queue_[this->queue_front_]);
|
||||
this->queue_front_ = (this->queue_front_ + 1) % (this->max_runs_ - 1);
|
||||
this->queue_front_ = (this->queue_front_ + 1) % queue_capacity;
|
||||
this->trigger_tuple_(*tuple_ptr, typename gens<sizeof...(Ts)>::type());
|
||||
}
|
||||
}
|
||||
@@ -164,13 +166,10 @@ template<typename... Ts> class QueueingScript : public Script<Ts...>, public Com
|
||||
// Lazy init queue on first use - avoids setup() ordering issues and saves memory
|
||||
// if script is never executed during this boot cycle
|
||||
inline void lazy_init_queue_() {
|
||||
if (this->var_queue_.capacity() == 0) {
|
||||
// Allocate max_runs_ - 1 slots for queued items (running item is separate)
|
||||
this->var_queue_.init(this->max_runs_ - 1);
|
||||
// Initialize all unique_ptr slots to nullptr
|
||||
for (int i = 0; i < this->max_runs_ - 1; i++) {
|
||||
this->var_queue_.push_back(nullptr);
|
||||
}
|
||||
if (!this->var_queue_) {
|
||||
// Allocate array of max_runs_ - 1 slots for queued items (running item is separate)
|
||||
// unique_ptr array is zero-initialized, so all slots start as nullptr
|
||||
this->var_queue_ = std::make_unique<std::unique_ptr<std::tuple<Ts...>>[]>(this->max_runs_ - 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,7 +180,7 @@ template<typename... Ts> class QueueingScript : public Script<Ts...>, public Com
|
||||
int num_queued_ = 0; // Number of queued instances (not including currently running)
|
||||
int max_runs_ = 0; // Maximum total instances (running + queued)
|
||||
size_t queue_front_ = 0; // Ring buffer read position (next item to execute)
|
||||
FixedVector<std::unique_ptr<std::tuple<Ts...>>> var_queue_; // Ring buffer of queued parameters
|
||||
std::unique_ptr<std::unique_ptr<std::tuple<Ts...>>[]> var_queue_; // Ring buffer of queued parameters
|
||||
};
|
||||
|
||||
/** A script type that executes new instances in parallel.
|
||||
|
||||
@@ -251,6 +251,9 @@ MaxFilter = sensor_ns.class_("MaxFilter", Filter)
|
||||
SlidingWindowMovingAverageFilter = sensor_ns.class_(
|
||||
"SlidingWindowMovingAverageFilter", Filter
|
||||
)
|
||||
StreamingMinFilter = sensor_ns.class_("StreamingMinFilter", Filter)
|
||||
StreamingMaxFilter = sensor_ns.class_("StreamingMaxFilter", Filter)
|
||||
StreamingMovingAverageFilter = sensor_ns.class_("StreamingMovingAverageFilter", Filter)
|
||||
ExponentialMovingAverageFilter = sensor_ns.class_(
|
||||
"ExponentialMovingAverageFilter", Filter
|
||||
)
|
||||
@@ -258,9 +261,12 @@ ThrottleAverageFilter = sensor_ns.class_("ThrottleAverageFilter", Filter, cg.Com
|
||||
LambdaFilter = sensor_ns.class_("LambdaFilter", Filter)
|
||||
OffsetFilter = sensor_ns.class_("OffsetFilter", Filter)
|
||||
MultiplyFilter = sensor_ns.class_("MultiplyFilter", Filter)
|
||||
FilterOutValueFilter = sensor_ns.class_("FilterOutValueFilter", Filter)
|
||||
ValueListFilter = sensor_ns.class_("ValueListFilter", Filter)
|
||||
FilterOutValueFilter = sensor_ns.class_("FilterOutValueFilter", ValueListFilter)
|
||||
ThrottleFilter = sensor_ns.class_("ThrottleFilter", Filter)
|
||||
ThrottleWithPriorityFilter = sensor_ns.class_("ThrottleWithPriorityFilter", Filter)
|
||||
ThrottleWithPriorityFilter = sensor_ns.class_(
|
||||
"ThrottleWithPriorityFilter", ValueListFilter
|
||||
)
|
||||
TimeoutFilter = sensor_ns.class_("TimeoutFilter", Filter, cg.Component)
|
||||
DebounceFilter = sensor_ns.class_("DebounceFilter", Filter, cg.Component)
|
||||
HeartbeatFilter = sensor_ns.class_("HeartbeatFilter", Filter, cg.Component)
|
||||
@@ -452,14 +458,21 @@ async def skip_initial_filter_to_code(config, filter_id):
|
||||
return cg.new_Pvariable(filter_id, config)
|
||||
|
||||
|
||||
@FILTER_REGISTRY.register("min", MinFilter, MIN_SCHEMA)
|
||||
@FILTER_REGISTRY.register("min", Filter, MIN_SCHEMA)
|
||||
async def min_filter_to_code(config, filter_id):
|
||||
return cg.new_Pvariable(
|
||||
filter_id,
|
||||
config[CONF_WINDOW_SIZE],
|
||||
config[CONF_SEND_EVERY],
|
||||
config[CONF_SEND_FIRST_AT],
|
||||
)
|
||||
window_size: int = config[CONF_WINDOW_SIZE]
|
||||
send_every: int = config[CONF_SEND_EVERY]
|
||||
send_first_at: int = config[CONF_SEND_FIRST_AT]
|
||||
|
||||
# Optimization: Use streaming filter for batch windows (window_size == send_every)
|
||||
# Saves 99.98% memory for large windows (e.g., 20KB → 4 bytes for window_size=5000)
|
||||
if window_size == send_every:
|
||||
# Use streaming filter - O(1) memory instead of O(n)
|
||||
rhs = StreamingMinFilter.new(window_size, send_first_at)
|
||||
return cg.Pvariable(filter_id, rhs, StreamingMinFilter)
|
||||
# Use sliding window filter - maintains ring buffer
|
||||
rhs = MinFilter.new(window_size, send_every, send_first_at)
|
||||
return cg.Pvariable(filter_id, rhs, MinFilter)
|
||||
|
||||
|
||||
MAX_SCHEMA = cv.All(
|
||||
@@ -474,14 +487,18 @@ MAX_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
@FILTER_REGISTRY.register("max", MaxFilter, MAX_SCHEMA)
|
||||
@FILTER_REGISTRY.register("max", Filter, MAX_SCHEMA)
|
||||
async def max_filter_to_code(config, filter_id):
|
||||
return cg.new_Pvariable(
|
||||
filter_id,
|
||||
config[CONF_WINDOW_SIZE],
|
||||
config[CONF_SEND_EVERY],
|
||||
config[CONF_SEND_FIRST_AT],
|
||||
)
|
||||
window_size: int = config[CONF_WINDOW_SIZE]
|
||||
send_every: int = config[CONF_SEND_EVERY]
|
||||
send_first_at: int = config[CONF_SEND_FIRST_AT]
|
||||
|
||||
# Optimization: Use streaming filter for batch windows (window_size == send_every)
|
||||
if window_size == send_every:
|
||||
rhs = StreamingMaxFilter.new(window_size, send_first_at)
|
||||
return cg.Pvariable(filter_id, rhs, StreamingMaxFilter)
|
||||
rhs = MaxFilter.new(window_size, send_every, send_first_at)
|
||||
return cg.Pvariable(filter_id, rhs, MaxFilter)
|
||||
|
||||
|
||||
SLIDING_AVERAGE_SCHEMA = cv.All(
|
||||
@@ -498,16 +515,20 @@ SLIDING_AVERAGE_SCHEMA = cv.All(
|
||||
|
||||
@FILTER_REGISTRY.register(
|
||||
"sliding_window_moving_average",
|
||||
SlidingWindowMovingAverageFilter,
|
||||
Filter,
|
||||
SLIDING_AVERAGE_SCHEMA,
|
||||
)
|
||||
async def sliding_window_moving_average_filter_to_code(config, filter_id):
|
||||
return cg.new_Pvariable(
|
||||
filter_id,
|
||||
config[CONF_WINDOW_SIZE],
|
||||
config[CONF_SEND_EVERY],
|
||||
config[CONF_SEND_FIRST_AT],
|
||||
)
|
||||
window_size: int = config[CONF_WINDOW_SIZE]
|
||||
send_every: int = config[CONF_SEND_EVERY]
|
||||
send_first_at: int = config[CONF_SEND_FIRST_AT]
|
||||
|
||||
# Optimization: Use streaming filter for batch windows (window_size == send_every)
|
||||
if window_size == send_every:
|
||||
rhs = StreamingMovingAverageFilter.new(window_size, send_first_at)
|
||||
return cg.Pvariable(filter_id, rhs, StreamingMovingAverageFilter)
|
||||
rhs = SlidingWindowMovingAverageFilter.new(window_size, send_every, send_first_at)
|
||||
return cg.Pvariable(filter_id, rhs, SlidingWindowMovingAverageFilter)
|
||||
|
||||
|
||||
EXPONENTIAL_AVERAGE_SCHEMA = cv.All(
|
||||
|
||||
@@ -32,50 +32,76 @@ void Filter::initialize(Sensor *parent, Filter *next) {
|
||||
this->next_ = next;
|
||||
}
|
||||
|
||||
// MedianFilter
|
||||
MedianFilter::MedianFilter(size_t window_size, size_t send_every, size_t send_first_at)
|
||||
: send_every_(send_every), send_at_(send_every - send_first_at), window_size_(window_size) {}
|
||||
void MedianFilter::set_send_every(size_t send_every) { this->send_every_ = send_every; }
|
||||
void MedianFilter::set_window_size(size_t window_size) { this->window_size_ = window_size; }
|
||||
optional<float> MedianFilter::new_value(float value) {
|
||||
while (this->queue_.size() >= this->window_size_) {
|
||||
this->queue_.pop_front();
|
||||
}
|
||||
this->queue_.push_back(value);
|
||||
ESP_LOGVV(TAG, "MedianFilter(%p)::new_value(%f)", this, value);
|
||||
// SlidingWindowFilter
|
||||
SlidingWindowFilter::SlidingWindowFilter(size_t window_size, size_t send_every, size_t send_first_at)
|
||||
: window_size_(window_size), send_every_(send_every), send_at_(send_every - send_first_at) {
|
||||
// Allocate ring buffer once at initialization
|
||||
this->window_.init(window_size);
|
||||
}
|
||||
|
||||
optional<float> SlidingWindowFilter::new_value(float value) {
|
||||
// Add value to ring buffer
|
||||
if (this->window_count_ < this->window_size_) {
|
||||
// Buffer not yet full - just append
|
||||
this->window_.push_back(value);
|
||||
this->window_count_++;
|
||||
} else {
|
||||
// Buffer full - overwrite oldest value (ring buffer)
|
||||
this->window_[this->window_head_] = value;
|
||||
this->window_head_++;
|
||||
if (this->window_head_ >= this->window_size_) {
|
||||
this->window_head_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we should send a result
|
||||
if (++this->send_at_ >= this->send_every_) {
|
||||
this->send_at_ = 0;
|
||||
|
||||
float median = NAN;
|
||||
if (!this->queue_.empty()) {
|
||||
// Copy queue without NaN values
|
||||
std::vector<float> median_queue;
|
||||
median_queue.reserve(this->queue_.size());
|
||||
for (auto v : this->queue_) {
|
||||
if (!std::isnan(v)) {
|
||||
median_queue.push_back(v);
|
||||
}
|
||||
}
|
||||
|
||||
sort(median_queue.begin(), median_queue.end());
|
||||
|
||||
size_t queue_size = median_queue.size();
|
||||
if (queue_size) {
|
||||
if (queue_size % 2) {
|
||||
median = median_queue[queue_size / 2];
|
||||
} else {
|
||||
median = (median_queue[queue_size / 2] + median_queue[(queue_size / 2) - 1]) / 2.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGVV(TAG, "MedianFilter(%p)::new_value(%f) SENDING %f", this, value, median);
|
||||
return median;
|
||||
float result = this->compute_result();
|
||||
ESP_LOGVV(TAG, "SlidingWindowFilter(%p)::new_value(%f) SENDING %f", this, value, result);
|
||||
return result;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
// SortedWindowFilter
|
||||
FixedVector<float> SortedWindowFilter::get_window_values_() {
|
||||
// Copy window without NaN values using FixedVector (no heap allocation)
|
||||
// Returns unsorted values - caller will use std::nth_element for partial sorting as needed
|
||||
FixedVector<float> values;
|
||||
values.init(this->window_count_);
|
||||
for (size_t i = 0; i < this->window_count_; i++) {
|
||||
float v = this->window_[i];
|
||||
if (!std::isnan(v)) {
|
||||
values.push_back(v);
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
// MedianFilter
|
||||
float MedianFilter::compute_result() {
|
||||
FixedVector<float> values = this->get_window_values_();
|
||||
if (values.empty())
|
||||
return NAN;
|
||||
|
||||
size_t size = values.size();
|
||||
size_t mid = size / 2;
|
||||
|
||||
if (size % 2) {
|
||||
// Odd number of elements - use nth_element to find middle element
|
||||
std::nth_element(values.begin(), values.begin() + mid, values.end());
|
||||
return values[mid];
|
||||
}
|
||||
// Even number of elements - need both middle elements
|
||||
// Use nth_element to find upper middle element
|
||||
std::nth_element(values.begin(), values.begin() + mid, values.end());
|
||||
float upper = values[mid];
|
||||
// Find the maximum of the lower half (which is now everything before mid)
|
||||
float lower = *std::max_element(values.begin(), values.begin() + mid);
|
||||
return (lower + upper) / 2.0f;
|
||||
}
|
||||
|
||||
// SkipInitialFilter
|
||||
SkipInitialFilter::SkipInitialFilter(size_t num_to_ignore) : num_to_ignore_(num_to_ignore) {}
|
||||
optional<float> SkipInitialFilter::new_value(float value) {
|
||||
@@ -91,136 +117,39 @@ optional<float> SkipInitialFilter::new_value(float value) {
|
||||
|
||||
// QuantileFilter
|
||||
QuantileFilter::QuantileFilter(size_t window_size, size_t send_every, size_t send_first_at, float quantile)
|
||||
: send_every_(send_every), send_at_(send_every - send_first_at), window_size_(window_size), quantile_(quantile) {}
|
||||
void QuantileFilter::set_send_every(size_t send_every) { this->send_every_ = send_every; }
|
||||
void QuantileFilter::set_window_size(size_t window_size) { this->window_size_ = window_size; }
|
||||
void QuantileFilter::set_quantile(float quantile) { this->quantile_ = quantile; }
|
||||
optional<float> QuantileFilter::new_value(float value) {
|
||||
while (this->queue_.size() >= this->window_size_) {
|
||||
this->queue_.pop_front();
|
||||
}
|
||||
this->queue_.push_back(value);
|
||||
ESP_LOGVV(TAG, "QuantileFilter(%p)::new_value(%f), quantile:%f", this, value, this->quantile_);
|
||||
: SortedWindowFilter(window_size, send_every, send_first_at), quantile_(quantile) {}
|
||||
|
||||
if (++this->send_at_ >= this->send_every_) {
|
||||
this->send_at_ = 0;
|
||||
float QuantileFilter::compute_result() {
|
||||
FixedVector<float> values = this->get_window_values_();
|
||||
if (values.empty())
|
||||
return NAN;
|
||||
|
||||
float result = NAN;
|
||||
if (!this->queue_.empty()) {
|
||||
// Copy queue without NaN values
|
||||
std::vector<float> quantile_queue;
|
||||
for (auto v : this->queue_) {
|
||||
if (!std::isnan(v)) {
|
||||
quantile_queue.push_back(v);
|
||||
}
|
||||
}
|
||||
size_t position = ceilf(values.size() * this->quantile_) - 1;
|
||||
ESP_LOGVV(TAG, "QuantileFilter(%p)::position: %zu/%zu", this, position + 1, values.size());
|
||||
|
||||
sort(quantile_queue.begin(), quantile_queue.end());
|
||||
|
||||
size_t queue_size = quantile_queue.size();
|
||||
if (queue_size) {
|
||||
size_t position = ceilf(queue_size * this->quantile_) - 1;
|
||||
ESP_LOGVV(TAG, "QuantileFilter(%p)::position: %zu/%zu", this, position + 1, queue_size);
|
||||
result = quantile_queue[position];
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGVV(TAG, "QuantileFilter(%p)::new_value(%f) SENDING %f", this, value, result);
|
||||
return result;
|
||||
}
|
||||
return {};
|
||||
// Use nth_element to find the quantile element (O(n) instead of O(n log n))
|
||||
std::nth_element(values.begin(), values.begin() + position, values.end());
|
||||
return values[position];
|
||||
}
|
||||
|
||||
// MinFilter
|
||||
MinFilter::MinFilter(size_t window_size, size_t send_every, size_t send_first_at)
|
||||
: send_every_(send_every), send_at_(send_every - send_first_at), window_size_(window_size) {}
|
||||
void MinFilter::set_send_every(size_t send_every) { this->send_every_ = send_every; }
|
||||
void MinFilter::set_window_size(size_t window_size) { this->window_size_ = window_size; }
|
||||
optional<float> MinFilter::new_value(float value) {
|
||||
while (this->queue_.size() >= this->window_size_) {
|
||||
this->queue_.pop_front();
|
||||
}
|
||||
this->queue_.push_back(value);
|
||||
ESP_LOGVV(TAG, "MinFilter(%p)::new_value(%f)", this, value);
|
||||
|
||||
if (++this->send_at_ >= this->send_every_) {
|
||||
this->send_at_ = 0;
|
||||
|
||||
float min = NAN;
|
||||
for (auto v : this->queue_) {
|
||||
if (!std::isnan(v)) {
|
||||
min = std::isnan(min) ? v : std::min(min, v);
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGVV(TAG, "MinFilter(%p)::new_value(%f) SENDING %f", this, value, min);
|
||||
return min;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
float MinFilter::compute_result() { return this->find_extremum_<std::less<float>>(); }
|
||||
|
||||
// MaxFilter
|
||||
MaxFilter::MaxFilter(size_t window_size, size_t send_every, size_t send_first_at)
|
||||
: send_every_(send_every), send_at_(send_every - send_first_at), window_size_(window_size) {}
|
||||
void MaxFilter::set_send_every(size_t send_every) { this->send_every_ = send_every; }
|
||||
void MaxFilter::set_window_size(size_t window_size) { this->window_size_ = window_size; }
|
||||
optional<float> MaxFilter::new_value(float value) {
|
||||
while (this->queue_.size() >= this->window_size_) {
|
||||
this->queue_.pop_front();
|
||||
}
|
||||
this->queue_.push_back(value);
|
||||
ESP_LOGVV(TAG, "MaxFilter(%p)::new_value(%f)", this, value);
|
||||
|
||||
if (++this->send_at_ >= this->send_every_) {
|
||||
this->send_at_ = 0;
|
||||
|
||||
float max = NAN;
|
||||
for (auto v : this->queue_) {
|
||||
if (!std::isnan(v)) {
|
||||
max = std::isnan(max) ? v : std::max(max, v);
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGVV(TAG, "MaxFilter(%p)::new_value(%f) SENDING %f", this, value, max);
|
||||
return max;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
float MaxFilter::compute_result() { return this->find_extremum_<std::greater<float>>(); }
|
||||
|
||||
// SlidingWindowMovingAverageFilter
|
||||
SlidingWindowMovingAverageFilter::SlidingWindowMovingAverageFilter(size_t window_size, size_t send_every,
|
||||
size_t send_first_at)
|
||||
: send_every_(send_every), send_at_(send_every - send_first_at), window_size_(window_size) {}
|
||||
void SlidingWindowMovingAverageFilter::set_send_every(size_t send_every) { this->send_every_ = send_every; }
|
||||
void SlidingWindowMovingAverageFilter::set_window_size(size_t window_size) { this->window_size_ = window_size; }
|
||||
optional<float> SlidingWindowMovingAverageFilter::new_value(float value) {
|
||||
while (this->queue_.size() >= this->window_size_) {
|
||||
this->queue_.pop_front();
|
||||
}
|
||||
this->queue_.push_back(value);
|
||||
ESP_LOGVV(TAG, "SlidingWindowMovingAverageFilter(%p)::new_value(%f)", this, value);
|
||||
|
||||
if (++this->send_at_ >= this->send_every_) {
|
||||
this->send_at_ = 0;
|
||||
|
||||
float sum = 0;
|
||||
size_t valid_count = 0;
|
||||
for (auto v : this->queue_) {
|
||||
if (!std::isnan(v)) {
|
||||
sum += v;
|
||||
valid_count++;
|
||||
}
|
||||
float SlidingWindowMovingAverageFilter::compute_result() {
|
||||
float sum = 0;
|
||||
size_t valid_count = 0;
|
||||
for (size_t i = 0; i < this->window_count_; i++) {
|
||||
float v = this->window_[i];
|
||||
if (!std::isnan(v)) {
|
||||
sum += v;
|
||||
valid_count++;
|
||||
}
|
||||
|
||||
float average = NAN;
|
||||
if (valid_count) {
|
||||
average = sum / valid_count;
|
||||
}
|
||||
|
||||
ESP_LOGVV(TAG, "SlidingWindowMovingAverageFilter(%p)::new_value(%f) SENDING %f", this, value, average);
|
||||
return average;
|
||||
}
|
||||
return {};
|
||||
return valid_count ? sum / valid_count : NAN;
|
||||
}
|
||||
|
||||
// ExponentialMovingAverageFilter
|
||||
@@ -299,27 +228,40 @@ MultiplyFilter::MultiplyFilter(TemplatableValue<float> multiplier) : multiplier_
|
||||
|
||||
optional<float> MultiplyFilter::new_value(float value) { return value * this->multiplier_.value(); }
|
||||
|
||||
// FilterOutValueFilter
|
||||
FilterOutValueFilter::FilterOutValueFilter(std::vector<TemplatableValue<float>> values_to_filter_out)
|
||||
: values_to_filter_out_(std::move(values_to_filter_out)) {}
|
||||
// ValueListFilter (base class)
|
||||
ValueListFilter::ValueListFilter(std::initializer_list<TemplatableValue<float>> values) : values_(values) {}
|
||||
|
||||
optional<float> FilterOutValueFilter::new_value(float value) {
|
||||
bool ValueListFilter::value_matches_any_(float sensor_value) {
|
||||
int8_t accuracy = this->parent_->get_accuracy_decimals();
|
||||
float accuracy_mult = powf(10.0f, accuracy);
|
||||
for (auto filter_value : this->values_to_filter_out_) {
|
||||
if (std::isnan(filter_value.value())) {
|
||||
if (std::isnan(value)) {
|
||||
return {};
|
||||
}
|
||||
float rounded_sensor = roundf(accuracy_mult * sensor_value);
|
||||
|
||||
for (auto &filter_value : this->values_) {
|
||||
float fv = filter_value.value();
|
||||
|
||||
// Handle NaN comparison
|
||||
if (std::isnan(fv)) {
|
||||
if (std::isnan(sensor_value))
|
||||
return true;
|
||||
continue;
|
||||
}
|
||||
float rounded_filter_out = roundf(accuracy_mult * filter_value.value());
|
||||
float rounded_value = roundf(accuracy_mult * value);
|
||||
if (rounded_filter_out == rounded_value) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Compare rounded values
|
||||
if (roundf(accuracy_mult * fv) == rounded_sensor)
|
||||
return true;
|
||||
}
|
||||
return value;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// FilterOutValueFilter
|
||||
FilterOutValueFilter::FilterOutValueFilter(std::initializer_list<TemplatableValue<float>> values_to_filter_out)
|
||||
: ValueListFilter(values_to_filter_out) {}
|
||||
|
||||
optional<float> FilterOutValueFilter::new_value(float value) {
|
||||
if (this->value_matches_any_(value))
|
||||
return {}; // Filter out
|
||||
return value; // Pass through
|
||||
}
|
||||
|
||||
// ThrottleFilter
|
||||
@@ -334,33 +276,15 @@ optional<float> ThrottleFilter::new_value(float value) {
|
||||
}
|
||||
|
||||
// ThrottleWithPriorityFilter
|
||||
ThrottleWithPriorityFilter::ThrottleWithPriorityFilter(uint32_t min_time_between_inputs,
|
||||
std::vector<TemplatableValue<float>> prioritized_values)
|
||||
: min_time_between_inputs_(min_time_between_inputs), prioritized_values_(std::move(prioritized_values)) {}
|
||||
ThrottleWithPriorityFilter::ThrottleWithPriorityFilter(
|
||||
uint32_t min_time_between_inputs, std::initializer_list<TemplatableValue<float>> prioritized_values)
|
||||
: ValueListFilter(prioritized_values), min_time_between_inputs_(min_time_between_inputs) {}
|
||||
|
||||
optional<float> ThrottleWithPriorityFilter::new_value(float value) {
|
||||
bool is_prioritized_value = false;
|
||||
int8_t accuracy = this->parent_->get_accuracy_decimals();
|
||||
float accuracy_mult = powf(10.0f, accuracy);
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
// First, determine if the new value is one of the prioritized values
|
||||
for (auto prioritized_value : this->prioritized_values_) {
|
||||
if (std::isnan(prioritized_value.value())) {
|
||||
if (std::isnan(value)) {
|
||||
is_prioritized_value = true;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
float rounded_prioritized_value = roundf(accuracy_mult * prioritized_value.value());
|
||||
float rounded_value = roundf(accuracy_mult * value);
|
||||
if (rounded_prioritized_value == rounded_value) {
|
||||
is_prioritized_value = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Finally, determine if the new value should be throttled and pass it through if not
|
||||
if (this->last_input_ == 0 || now - this->last_input_ >= min_time_between_inputs_ || is_prioritized_value) {
|
||||
// Allow value through if: no previous input, time expired, or is prioritized
|
||||
if (this->last_input_ == 0 || now - this->last_input_ >= min_time_between_inputs_ ||
|
||||
this->value_matches_any_(value)) {
|
||||
this->last_input_ = now;
|
||||
return value;
|
||||
}
|
||||
@@ -543,5 +467,78 @@ optional<float> ToNTCTemperatureFilter::new_value(float value) {
|
||||
return temp;
|
||||
}
|
||||
|
||||
// StreamingFilter (base class)
|
||||
StreamingFilter::StreamingFilter(size_t window_size, size_t send_first_at)
|
||||
: window_size_(window_size), send_first_at_(send_first_at) {}
|
||||
|
||||
optional<float> StreamingFilter::new_value(float value) {
|
||||
// Process the value (child class tracks min/max/sum/etc)
|
||||
this->process_value(value);
|
||||
|
||||
this->count_++;
|
||||
|
||||
// Check if we should send (handle send_first_at for first value)
|
||||
bool should_send = false;
|
||||
if (this->first_send_ && this->count_ >= this->send_first_at_) {
|
||||
should_send = true;
|
||||
this->first_send_ = false;
|
||||
} else if (!this->first_send_ && this->count_ >= this->window_size_) {
|
||||
should_send = true;
|
||||
}
|
||||
|
||||
if (should_send) {
|
||||
float result = this->compute_batch_result();
|
||||
// Reset for next batch
|
||||
this->count_ = 0;
|
||||
this->reset_batch();
|
||||
ESP_LOGVV(TAG, "StreamingFilter(%p)::new_value(%f) SENDING %f", this, value, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
// StreamingMinFilter
|
||||
void StreamingMinFilter::process_value(float value) {
|
||||
// Update running minimum (ignore NaN values)
|
||||
if (!std::isnan(value)) {
|
||||
this->current_min_ = std::isnan(this->current_min_) ? value : std::min(this->current_min_, value);
|
||||
}
|
||||
}
|
||||
|
||||
float StreamingMinFilter::compute_batch_result() { return this->current_min_; }
|
||||
|
||||
void StreamingMinFilter::reset_batch() { this->current_min_ = NAN; }
|
||||
|
||||
// StreamingMaxFilter
|
||||
void StreamingMaxFilter::process_value(float value) {
|
||||
// Update running maximum (ignore NaN values)
|
||||
if (!std::isnan(value)) {
|
||||
this->current_max_ = std::isnan(this->current_max_) ? value : std::max(this->current_max_, value);
|
||||
}
|
||||
}
|
||||
|
||||
float StreamingMaxFilter::compute_batch_result() { return this->current_max_; }
|
||||
|
||||
void StreamingMaxFilter::reset_batch() { this->current_max_ = NAN; }
|
||||
|
||||
// StreamingMovingAverageFilter
|
||||
void StreamingMovingAverageFilter::process_value(float value) {
|
||||
// Accumulate sum (ignore NaN values)
|
||||
if (!std::isnan(value)) {
|
||||
this->sum_ += value;
|
||||
this->valid_count_++;
|
||||
}
|
||||
}
|
||||
|
||||
float StreamingMovingAverageFilter::compute_batch_result() {
|
||||
return this->valid_count_ > 0 ? this->sum_ / this->valid_count_ : NAN;
|
||||
}
|
||||
|
||||
void StreamingMovingAverageFilter::reset_batch() {
|
||||
this->sum_ = 0.0f;
|
||||
this->valid_count_ = 0;
|
||||
}
|
||||
|
||||
} // namespace sensor
|
||||
} // namespace esphome
|
||||
|
||||
@@ -44,11 +44,75 @@ class Filter {
|
||||
Sensor *parent_{nullptr};
|
||||
};
|
||||
|
||||
/** Base class for filters that use a sliding window of values.
|
||||
*
|
||||
* Uses a ring buffer to efficiently maintain a fixed-size sliding window without
|
||||
* reallocations or pop_front() overhead. Eliminates deque fragmentation issues.
|
||||
*/
|
||||
class SlidingWindowFilter : public Filter {
|
||||
public:
|
||||
SlidingWindowFilter(size_t window_size, size_t send_every, size_t send_first_at);
|
||||
|
||||
optional<float> new_value(float value) final;
|
||||
|
||||
protected:
|
||||
/// Called by new_value() to compute the filtered result from the current window
|
||||
virtual float compute_result() = 0;
|
||||
|
||||
/// Access the sliding window values (ring buffer implementation)
|
||||
/// Use: for (size_t i = 0; i < window_count_; i++) { float val = window_[i]; }
|
||||
FixedVector<float> window_;
|
||||
size_t window_head_{0}; ///< Index where next value will be written
|
||||
size_t window_count_{0}; ///< Number of valid values in window (0 to window_size_)
|
||||
size_t window_size_; ///< Maximum window size
|
||||
size_t send_every_; ///< Send result every N values
|
||||
size_t send_at_; ///< Counter for send_every
|
||||
};
|
||||
|
||||
/** Base class for Min/Max filters.
|
||||
*
|
||||
* Provides a templated helper to find extremum values efficiently.
|
||||
*/
|
||||
class MinMaxFilter : public SlidingWindowFilter {
|
||||
public:
|
||||
using SlidingWindowFilter::SlidingWindowFilter;
|
||||
|
||||
protected:
|
||||
/// Helper to find min or max value in window, skipping NaN values
|
||||
/// Usage: find_extremum_<std::less<float>>() for min, find_extremum_<std::greater<float>>() for max
|
||||
template<typename Compare> float find_extremum_() {
|
||||
float result = NAN;
|
||||
Compare comp;
|
||||
for (size_t i = 0; i < this->window_count_; i++) {
|
||||
float v = this->window_[i];
|
||||
if (!std::isnan(v)) {
|
||||
result = std::isnan(result) ? v : (comp(v, result) ? v : result);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
/** Base class for filters that need a sorted window (Median, Quantile).
|
||||
*
|
||||
* Extends SlidingWindowFilter to provide a helper that filters out NaN values.
|
||||
* Derived classes use std::nth_element for efficient partial sorting.
|
||||
*/
|
||||
class SortedWindowFilter : public SlidingWindowFilter {
|
||||
public:
|
||||
using SlidingWindowFilter::SlidingWindowFilter;
|
||||
|
||||
protected:
|
||||
/// Helper to get non-NaN values from the window (not sorted - caller will use nth_element)
|
||||
/// Returns empty FixedVector if all values are NaN
|
||||
FixedVector<float> get_window_values_();
|
||||
};
|
||||
|
||||
/** Simple quantile filter.
|
||||
*
|
||||
* Takes the quantile of the last <send_every> values and pushes it out every <send_every>.
|
||||
* Takes the quantile of the last <window_size> values and pushes it out every <send_every>.
|
||||
*/
|
||||
class QuantileFilter : public Filter {
|
||||
class QuantileFilter : public SortedWindowFilter {
|
||||
public:
|
||||
/** Construct a QuantileFilter.
|
||||
*
|
||||
@@ -61,25 +125,18 @@ class QuantileFilter : public Filter {
|
||||
*/
|
||||
explicit QuantileFilter(size_t window_size, size_t send_every, size_t send_first_at, float quantile);
|
||||
|
||||
optional<float> new_value(float value) override;
|
||||
|
||||
void set_send_every(size_t send_every);
|
||||
void set_window_size(size_t window_size);
|
||||
void set_quantile(float quantile);
|
||||
void set_quantile(float quantile) { this->quantile_ = quantile; }
|
||||
|
||||
protected:
|
||||
std::deque<float> queue_;
|
||||
size_t send_every_;
|
||||
size_t send_at_;
|
||||
size_t window_size_;
|
||||
float compute_result() override;
|
||||
float quantile_;
|
||||
};
|
||||
|
||||
/** Simple median filter.
|
||||
*
|
||||
* Takes the median of the last <send_every> values and pushes it out every <send_every>.
|
||||
* Takes the median of the last <window_size> values and pushes it out every <send_every>.
|
||||
*/
|
||||
class MedianFilter : public Filter {
|
||||
class MedianFilter : public SortedWindowFilter {
|
||||
public:
|
||||
/** Construct a MedianFilter.
|
||||
*
|
||||
@@ -89,18 +146,10 @@ class MedianFilter : public Filter {
|
||||
* on startup being published on the first *raw* value, so with no filter applied. Must be less than or equal to
|
||||
* send_every.
|
||||
*/
|
||||
explicit MedianFilter(size_t window_size, size_t send_every, size_t send_first_at);
|
||||
|
||||
optional<float> new_value(float value) override;
|
||||
|
||||
void set_send_every(size_t send_every);
|
||||
void set_window_size(size_t window_size);
|
||||
using SortedWindowFilter::SortedWindowFilter;
|
||||
|
||||
protected:
|
||||
std::deque<float> queue_;
|
||||
size_t send_every_;
|
||||
size_t send_at_;
|
||||
size_t window_size_;
|
||||
float compute_result() override;
|
||||
};
|
||||
|
||||
/** Simple skip filter.
|
||||
@@ -123,9 +172,9 @@ class SkipInitialFilter : public Filter {
|
||||
|
||||
/** Simple min filter.
|
||||
*
|
||||
* Takes the min of the last <send_every> values and pushes it out every <send_every>.
|
||||
* Takes the min of the last <window_size> values and pushes it out every <send_every>.
|
||||
*/
|
||||
class MinFilter : public Filter {
|
||||
class MinFilter : public MinMaxFilter {
|
||||
public:
|
||||
/** Construct a MinFilter.
|
||||
*
|
||||
@@ -135,25 +184,17 @@ class MinFilter : public Filter {
|
||||
* on startup being published on the first *raw* value, so with no filter applied. Must be less than or equal to
|
||||
* send_every.
|
||||
*/
|
||||
explicit MinFilter(size_t window_size, size_t send_every, size_t send_first_at);
|
||||
|
||||
optional<float> new_value(float value) override;
|
||||
|
||||
void set_send_every(size_t send_every);
|
||||
void set_window_size(size_t window_size);
|
||||
using MinMaxFilter::MinMaxFilter;
|
||||
|
||||
protected:
|
||||
std::deque<float> queue_;
|
||||
size_t send_every_;
|
||||
size_t send_at_;
|
||||
size_t window_size_;
|
||||
float compute_result() override;
|
||||
};
|
||||
|
||||
/** Simple max filter.
|
||||
*
|
||||
* Takes the max of the last <send_every> values and pushes it out every <send_every>.
|
||||
* Takes the max of the last <window_size> values and pushes it out every <send_every>.
|
||||
*/
|
||||
class MaxFilter : public Filter {
|
||||
class MaxFilter : public MinMaxFilter {
|
||||
public:
|
||||
/** Construct a MaxFilter.
|
||||
*
|
||||
@@ -163,18 +204,10 @@ class MaxFilter : public Filter {
|
||||
* on startup being published on the first *raw* value, so with no filter applied. Must be less than or equal to
|
||||
* send_every.
|
||||
*/
|
||||
explicit MaxFilter(size_t window_size, size_t send_every, size_t send_first_at);
|
||||
|
||||
optional<float> new_value(float value) override;
|
||||
|
||||
void set_send_every(size_t send_every);
|
||||
void set_window_size(size_t window_size);
|
||||
using MinMaxFilter::MinMaxFilter;
|
||||
|
||||
protected:
|
||||
std::deque<float> queue_;
|
||||
size_t send_every_;
|
||||
size_t send_at_;
|
||||
size_t window_size_;
|
||||
float compute_result() override;
|
||||
};
|
||||
|
||||
/** Simple sliding window moving average filter.
|
||||
@@ -182,7 +215,7 @@ class MaxFilter : public Filter {
|
||||
* Essentially just takes takes the average of the last window_size values and pushes them out
|
||||
* every send_every.
|
||||
*/
|
||||
class SlidingWindowMovingAverageFilter : public Filter {
|
||||
class SlidingWindowMovingAverageFilter : public SlidingWindowFilter {
|
||||
public:
|
||||
/** Construct a SlidingWindowMovingAverageFilter.
|
||||
*
|
||||
@@ -192,18 +225,10 @@ class SlidingWindowMovingAverageFilter : public Filter {
|
||||
* on startup being published on the first *raw* value, so with no filter applied. Must be less than or equal to
|
||||
* send_every.
|
||||
*/
|
||||
explicit SlidingWindowMovingAverageFilter(size_t window_size, size_t send_every, size_t send_first_at);
|
||||
|
||||
optional<float> new_value(float value) override;
|
||||
|
||||
void set_send_every(size_t send_every);
|
||||
void set_window_size(size_t window_size);
|
||||
using SlidingWindowFilter::SlidingWindowFilter;
|
||||
|
||||
protected:
|
||||
std::deque<float> queue_;
|
||||
size_t send_every_;
|
||||
size_t send_at_;
|
||||
size_t window_size_;
|
||||
float compute_result() override;
|
||||
};
|
||||
|
||||
/** Simple exponential moving average filter.
|
||||
@@ -292,15 +317,28 @@ class MultiplyFilter : public Filter {
|
||||
TemplatableValue<float> multiplier_;
|
||||
};
|
||||
|
||||
/** Base class for filters that compare sensor values against a list of configured values.
|
||||
*
|
||||
* This base class provides common functionality for filters that need to check if a sensor
|
||||
* value matches any value in a configured list, with proper handling of NaN values and
|
||||
* accuracy-based rounding for comparisons.
|
||||
*/
|
||||
class ValueListFilter : public Filter {
|
||||
protected:
|
||||
explicit ValueListFilter(std::initializer_list<TemplatableValue<float>> values);
|
||||
|
||||
/// Check if sensor value matches any configured value (with accuracy rounding)
|
||||
bool value_matches_any_(float sensor_value);
|
||||
|
||||
FixedVector<TemplatableValue<float>> values_;
|
||||
};
|
||||
|
||||
/// A simple filter that only forwards the filter chain if it doesn't receive `value_to_filter_out`.
|
||||
class FilterOutValueFilter : public Filter {
|
||||
class FilterOutValueFilter : public ValueListFilter {
|
||||
public:
|
||||
explicit FilterOutValueFilter(std::vector<TemplatableValue<float>> values_to_filter_out);
|
||||
explicit FilterOutValueFilter(std::initializer_list<TemplatableValue<float>> values_to_filter_out);
|
||||
|
||||
optional<float> new_value(float value) override;
|
||||
|
||||
protected:
|
||||
std::vector<TemplatableValue<float>> values_to_filter_out_;
|
||||
};
|
||||
|
||||
class ThrottleFilter : public Filter {
|
||||
@@ -315,17 +353,16 @@ class ThrottleFilter : public Filter {
|
||||
};
|
||||
|
||||
/// Same as 'throttle' but will immediately publish values contained in `value_to_prioritize`.
|
||||
class ThrottleWithPriorityFilter : public Filter {
|
||||
class ThrottleWithPriorityFilter : public ValueListFilter {
|
||||
public:
|
||||
explicit ThrottleWithPriorityFilter(uint32_t min_time_between_inputs,
|
||||
std::vector<TemplatableValue<float>> prioritized_values);
|
||||
std::initializer_list<TemplatableValue<float>> prioritized_values);
|
||||
|
||||
optional<float> new_value(float value) override;
|
||||
|
||||
protected:
|
||||
uint32_t last_input_{0};
|
||||
uint32_t min_time_between_inputs_;
|
||||
std::vector<TemplatableValue<float>> prioritized_values_;
|
||||
};
|
||||
|
||||
class TimeoutFilter : public Filter, public Component {
|
||||
@@ -476,5 +513,81 @@ class ToNTCTemperatureFilter : public Filter {
|
||||
double c_;
|
||||
};
|
||||
|
||||
/** Base class for streaming filters (batch windows where window_size == send_every).
|
||||
*
|
||||
* When window_size equals send_every, we don't need a sliding window.
|
||||
* This base class handles the common batching logic.
|
||||
*/
|
||||
class StreamingFilter : public Filter {
|
||||
public:
|
||||
StreamingFilter(size_t window_size, size_t send_first_at);
|
||||
|
||||
optional<float> new_value(float value) final;
|
||||
|
||||
protected:
|
||||
/// Called by new_value() to process each value in the batch
|
||||
virtual void process_value(float value) = 0;
|
||||
|
||||
/// Called by new_value() to compute the result after collecting window_size values
|
||||
virtual float compute_batch_result() = 0;
|
||||
|
||||
/// Called by new_value() to reset internal state after sending a result
|
||||
virtual void reset_batch() = 0;
|
||||
|
||||
size_t window_size_;
|
||||
size_t count_{0};
|
||||
size_t send_first_at_;
|
||||
bool first_send_{true};
|
||||
};
|
||||
|
||||
/** Streaming min filter for batch windows (window_size == send_every).
|
||||
*
|
||||
* Uses O(1) memory instead of O(n) by tracking only the minimum value.
|
||||
*/
|
||||
class StreamingMinFilter : public StreamingFilter {
|
||||
public:
|
||||
using StreamingFilter::StreamingFilter;
|
||||
|
||||
protected:
|
||||
void process_value(float value) override;
|
||||
float compute_batch_result() override;
|
||||
void reset_batch() override;
|
||||
|
||||
float current_min_{NAN};
|
||||
};
|
||||
|
||||
/** Streaming max filter for batch windows (window_size == send_every).
|
||||
*
|
||||
* Uses O(1) memory instead of O(n) by tracking only the maximum value.
|
||||
*/
|
||||
class StreamingMaxFilter : public StreamingFilter {
|
||||
public:
|
||||
using StreamingFilter::StreamingFilter;
|
||||
|
||||
protected:
|
||||
void process_value(float value) override;
|
||||
float compute_batch_result() override;
|
||||
void reset_batch() override;
|
||||
|
||||
float current_max_{NAN};
|
||||
};
|
||||
|
||||
/** Streaming moving average filter for batch windows (window_size == send_every).
|
||||
*
|
||||
* Uses O(1) memory instead of O(n) by tracking only sum and count.
|
||||
*/
|
||||
class StreamingMovingAverageFilter : public StreamingFilter {
|
||||
public:
|
||||
using StreamingFilter::StreamingFilter;
|
||||
|
||||
protected:
|
||||
void process_value(float value) override;
|
||||
float compute_batch_result() override;
|
||||
void reset_batch() override;
|
||||
|
||||
float sum_{0.0f};
|
||||
size_t valid_count_{0};
|
||||
};
|
||||
|
||||
} // namespace sensor
|
||||
} // namespace esphome
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from collections.abc import Callable, MutableMapping
|
||||
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.core import CORE
|
||||
@@ -9,6 +11,32 @@ IMPLEMENTATION_LWIP_TCP = "lwip_tcp"
|
||||
IMPLEMENTATION_LWIP_SOCKETS = "lwip_sockets"
|
||||
IMPLEMENTATION_BSD_SOCKETS = "bsd_sockets"
|
||||
|
||||
# Socket tracking infrastructure
|
||||
# Components register their socket needs and platforms read this to configure appropriately
|
||||
KEY_SOCKET_CONSUMERS = "socket_consumers"
|
||||
|
||||
|
||||
def consume_sockets(
|
||||
value: int, consumer: str
|
||||
) -> Callable[[MutableMapping], MutableMapping]:
|
||||
"""Register socket usage for a component.
|
||||
|
||||
Args:
|
||||
value: Number of sockets needed by the component
|
||||
consumer: Name of the component consuming the sockets
|
||||
|
||||
Returns:
|
||||
A validator function that records the socket usage
|
||||
"""
|
||||
|
||||
def _consume_sockets(config: MutableMapping) -> MutableMapping:
|
||||
consumers: dict[str, int] = CORE.data.setdefault(KEY_SOCKET_CONSUMERS, {})
|
||||
consumers[consumer] = consumers.get(consumer, 0) + value
|
||||
return config
|
||||
|
||||
return _consume_sockets
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.SplitDefault(
|
||||
|
||||
@@ -28,21 +28,6 @@
|
||||
namespace esphome {
|
||||
namespace statsd {
|
||||
|
||||
using sensor_type_t = enum { TYPE_SENSOR, TYPE_BINARY_SENSOR };
|
||||
|
||||
using sensors_t = struct {
|
||||
const char *name;
|
||||
sensor_type_t type;
|
||||
union {
|
||||
#ifdef USE_SENSOR
|
||||
esphome::sensor::Sensor *sensor;
|
||||
#endif
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
esphome::binary_sensor::BinarySensor *binary_sensor;
|
||||
#endif
|
||||
};
|
||||
};
|
||||
|
||||
class StatsdComponent : public PollingComponent {
|
||||
public:
|
||||
~StatsdComponent();
|
||||
@@ -71,6 +56,20 @@ class StatsdComponent : public PollingComponent {
|
||||
const char *prefix_;
|
||||
uint16_t port_;
|
||||
|
||||
using sensor_type_t = enum { TYPE_SENSOR, TYPE_BINARY_SENSOR };
|
||||
using sensors_t = struct {
|
||||
const char *name;
|
||||
sensor_type_t type;
|
||||
union {
|
||||
#ifdef USE_SENSOR
|
||||
esphome::sensor::Sensor *sensor;
|
||||
#endif
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
esphome::binary_sensor::BinarySensor *binary_sensor;
|
||||
#endif
|
||||
};
|
||||
};
|
||||
|
||||
std::vector<sensors_t> sensors_;
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
|
||||
@@ -6,7 +6,7 @@ import esphome.config_validation as cv
|
||||
from esphome.const import CONF_SUBSTITUTIONS, VALID_SUBSTITUTIONS_CHARACTERS
|
||||
from esphome.yaml_util import ESPHomeDataBase, ESPLiteralValue, make_data_base
|
||||
|
||||
from .jinja import Jinja, JinjaStr, TemplateError, TemplateRuntimeError, has_jinja
|
||||
from .jinja import Jinja, JinjaError, JinjaStr, has_jinja
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
_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),
|
||||
err.message,
|
||||
)
|
||||
except (
|
||||
TemplateError,
|
||||
TemplateRuntimeError,
|
||||
RuntimeError,
|
||||
ArithmeticError,
|
||||
AttributeError,
|
||||
TypeError,
|
||||
) as err:
|
||||
except JinjaError as err:
|
||||
raise cv.Invalid(
|
||||
f"{type(err).__name__} Error evaluating jinja expression '{value}': {str(err)}."
|
||||
f" See {'->'.join(str(x) for x in path)}",
|
||||
f"{err.error_name()} Error evaluating jinja expression '{value}': {str(err.parent())}."
|
||||
f"\nEvaluation stack: (most recent evaluation last)\n{err.stack_trace_str()}"
|
||||
f"\nRelevant context:\n{err.context_trace_str()}"
|
||||
f"\nSee {'->'.join(str(x) for x in path)}",
|
||||
path,
|
||||
)
|
||||
return value
|
||||
|
||||
@@ -6,6 +6,8 @@ import re
|
||||
import jinja2 as jinja
|
||||
from jinja2.sandbox import SandboxedEnvironment
|
||||
|
||||
from esphome.yaml_util import ESPLiteralValue
|
||||
|
||||
TemplateError = jinja.TemplateError
|
||||
TemplateSyntaxError = jinja.TemplateSyntaxError
|
||||
TemplateRuntimeError = jinja.TemplateRuntimeError
|
||||
@@ -26,18 +28,20 @@ def has_jinja(st):
|
||||
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
|
||||
# arbitrary code execution, file access, or other security risks are included.
|
||||
#
|
||||
# The following functions are considered safe:
|
||||
# - math: The entire math module is injected, allowing access to mathematical functions like sin, cos, sqrt, etc.
|
||||
# - ord: Converts a character to its Unicode code point integer.
|
||||
# - chr: Converts an integer to its corresponding Unicode character.
|
||||
# - len: Returns the length of a sequence or collection.
|
||||
#
|
||||
# These functions were chosen because they are pure, have no side effects, and do not provide access
|
||||
# to the file system, environment, or other potentially sensitive resources.
|
||||
SAFE_GLOBAL_FUNCTIONS = {
|
||||
SAFE_GLOBALS = {
|
||||
"math": math, # Inject entire math module
|
||||
"ord": ord,
|
||||
"chr": chr,
|
||||
"len": len,
|
||||
@@ -56,22 +60,62 @@ class JinjaStr(str):
|
||||
later in the main substitutions pass.
|
||||
"""
|
||||
|
||||
Undefined = object()
|
||||
|
||||
def __new__(cls, value: str, upvalues=None):
|
||||
obj = super().__new__(cls, value)
|
||||
obj.upvalues = upvalues or {}
|
||||
if isinstance(value, JinjaStr):
|
||||
base = str(value)
|
||||
merged = {**value.upvalues, **(upvalues or {})}
|
||||
else:
|
||||
base = value
|
||||
merged = dict(upvalues or {})
|
||||
obj = super().__new__(cls, base)
|
||||
obj.upvalues = merged
|
||||
obj.result = JinjaStr.Undefined
|
||||
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
|
||||
"""
|
||||
|
||||
def __init__(self, context_vars):
|
||||
self.env = SandboxedEnvironment(
|
||||
super().__init__(
|
||||
trim_blocks=True,
|
||||
lstrip_blocks=True,
|
||||
block_start_string="<%",
|
||||
@@ -82,13 +126,20 @@ class Jinja:
|
||||
variable_end_string="}",
|
||||
undefined=jinja.StrictUndefined,
|
||||
)
|
||||
self.env.add_extension("jinja2.ext.do")
|
||||
self.env.globals["math"] = math # Inject entire math module
|
||||
self.context_class = TrackerContext
|
||||
self.add_extension("jinja2.ext.do")
|
||||
self.context_trace = {}
|
||||
self.context_vars = {**context_vars}
|
||||
self.env.globals = {
|
||||
**self.env.globals,
|
||||
for k, v in self.context_vars.items():
|
||||
if isinstance(v, ESPLiteralValue):
|
||||
continue
|
||||
if isinstance(v, str) and not isinstance(v, JinjaStr) and has_jinja(v):
|
||||
self.context_vars[k] = JinjaStr(v, self.context_vars)
|
||||
|
||||
self.globals = {
|
||||
**self.globals,
|
||||
**self.context_vars,
|
||||
**SAFE_GLOBAL_FUNCTIONS,
|
||||
**SAFE_GLOBALS,
|
||||
}
|
||||
|
||||
def safe_eval(self, expr):
|
||||
@@ -110,23 +161,43 @@ class Jinja:
|
||||
result = None
|
||||
override_vars = {}
|
||||
if isinstance(content_str, JinjaStr):
|
||||
if content_str.result is not JinjaStr.Undefined:
|
||||
return content_str.result, None
|
||||
# If `value` is already a JinjaStr, it means we are trying to evaluate it again
|
||||
# in a parent pass.
|
||||
# Hopefully, all required variables are visible now.
|
||||
override_vars = content_str.upvalues
|
||||
|
||||
old_trace = self.context_trace
|
||||
self.context_trace = {}
|
||||
try:
|
||||
template = self.env.from_string(content_str)
|
||||
template = self.from_string(content_str)
|
||||
result = self.safe_eval(template.render(override_vars))
|
||||
if isinstance(result, Undefined):
|
||||
# This happens when the expression is simply an undefined variable. Jinja does not
|
||||
# raise an exception, instead we get "Undefined".
|
||||
# Trigger an UndefinedError exception so we skip to below.
|
||||
print("" + result)
|
||||
print("" + result) # force a UndefinedError exception
|
||||
except (TemplateSyntaxError, UndefinedError) as err:
|
||||
# `content_str` contains a Jinja expression that refers to a variable that is undefined
|
||||
# in this scope. Perhaps it refers to a root substitution that is not visible yet.
|
||||
# Therefore, return 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.
|
||||
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
|
||||
|
||||
@@ -71,9 +71,14 @@ from esphome.const import (
|
||||
CONF_VISUAL,
|
||||
)
|
||||
|
||||
CONF_PRESET_CHANGE = "preset_change"
|
||||
CONF_DEFAULT_PRESET = "default_preset"
|
||||
CONF_HUMIDITY_CONTROL_DEHUMIDIFY_ACTION = "humidity_control_dehumidify_action"
|
||||
CONF_HUMIDITY_CONTROL_HUMIDIFY_ACTION = "humidity_control_humidify_action"
|
||||
CONF_HUMIDITY_CONTROL_OFF_ACTION = "humidity_control_off_action"
|
||||
CONF_HUMIDITY_HYSTERESIS = "humidity_hysteresis"
|
||||
CONF_ON_BOOT_RESTORE_FROM = "on_boot_restore_from"
|
||||
CONF_PRESET_CHANGE = "preset_change"
|
||||
CONF_TARGET_HUMIDITY_CHANGE_ACTION = "target_humidity_change_action"
|
||||
|
||||
CODEOWNERS = ["@kbx81"]
|
||||
|
||||
@@ -241,6 +246,14 @@ def validate_thermostat(config):
|
||||
CONF_MAX_HEATING_RUN_TIME,
|
||||
CONF_SUPPLEMENTAL_HEATING_ACTION,
|
||||
],
|
||||
CONF_HUMIDITY_CONTROL_DEHUMIDIFY_ACTION: [
|
||||
CONF_HUMIDITY_CONTROL_OFF_ACTION,
|
||||
CONF_HUMIDITY_SENSOR,
|
||||
],
|
||||
CONF_HUMIDITY_CONTROL_HUMIDIFY_ACTION: [
|
||||
CONF_HUMIDITY_CONTROL_OFF_ACTION,
|
||||
CONF_HUMIDITY_SENSOR,
|
||||
],
|
||||
}
|
||||
for config_trigger, req_triggers in requirements.items():
|
||||
for req_trigger in req_triggers:
|
||||
@@ -338,7 +351,7 @@ def validate_thermostat(config):
|
||||
# Warn about using the removed CONF_DEFAULT_MODE and advise users
|
||||
if CONF_DEFAULT_MODE in config and config[CONF_DEFAULT_MODE] is not None:
|
||||
raise cv.Invalid(
|
||||
f"{CONF_DEFAULT_MODE} is no longer valid. Please switch to using presets and specify a {CONF_DEFAULT_PRESET}."
|
||||
f"{CONF_DEFAULT_MODE} is no longer valid. Please switch to using presets and specify a {CONF_DEFAULT_PRESET}"
|
||||
)
|
||||
|
||||
default_mode = config[CONF_DEFAULT_MODE]
|
||||
@@ -588,9 +601,24 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_SWING_VERTICAL_ACTION): automation.validate_automation(
|
||||
single=True
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_TARGET_HUMIDITY_CHANGE_ACTION
|
||||
): automation.validate_automation(single=True),
|
||||
cv.Optional(
|
||||
CONF_TARGET_TEMPERATURE_CHANGE_ACTION
|
||||
): automation.validate_automation(single=True),
|
||||
cv.Exclusive(
|
||||
CONF_HUMIDITY_CONTROL_DEHUMIDIFY_ACTION,
|
||||
group_of_exclusion="humidity_control",
|
||||
): automation.validate_automation(single=True),
|
||||
cv.Exclusive(
|
||||
CONF_HUMIDITY_CONTROL_HUMIDIFY_ACTION,
|
||||
group_of_exclusion="humidity_control",
|
||||
): automation.validate_automation(single=True),
|
||||
cv.Optional(
|
||||
CONF_HUMIDITY_CONTROL_OFF_ACTION
|
||||
): automation.validate_automation(single=True),
|
||||
cv.Optional(CONF_HUMIDITY_HYSTERESIS, default=1.0): cv.percentage,
|
||||
cv.Optional(CONF_DEFAULT_MODE, default=None): cv.valid,
|
||||
cv.Optional(CONF_DEFAULT_PRESET): cv.templatable(cv.string),
|
||||
cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature,
|
||||
@@ -882,12 +910,39 @@ async def to_code(config):
|
||||
config[CONF_SWING_VERTICAL_ACTION],
|
||||
)
|
||||
cg.add(var.set_supports_swing_mode_vertical(True))
|
||||
if CONF_TARGET_HUMIDITY_CHANGE_ACTION in config:
|
||||
await automation.build_automation(
|
||||
var.get_humidity_change_trigger(),
|
||||
[],
|
||||
config[CONF_TARGET_HUMIDITY_CHANGE_ACTION],
|
||||
)
|
||||
if CONF_TARGET_TEMPERATURE_CHANGE_ACTION in config:
|
||||
await automation.build_automation(
|
||||
var.get_temperature_change_trigger(),
|
||||
[],
|
||||
config[CONF_TARGET_TEMPERATURE_CHANGE_ACTION],
|
||||
)
|
||||
if CONF_HUMIDITY_CONTROL_DEHUMIDIFY_ACTION in config:
|
||||
cg.add(var.set_supports_dehumidification(True))
|
||||
await automation.build_automation(
|
||||
var.get_humidity_control_dehumidify_action_trigger(),
|
||||
[],
|
||||
config[CONF_HUMIDITY_CONTROL_DEHUMIDIFY_ACTION],
|
||||
)
|
||||
if CONF_HUMIDITY_CONTROL_HUMIDIFY_ACTION in config:
|
||||
cg.add(var.set_supports_humidification(True))
|
||||
await automation.build_automation(
|
||||
var.get_humidity_control_humidify_action_trigger(),
|
||||
[],
|
||||
config[CONF_HUMIDITY_CONTROL_HUMIDIFY_ACTION],
|
||||
)
|
||||
if CONF_HUMIDITY_CONTROL_OFF_ACTION in config:
|
||||
await automation.build_automation(
|
||||
var.get_humidity_control_off_action_trigger(),
|
||||
[],
|
||||
config[CONF_HUMIDITY_CONTROL_OFF_ACTION],
|
||||
)
|
||||
cg.add(var.set_humidity_hysteresis(config[CONF_HUMIDITY_HYSTERESIS]))
|
||||
|
||||
if CONF_PRESET in config:
|
||||
for preset_config in config[CONF_PRESET]:
|
||||
|
||||
@@ -32,6 +32,7 @@ void ThermostatClimate::setup() {
|
||||
if (this->humidity_sensor_ != nullptr) {
|
||||
this->humidity_sensor_->add_on_state_callback([this](float state) {
|
||||
this->current_humidity = state;
|
||||
this->switch_to_humidity_control_action_(this->compute_humidity_control_action_());
|
||||
this->publish_state();
|
||||
});
|
||||
this->current_humidity = this->humidity_sensor_->state;
|
||||
@@ -84,6 +85,8 @@ void ThermostatClimate::refresh() {
|
||||
this->switch_to_supplemental_action_(this->compute_supplemental_action_());
|
||||
this->switch_to_fan_mode_(this->fan_mode.value(), false);
|
||||
this->switch_to_swing_mode_(this->swing_mode, false);
|
||||
this->switch_to_humidity_control_action_(this->compute_humidity_control_action_());
|
||||
this->check_humidity_change_trigger_();
|
||||
this->check_temperature_change_trigger_();
|
||||
this->publish_state();
|
||||
}
|
||||
@@ -129,6 +132,11 @@ bool ThermostatClimate::hysteresis_valid() {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ThermostatClimate::humidity_hysteresis_valid() {
|
||||
return !std::isnan(this->humidity_hysteresis_) && this->humidity_hysteresis_ >= 0.0f &&
|
||||
this->humidity_hysteresis_ < 100.0f;
|
||||
}
|
||||
|
||||
bool ThermostatClimate::limit_setpoints_for_heat_cool() {
|
||||
return this->mode == climate::CLIMATE_MODE_HEAT_COOL ||
|
||||
(this->mode == climate::CLIMATE_MODE_AUTO && this->supports_heat_cool_);
|
||||
@@ -189,6 +197,16 @@ void ThermostatClimate::validate_target_temperature_high() {
|
||||
}
|
||||
}
|
||||
|
||||
void ThermostatClimate::validate_target_humidity() {
|
||||
if (std::isnan(this->target_humidity)) {
|
||||
this->target_humidity =
|
||||
(this->get_traits().get_visual_max_humidity() - this->get_traits().get_visual_min_humidity()) / 2.0f;
|
||||
} else {
|
||||
this->target_humidity = clamp<float>(this->target_humidity, this->get_traits().get_visual_min_humidity(),
|
||||
this->get_traits().get_visual_max_humidity());
|
||||
}
|
||||
}
|
||||
|
||||
void ThermostatClimate::control(const climate::ClimateCall &call) {
|
||||
bool target_temperature_high_changed = false;
|
||||
|
||||
@@ -235,6 +253,10 @@ void ThermostatClimate::control(const climate::ClimateCall &call) {
|
||||
this->validate_target_temperature();
|
||||
}
|
||||
}
|
||||
if (call.get_target_humidity().has_value()) {
|
||||
this->target_humidity = call.get_target_humidity().value();
|
||||
this->validate_target_humidity();
|
||||
}
|
||||
// make any changes happen
|
||||
this->refresh();
|
||||
}
|
||||
@@ -250,6 +272,9 @@ climate::ClimateTraits ThermostatClimate::traits() {
|
||||
if (this->humidity_sensor_ != nullptr)
|
||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY);
|
||||
|
||||
if (this->supports_humidification_ || this->supports_dehumidification_)
|
||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY);
|
||||
|
||||
if (this->supports_auto_)
|
||||
traits.add_supported_mode(climate::CLIMATE_MODE_AUTO);
|
||||
if (this->supports_heat_cool_)
|
||||
@@ -423,6 +448,28 @@ climate::ClimateAction ThermostatClimate::compute_supplemental_action_() {
|
||||
return target_action;
|
||||
}
|
||||
|
||||
HumidificationAction ThermostatClimate::compute_humidity_control_action_() {
|
||||
auto target_action = THERMOSTAT_HUMIDITY_CONTROL_ACTION_OFF;
|
||||
// if hysteresis value or current_humidity is not valid, we go to OFF
|
||||
if (std::isnan(this->current_humidity) || !this->humidity_hysteresis_valid()) {
|
||||
return THERMOSTAT_HUMIDITY_CONTROL_ACTION_OFF;
|
||||
}
|
||||
|
||||
// ensure set point is valid before computing the action
|
||||
this->validate_target_humidity();
|
||||
// everything has been validated so we can now safely compute the action
|
||||
if (this->dehumidification_required_() && this->humidification_required_()) {
|
||||
// this is bad and should never happen, so just stop.
|
||||
// target_action = THERMOSTAT_HUMIDITY_CONTROL_ACTION_OFF;
|
||||
} else if (this->supports_dehumidification_ && this->dehumidification_required_()) {
|
||||
target_action = THERMOSTAT_HUMIDITY_CONTROL_ACTION_DEHUMIDIFY;
|
||||
} else if (this->supports_humidification_ && this->humidification_required_()) {
|
||||
target_action = THERMOSTAT_HUMIDITY_CONTROL_ACTION_HUMIDIFY;
|
||||
}
|
||||
|
||||
return target_action;
|
||||
}
|
||||
|
||||
void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool publish_state) {
|
||||
// setup_complete_ helps us ensure an action is called immediately after boot
|
||||
if ((action == this->action) && this->setup_complete_) {
|
||||
@@ -596,6 +643,44 @@ void ThermostatClimate::trigger_supplemental_action_() {
|
||||
}
|
||||
}
|
||||
|
||||
void ThermostatClimate::switch_to_humidity_control_action_(HumidificationAction action) {
|
||||
// setup_complete_ helps us ensure an action is called immediately after boot
|
||||
if ((action == this->humidification_action_) && this->setup_complete_) {
|
||||
// already in target mode
|
||||
return;
|
||||
}
|
||||
|
||||
Trigger<> *trig = this->humidity_control_off_action_trigger_;
|
||||
switch (action) {
|
||||
case THERMOSTAT_HUMIDITY_CONTROL_ACTION_OFF:
|
||||
// trig = this->humidity_control_off_action_trigger_;
|
||||
ESP_LOGVV(TAG, "Switching to HUMIDIFICATION_OFF action");
|
||||
break;
|
||||
case THERMOSTAT_HUMIDITY_CONTROL_ACTION_DEHUMIDIFY:
|
||||
trig = this->humidity_control_dehumidify_action_trigger_;
|
||||
ESP_LOGVV(TAG, "Switching to DEHUMIDIFY action");
|
||||
break;
|
||||
case THERMOSTAT_HUMIDITY_CONTROL_ACTION_HUMIDIFY:
|
||||
trig = this->humidity_control_humidify_action_trigger_;
|
||||
ESP_LOGVV(TAG, "Switching to HUMIDIFY action");
|
||||
break;
|
||||
case THERMOSTAT_HUMIDITY_CONTROL_ACTION_NONE:
|
||||
default:
|
||||
action = THERMOSTAT_HUMIDITY_CONTROL_ACTION_OFF;
|
||||
// trig = this->humidity_control_off_action_trigger_;
|
||||
}
|
||||
|
||||
if (this->prev_humidity_control_trigger_ != nullptr) {
|
||||
this->prev_humidity_control_trigger_->stop_action();
|
||||
this->prev_humidity_control_trigger_ = nullptr;
|
||||
}
|
||||
this->humidification_action_ = action;
|
||||
this->prev_humidity_control_trigger_ = trig;
|
||||
if (trig != nullptr) {
|
||||
trig->trigger();
|
||||
}
|
||||
}
|
||||
|
||||
void ThermostatClimate::switch_to_fan_mode_(climate::ClimateFanMode fan_mode, bool publish_state) {
|
||||
// setup_complete_ helps us ensure an action is called immediately after boot
|
||||
if ((fan_mode == this->prev_fan_mode_) && this->setup_complete_) {
|
||||
@@ -887,6 +972,20 @@ void ThermostatClimate::idle_on_timer_callback_() {
|
||||
this->switch_to_supplemental_action_(this->compute_supplemental_action_());
|
||||
}
|
||||
|
||||
void ThermostatClimate::check_humidity_change_trigger_() {
|
||||
if ((this->prev_target_humidity_ == this->target_humidity) && this->setup_complete_) {
|
||||
return; // nothing changed, no reason to trigger
|
||||
} else {
|
||||
// save the new temperature so we can check it again later; the trigger will fire below
|
||||
this->prev_target_humidity_ = this->target_humidity;
|
||||
}
|
||||
// trigger the action
|
||||
Trigger<> *trig = this->humidity_change_trigger_;
|
||||
if (trig != nullptr) {
|
||||
trig->trigger();
|
||||
}
|
||||
}
|
||||
|
||||
void ThermostatClimate::check_temperature_change_trigger_() {
|
||||
if (this->supports_two_points_) {
|
||||
// setup_complete_ helps us ensure an action is called immediately after boot
|
||||
@@ -996,6 +1095,32 @@ bool ThermostatClimate::supplemental_heating_required_() {
|
||||
(this->supplemental_action_ == climate::CLIMATE_ACTION_HEATING));
|
||||
}
|
||||
|
||||
bool ThermostatClimate::dehumidification_required_() {
|
||||
if (this->current_humidity > this->target_humidity + this->humidity_hysteresis_) {
|
||||
// if the current humidity exceeds the target + hysteresis, dehumidification is required
|
||||
return true;
|
||||
} else if (this->current_humidity < this->target_humidity - this->humidity_hysteresis_) {
|
||||
// if the current humidity is less than the target - hysteresis, dehumidification should stop
|
||||
return false;
|
||||
}
|
||||
// if we get here, the current humidity is between target + hysteresis and target - hysteresis,
|
||||
// so the action should not change
|
||||
return this->humidification_action_ == THERMOSTAT_HUMIDITY_CONTROL_ACTION_DEHUMIDIFY;
|
||||
}
|
||||
|
||||
bool ThermostatClimate::humidification_required_() {
|
||||
if (this->current_humidity < this->target_humidity - this->humidity_hysteresis_) {
|
||||
// if the current humidity is below the target - hysteresis, humidification is required
|
||||
return true;
|
||||
} else if (this->current_humidity > this->target_humidity + this->humidity_hysteresis_) {
|
||||
// if the current humidity is above the target + hysteresis, humidification should stop
|
||||
return false;
|
||||
}
|
||||
// if we get here, the current humidity is between target - hysteresis and target + hysteresis,
|
||||
// so the action should not change
|
||||
return this->humidification_action_ == THERMOSTAT_HUMIDITY_CONTROL_ACTION_HUMIDIFY;
|
||||
}
|
||||
|
||||
void ThermostatClimate::dump_preset_config_(const char *preset_name, const ThermostatClimateTargetTempConfig &config) {
|
||||
if (this->supports_heat_) {
|
||||
ESP_LOGCONFIG(TAG, " Default Target Temperature Low: %.1f°C",
|
||||
@@ -1152,8 +1277,12 @@ ThermostatClimate::ThermostatClimate()
|
||||
swing_mode_off_trigger_(new Trigger<>()),
|
||||
swing_mode_horizontal_trigger_(new Trigger<>()),
|
||||
swing_mode_vertical_trigger_(new Trigger<>()),
|
||||
humidity_change_trigger_(new Trigger<>()),
|
||||
temperature_change_trigger_(new Trigger<>()),
|
||||
preset_change_trigger_(new Trigger<>()) {}
|
||||
preset_change_trigger_(new Trigger<>()),
|
||||
humidity_control_dehumidify_action_trigger_(new Trigger<>()),
|
||||
humidity_control_humidify_action_trigger_(new Trigger<>()),
|
||||
humidity_control_off_action_trigger_(new Trigger<>()) {}
|
||||
|
||||
void ThermostatClimate::set_default_preset(const std::string &custom_preset) {
|
||||
this->default_custom_preset_ = custom_preset;
|
||||
@@ -1217,6 +1346,9 @@ void ThermostatClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sen
|
||||
void ThermostatClimate::set_humidity_sensor(sensor::Sensor *humidity_sensor) {
|
||||
this->humidity_sensor_ = humidity_sensor;
|
||||
}
|
||||
void ThermostatClimate::set_humidity_hysteresis(float humidity_hysteresis) {
|
||||
this->humidity_hysteresis_ = std::clamp<float>(humidity_hysteresis, 0.0f, 100.0f);
|
||||
}
|
||||
void ThermostatClimate::set_use_startup_delay(bool use_startup_delay) { this->use_startup_delay_ = use_startup_delay; }
|
||||
void ThermostatClimate::set_supports_heat_cool(bool supports_heat_cool) {
|
||||
this->supports_heat_cool_ = supports_heat_cool;
|
||||
@@ -1284,6 +1416,18 @@ void ThermostatClimate::set_supports_swing_mode_vertical(bool supports_swing_mod
|
||||
void ThermostatClimate::set_supports_two_points(bool supports_two_points) {
|
||||
this->supports_two_points_ = supports_two_points;
|
||||
}
|
||||
void ThermostatClimate::set_supports_dehumidification(bool supports_dehumidification) {
|
||||
this->supports_dehumidification_ = supports_dehumidification;
|
||||
if (supports_dehumidification) {
|
||||
this->supports_humidification_ = false;
|
||||
}
|
||||
}
|
||||
void ThermostatClimate::set_supports_humidification(bool supports_humidification) {
|
||||
this->supports_humidification_ = supports_humidification;
|
||||
if (supports_humidification) {
|
||||
this->supports_dehumidification_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
Trigger<> *ThermostatClimate::get_cool_action_trigger() const { return this->cool_action_trigger_; }
|
||||
Trigger<> *ThermostatClimate::get_supplemental_cool_action_trigger() const {
|
||||
@@ -1317,8 +1461,18 @@ Trigger<> *ThermostatClimate::get_swing_mode_both_trigger() const { return this-
|
||||
Trigger<> *ThermostatClimate::get_swing_mode_off_trigger() const { return this->swing_mode_off_trigger_; }
|
||||
Trigger<> *ThermostatClimate::get_swing_mode_horizontal_trigger() const { return this->swing_mode_horizontal_trigger_; }
|
||||
Trigger<> *ThermostatClimate::get_swing_mode_vertical_trigger() const { return this->swing_mode_vertical_trigger_; }
|
||||
Trigger<> *ThermostatClimate::get_humidity_change_trigger() const { return this->humidity_change_trigger_; }
|
||||
Trigger<> *ThermostatClimate::get_temperature_change_trigger() const { return this->temperature_change_trigger_; }
|
||||
Trigger<> *ThermostatClimate::get_preset_change_trigger() const { return this->preset_change_trigger_; }
|
||||
Trigger<> *ThermostatClimate::get_humidity_control_dehumidify_action_trigger() const {
|
||||
return this->humidity_control_dehumidify_action_trigger_;
|
||||
}
|
||||
Trigger<> *ThermostatClimate::get_humidity_control_humidify_action_trigger() const {
|
||||
return this->humidity_control_humidify_action_trigger_;
|
||||
}
|
||||
Trigger<> *ThermostatClimate::get_humidity_control_off_action_trigger() const {
|
||||
return this->humidity_control_off_action_trigger_;
|
||||
}
|
||||
|
||||
void ThermostatClimate::dump_config() {
|
||||
LOG_CLIMATE("", "Thermostat", this);
|
||||
@@ -1422,7 +1576,12 @@ void ThermostatClimate::dump_config() {
|
||||
" OFF: %s\n"
|
||||
" HORIZONTAL: %s\n"
|
||||
" VERTICAL: %s\n"
|
||||
" Supports TWO SET POINTS: %s",
|
||||
" Supports TWO SET POINTS: %s\n"
|
||||
" Supported Humidity Parameters:\n"
|
||||
" CURRENT: %s\n"
|
||||
" TARGET: %s\n"
|
||||
" DEHUMIDIFICATION: %s\n"
|
||||
" HUMIDIFICATION: %s",
|
||||
YESNO(this->supports_fan_mode_on_), YESNO(this->supports_fan_mode_off_),
|
||||
YESNO(this->supports_fan_mode_auto_), YESNO(this->supports_fan_mode_low_),
|
||||
YESNO(this->supports_fan_mode_medium_), YESNO(this->supports_fan_mode_high_),
|
||||
@@ -1430,7 +1589,10 @@ void ThermostatClimate::dump_config() {
|
||||
YESNO(this->supports_fan_mode_diffuse_), YESNO(this->supports_fan_mode_quiet_),
|
||||
YESNO(this->supports_swing_mode_both_), YESNO(this->supports_swing_mode_off_),
|
||||
YESNO(this->supports_swing_mode_horizontal_), YESNO(this->supports_swing_mode_vertical_),
|
||||
YESNO(this->supports_two_points_));
|
||||
YESNO(this->supports_two_points_),
|
||||
YESNO(this->get_traits().has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)),
|
||||
YESNO(this->supports_dehumidification_ || this->supports_humidification_),
|
||||
YESNO(this->supports_dehumidification_), YESNO(this->supports_humidification_));
|
||||
|
||||
if (!this->preset_config_.empty()) {
|
||||
ESP_LOGCONFIG(TAG, " Supported PRESETS:");
|
||||
|
||||
@@ -13,6 +13,13 @@
|
||||
namespace esphome {
|
||||
namespace thermostat {
|
||||
|
||||
enum HumidificationAction : uint8_t {
|
||||
THERMOSTAT_HUMIDITY_CONTROL_ACTION_OFF = 0,
|
||||
THERMOSTAT_HUMIDITY_CONTROL_ACTION_DEHUMIDIFY = 1,
|
||||
THERMOSTAT_HUMIDITY_CONTROL_ACTION_HUMIDIFY = 2,
|
||||
THERMOSTAT_HUMIDITY_CONTROL_ACTION_NONE,
|
||||
};
|
||||
|
||||
enum ThermostatClimateTimerIndex : uint8_t {
|
||||
THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME = 0,
|
||||
THERMOSTAT_TIMER_COOLING_OFF = 1,
|
||||
@@ -90,6 +97,7 @@ class ThermostatClimate : public climate::Climate, public Component {
|
||||
void set_idle_minimum_time_in_sec(uint32_t time);
|
||||
void set_sensor(sensor::Sensor *sensor);
|
||||
void set_humidity_sensor(sensor::Sensor *humidity_sensor);
|
||||
void set_humidity_hysteresis(float humidity_hysteresis);
|
||||
void set_use_startup_delay(bool use_startup_delay);
|
||||
void set_supports_auto(bool supports_auto);
|
||||
void set_supports_heat_cool(bool supports_heat_cool);
|
||||
@@ -115,6 +123,8 @@ class ThermostatClimate : public climate::Climate, public Component {
|
||||
void set_supports_swing_mode_horizontal(bool supports_swing_mode_horizontal);
|
||||
void set_supports_swing_mode_off(bool supports_swing_mode_off);
|
||||
void set_supports_swing_mode_vertical(bool supports_swing_mode_vertical);
|
||||
void set_supports_dehumidification(bool supports_dehumidification);
|
||||
void set_supports_humidification(bool supports_humidification);
|
||||
void set_supports_two_points(bool supports_two_points);
|
||||
|
||||
void set_preset_config(climate::ClimatePreset preset, const ThermostatClimateTargetTempConfig &config);
|
||||
@@ -148,8 +158,12 @@ class ThermostatClimate : public climate::Climate, public Component {
|
||||
Trigger<> *get_swing_mode_horizontal_trigger() const;
|
||||
Trigger<> *get_swing_mode_off_trigger() const;
|
||||
Trigger<> *get_swing_mode_vertical_trigger() const;
|
||||
Trigger<> *get_humidity_change_trigger() const;
|
||||
Trigger<> *get_temperature_change_trigger() const;
|
||||
Trigger<> *get_preset_change_trigger() const;
|
||||
Trigger<> *get_humidity_control_dehumidify_action_trigger() const;
|
||||
Trigger<> *get_humidity_control_humidify_action_trigger() const;
|
||||
Trigger<> *get_humidity_control_off_action_trigger() const;
|
||||
/// Get current hysteresis values
|
||||
float cool_deadband();
|
||||
float cool_overrun();
|
||||
@@ -166,11 +180,13 @@ class ThermostatClimate : public climate::Climate, public Component {
|
||||
climate::ClimateFanMode locked_fan_mode();
|
||||
/// Set point and hysteresis validation
|
||||
bool hysteresis_valid(); // returns true if valid
|
||||
bool humidity_hysteresis_valid(); // returns true if valid
|
||||
bool limit_setpoints_for_heat_cool(); // returns true if set points should be further limited within visual range
|
||||
void validate_target_temperature();
|
||||
void validate_target_temperatures(bool pin_target_temperature_high);
|
||||
void validate_target_temperature_low();
|
||||
void validate_target_temperature_high();
|
||||
void validate_target_humidity();
|
||||
|
||||
protected:
|
||||
/// Override control to change settings of the climate device.
|
||||
@@ -192,11 +208,13 @@ class ThermostatClimate : public climate::Climate, public Component {
|
||||
/// Re-compute the required action of this climate controller.
|
||||
climate::ClimateAction compute_action_(bool ignore_timers = false);
|
||||
climate::ClimateAction compute_supplemental_action_();
|
||||
HumidificationAction compute_humidity_control_action_();
|
||||
|
||||
/// Switch the climate device to the given climate action.
|
||||
void switch_to_action_(climate::ClimateAction action, bool publish_state = true);
|
||||
void switch_to_supplemental_action_(climate::ClimateAction action);
|
||||
void trigger_supplemental_action_();
|
||||
void switch_to_humidity_control_action_(HumidificationAction action);
|
||||
|
||||
/// Switch the climate device to the given climate fan mode.
|
||||
void switch_to_fan_mode_(climate::ClimateFanMode fan_mode, bool publish_state = true);
|
||||
@@ -207,6 +225,9 @@ class ThermostatClimate : public climate::Climate, public Component {
|
||||
/// Switch the climate device to the given climate swing mode.
|
||||
void switch_to_swing_mode_(climate::ClimateSwingMode swing_mode, bool publish_state = true);
|
||||
|
||||
/// Check if the humidity change trigger should be called.
|
||||
void check_humidity_change_trigger_();
|
||||
|
||||
/// Check if the temperature change trigger should be called.
|
||||
void check_temperature_change_trigger_();
|
||||
|
||||
@@ -243,6 +264,8 @@ class ThermostatClimate : public climate::Climate, public Component {
|
||||
bool heating_required_();
|
||||
bool supplemental_cooling_required_();
|
||||
bool supplemental_heating_required_();
|
||||
bool dehumidification_required_();
|
||||
bool humidification_required_();
|
||||
|
||||
void dump_preset_config_(const char *preset_name, const ThermostatClimateTargetTempConfig &config);
|
||||
|
||||
@@ -259,6 +282,9 @@ class ThermostatClimate : public climate::Climate, public Component {
|
||||
/// The current supplemental action
|
||||
climate::ClimateAction supplemental_action_{climate::CLIMATE_ACTION_OFF};
|
||||
|
||||
/// The current humidification action
|
||||
HumidificationAction humidification_action_{THERMOSTAT_HUMIDITY_CONTROL_ACTION_NONE};
|
||||
|
||||
/// Default standard preset to use on start up
|
||||
climate::ClimatePreset default_preset_{};
|
||||
|
||||
@@ -321,6 +347,12 @@ class ThermostatClimate : public climate::Climate, public Component {
|
||||
/// A false value means that the controller has no such support.
|
||||
bool supports_two_points_{false};
|
||||
|
||||
/// Whether the controller supports dehumidification and/or humidification
|
||||
///
|
||||
/// A false value means that the controller has no such support.
|
||||
bool supports_dehumidification_{false};
|
||||
bool supports_humidification_{false};
|
||||
|
||||
/// Flags indicating if maximum allowable run time was exceeded
|
||||
bool cooling_max_runtime_exceeded_{false};
|
||||
bool heating_max_runtime_exceeded_{false};
|
||||
@@ -331,9 +363,10 @@ class ThermostatClimate : public climate::Climate, public Component {
|
||||
/// setup_complete_ blocks modifying/resetting the temps immediately after boot
|
||||
bool setup_complete_{false};
|
||||
|
||||
/// Store previously-known temperatures
|
||||
/// Store previously-known humidity and temperatures
|
||||
///
|
||||
/// These are used to determine when the temperature change trigger/action needs to be called
|
||||
/// These are used to determine when a temperature/humidity has changed
|
||||
float prev_target_humidity_{NAN};
|
||||
float prev_target_temperature_{NAN};
|
||||
float prev_target_temperature_low_{NAN};
|
||||
float prev_target_temperature_high_{NAN};
|
||||
@@ -347,6 +380,9 @@ class ThermostatClimate : public climate::Climate, public Component {
|
||||
float heating_deadband_{0};
|
||||
float heating_overrun_{0};
|
||||
|
||||
/// Hysteresis values used for computing humidification action
|
||||
float humidity_hysteresis_{0};
|
||||
|
||||
/// Maximum allowable temperature deltas before engaging supplemental cooling/heating actions
|
||||
float supplemental_cool_delta_{0};
|
||||
float supplemental_heat_delta_{0};
|
||||
@@ -448,12 +484,24 @@ class ThermostatClimate : public climate::Climate, public Component {
|
||||
/// The trigger to call when the controller should switch the swing mode to "vertical".
|
||||
Trigger<> *swing_mode_vertical_trigger_{nullptr};
|
||||
|
||||
/// The trigger to call when the target humidity changes.
|
||||
Trigger<> *humidity_change_trigger_{nullptr};
|
||||
|
||||
/// The trigger to call when the target temperature(s) change(es).
|
||||
Trigger<> *temperature_change_trigger_{nullptr};
|
||||
|
||||
/// The trigger to call when the preset mode changes
|
||||
Trigger<> *preset_change_trigger_{nullptr};
|
||||
|
||||
/// The trigger to call when dehumidification is required
|
||||
Trigger<> *humidity_control_dehumidify_action_trigger_{nullptr};
|
||||
|
||||
/// The trigger to call when humidification is required
|
||||
Trigger<> *humidity_control_humidify_action_trigger_{nullptr};
|
||||
|
||||
/// The trigger to call when (de)humidification should stop
|
||||
Trigger<> *humidity_control_off_action_trigger_{nullptr};
|
||||
|
||||
/// A reference to the trigger that was previously active.
|
||||
///
|
||||
/// This is so that the previous trigger can be stopped before enabling a new one
|
||||
@@ -462,6 +510,7 @@ class ThermostatClimate : public climate::Climate, public Component {
|
||||
Trigger<> *prev_fan_mode_trigger_{nullptr};
|
||||
Trigger<> *prev_mode_trigger_{nullptr};
|
||||
Trigger<> *prev_swing_mode_trigger_{nullptr};
|
||||
Trigger<> *prev_humidity_control_trigger_{nullptr};
|
||||
|
||||
/// Default custom preset to use on start up
|
||||
std::string default_custom_preset_{};
|
||||
|
||||
@@ -27,6 +27,14 @@ class RealTimeClock : public PollingComponent {
|
||||
this->apply_timezone_();
|
||||
}
|
||||
|
||||
/// Set the time zone from raw buffer, only if it differs from the current one.
|
||||
void set_timezone(const char *tz, size_t len) {
|
||||
if (this->timezone_.length() != len || memcmp(this->timezone_.c_str(), tz, len) != 0) {
|
||||
this->timezone_.assign(tz, len);
|
||||
this->apply_timezone_();
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the time zone currently in use.
|
||||
std::string get_timezone() { return this->timezone_; }
|
||||
#endif
|
||||
|
||||
@@ -283,8 +283,11 @@ void TuyaClimate::control_fan_mode_(const climate::ClimateCall &call) {
|
||||
|
||||
climate::ClimateTraits TuyaClimate::traits() {
|
||||
auto traits = climate::ClimateTraits();
|
||||
traits.set_supports_action(true);
|
||||
traits.set_supports_current_temperature(this->current_temperature_id_.has_value());
|
||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_ACTION);
|
||||
if (this->current_temperature_id_.has_value()) {
|
||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
|
||||
}
|
||||
|
||||
if (supports_heat_)
|
||||
traits.add_supported_mode(climate::CLIMATE_MODE_HEAT);
|
||||
if (supports_cool_)
|
||||
|
||||
@@ -17,6 +17,12 @@ UponorSmatrixDevice = uponor_smatrix_ns.class_(
|
||||
"UponorSmatrixDevice", cg.Parented.template(UponorSmatrixComponent)
|
||||
)
|
||||
|
||||
|
||||
device_address = cv.All(
|
||||
cv.hex_int,
|
||||
cv.Range(min=0x1000000, max=0xFFFFFFFF, msg="Expected a 32 bit device address"),
|
||||
)
|
||||
|
||||
CONF_UPONOR_SMATRIX_ID = "uponor_smatrix_id"
|
||||
CONF_TIME_DEVICE_ADDRESS = "time_device_address"
|
||||
|
||||
@@ -24,9 +30,12 @@ CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(UponorSmatrixComponent),
|
||||
cv.Optional(CONF_ADDRESS): cv.hex_uint16_t,
|
||||
cv.Optional(CONF_ADDRESS): cv.invalid(
|
||||
f"The '{CONF_ADDRESS}' option has been removed. "
|
||||
"Use full 32 bit addresses in the device definitions instead."
|
||||
),
|
||||
cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
|
||||
cv.Optional(CONF_TIME_DEVICE_ADDRESS): cv.hex_uint16_t,
|
||||
cv.Optional(CONF_TIME_DEVICE_ADDRESS): device_address,
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
@@ -47,7 +56,7 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
UPONOR_SMATRIX_DEVICE_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_UPONOR_SMATRIX_ID): cv.use_id(UponorSmatrixComponent),
|
||||
cv.Required(CONF_ADDRESS): cv.hex_uint16_t,
|
||||
cv.Required(CONF_ADDRESS): device_address,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -58,17 +67,15 @@ async def to_code(config):
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
|
||||
if address := config.get(CONF_ADDRESS):
|
||||
cg.add(var.set_system_address(address))
|
||||
if time_id := config.get(CONF_TIME_ID):
|
||||
time_ = await cg.get_variable(time_id)
|
||||
cg.add(var.set_time_id(time_))
|
||||
if time_device_address := config.get(CONF_TIME_DEVICE_ADDRESS):
|
||||
cg.add(var.set_time_device_address(time_device_address))
|
||||
if time_device_address := config.get(CONF_TIME_DEVICE_ADDRESS):
|
||||
cg.add(var.set_time_device_address(time_device_address))
|
||||
|
||||
|
||||
async def register_uponor_smatrix_device(var, config):
|
||||
parent = await cg.get_variable(config[CONF_UPONOR_SMATRIX_ID])
|
||||
cg.add(var.set_parent(parent))
|
||||
cg.add(var.set_device_address(config[CONF_ADDRESS]))
|
||||
cg.add(var.set_address(config[CONF_ADDRESS]))
|
||||
cg.add(parent.register_device(var))
|
||||
|
||||
@@ -10,7 +10,7 @@ static const char *const TAG = "uponor_smatrix.climate";
|
||||
|
||||
void UponorSmatrixClimate::dump_config() {
|
||||
LOG_CLIMATE("", "Uponor Smatrix Climate", this);
|
||||
ESP_LOGCONFIG(TAG, " Device address: 0x%04X", this->address_);
|
||||
ESP_LOGCONFIG(TAG, " Device address: 0x%08X", this->address_);
|
||||
}
|
||||
|
||||
void UponorSmatrixClimate::loop() {
|
||||
@@ -30,10 +30,9 @@ void UponorSmatrixClimate::loop() {
|
||||
|
||||
climate::ClimateTraits UponorSmatrixClimate::traits() {
|
||||
auto traits = climate::ClimateTraits();
|
||||
traits.set_supports_current_temperature(true);
|
||||
traits.set_supports_current_humidity(true);
|
||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE | climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY |
|
||||
climate::CLIMATE_SUPPORTS_ACTION);
|
||||
traits.set_supported_modes({climate::CLIMATE_MODE_HEAT});
|
||||
traits.set_supports_action(true);
|
||||
traits.set_supported_presets({climate::CLIMATE_PRESET_ECO});
|
||||
traits.set_visual_min_temperature(this->min_temperature_);
|
||||
traits.set_visual_max_temperature(this->max_temperature_);
|
||||
|
||||
@@ -9,7 +9,7 @@ static const char *const TAG = "uponor_smatrix.sensor";
|
||||
void UponorSmatrixSensor::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Uponor Smatrix Sensor\n"
|
||||
" Device address: 0x%04X",
|
||||
" Device address: 0x%08X",
|
||||
this->address_);
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||
LOG_SENSOR(" ", "External Temperature", this->external_temperature_sensor_);
|
||||
|
||||
@@ -18,11 +18,10 @@ void UponorSmatrixComponent::setup() {
|
||||
|
||||
void UponorSmatrixComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Uponor Smatrix");
|
||||
ESP_LOGCONFIG(TAG, " System address: 0x%04X", this->address_);
|
||||
#ifdef USE_TIME
|
||||
if (this->time_id_ != nullptr) {
|
||||
ESP_LOGCONFIG(TAG, " Time synchronization: YES");
|
||||
ESP_LOGCONFIG(TAG, " Time master device address: 0x%04X", this->time_device_address_);
|
||||
ESP_LOGCONFIG(TAG, " Time master device address: 0x%08X", this->time_device_address_);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -31,7 +30,7 @@ void UponorSmatrixComponent::dump_config() {
|
||||
if (!this->unknown_devices_.empty()) {
|
||||
ESP_LOGCONFIG(TAG, " Detected unknown device addresses:");
|
||||
for (auto device_address : this->unknown_devices_) {
|
||||
ESP_LOGCONFIG(TAG, " 0x%04X", device_address);
|
||||
ESP_LOGCONFIG(TAG, " 0x%08X", device_address);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,8 +88,7 @@ bool UponorSmatrixComponent::parse_byte_(uint8_t byte) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t system_address = encode_uint16(packet[0], packet[1]);
|
||||
uint16_t device_address = encode_uint16(packet[2], packet[3]);
|
||||
uint32_t device_address = encode_uint32(packet[0], packet[1], packet[2], packet[3]);
|
||||
uint16_t crc = encode_uint16(packet[packet_len - 1], packet[packet_len - 2]);
|
||||
|
||||
uint16_t computed_crc = crc16(packet, packet_len - 2);
|
||||
@@ -99,24 +97,14 @@ bool UponorSmatrixComponent::parse_byte_(uint8_t byte) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "Received packet: sys=%04X, dev=%04X, data=%s, crc=%04X", system_address, device_address,
|
||||
ESP_LOGV(TAG, "Received packet: addr=%08X, data=%s, crc=%04X", device_address,
|
||||
format_hex(&packet[4], packet_len - 6).c_str(), crc);
|
||||
|
||||
// Detect or check system address
|
||||
if (this->address_ == 0) {
|
||||
ESP_LOGI(TAG, "Using detected system address 0x%04X", system_address);
|
||||
this->address_ = system_address;
|
||||
} else if (this->address_ != system_address) {
|
||||
// This should never happen except if the system address was set or detected incorrectly, so warn the user.
|
||||
ESP_LOGW(TAG, "Received packet from unknown system address 0x%04X", system_address);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Handle packet
|
||||
size_t data_len = (packet_len - 6) / 3;
|
||||
if (data_len == 0) {
|
||||
if (packet[4] == UPONOR_ID_REQUEST)
|
||||
ESP_LOGVV(TAG, "Ignoring request packet for device 0x%04X", device_address);
|
||||
ESP_LOGVV(TAG, "Ignoring request packet for device 0x%08X", device_address);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -141,7 +129,7 @@ bool UponorSmatrixComponent::parse_byte_(uint8_t byte) {
|
||||
if (data[i].id == UPONOR_ID_DATETIME1)
|
||||
found_time = true;
|
||||
if (found_temperature && found_time) {
|
||||
ESP_LOGI(TAG, "Using detected time device address 0x%04X", device_address);
|
||||
ESP_LOGI(TAG, "Using detected time device address 0x%08X", device_address);
|
||||
this->time_device_address_ = device_address;
|
||||
break;
|
||||
}
|
||||
@@ -160,7 +148,7 @@ bool UponorSmatrixComponent::parse_byte_(uint8_t byte) {
|
||||
|
||||
// Log unknown device addresses
|
||||
if (!found && !this->unknown_devices_.count(device_address)) {
|
||||
ESP_LOGI(TAG, "Received packet for unknown device address 0x%04X ", device_address);
|
||||
ESP_LOGI(TAG, "Received packet for unknown device address 0x%08X ", device_address);
|
||||
this->unknown_devices_.insert(device_address);
|
||||
}
|
||||
|
||||
@@ -168,16 +156,16 @@ bool UponorSmatrixComponent::parse_byte_(uint8_t byte) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UponorSmatrixComponent::send(uint16_t device_address, const UponorSmatrixData *data, size_t data_len) {
|
||||
if (this->address_ == 0 || device_address == 0 || data == nullptr || data_len == 0)
|
||||
bool UponorSmatrixComponent::send(uint32_t device_address, const UponorSmatrixData *data, size_t data_len) {
|
||||
if (device_address == 0 || data == nullptr || data_len == 0)
|
||||
return false;
|
||||
|
||||
// Assemble packet for send queue. All fields are big-endian except for the little-endian checksum.
|
||||
std::vector<uint8_t> packet;
|
||||
packet.reserve(6 + 3 * data_len);
|
||||
|
||||
packet.push_back(this->address_ >> 8);
|
||||
packet.push_back(this->address_ >> 0);
|
||||
packet.push_back(device_address >> 24);
|
||||
packet.push_back(device_address >> 16);
|
||||
packet.push_back(device_address >> 8);
|
||||
packet.push_back(device_address >> 0);
|
||||
|
||||
|
||||
@@ -71,23 +71,21 @@ class UponorSmatrixComponent : public uart::UARTDevice, public Component {
|
||||
void dump_config() override;
|
||||
void loop() override;
|
||||
|
||||
void set_system_address(uint16_t address) { this->address_ = address; }
|
||||
void register_device(UponorSmatrixDevice *device) { this->devices_.push_back(device); }
|
||||
|
||||
bool send(uint16_t device_address, const UponorSmatrixData *data, size_t data_len);
|
||||
bool send(uint32_t device_address, const UponorSmatrixData *data, size_t data_len);
|
||||
|
||||
#ifdef USE_TIME
|
||||
void set_time_id(time::RealTimeClock *time_id) { this->time_id_ = time_id; }
|
||||
void set_time_device_address(uint16_t address) { this->time_device_address_ = address; }
|
||||
void set_time_device_address(uint32_t address) { this->time_device_address_ = address; }
|
||||
void send_time() { this->send_time_requested_ = true; }
|
||||
#endif
|
||||
|
||||
protected:
|
||||
bool parse_byte_(uint8_t byte);
|
||||
|
||||
uint16_t address_;
|
||||
std::vector<UponorSmatrixDevice *> devices_;
|
||||
std::set<uint16_t> unknown_devices_;
|
||||
std::set<uint32_t> unknown_devices_;
|
||||
|
||||
std::vector<uint8_t> rx_buffer_;
|
||||
std::queue<std::vector<uint8_t>> tx_queue_;
|
||||
@@ -96,7 +94,7 @@ class UponorSmatrixComponent : public uart::UARTDevice, public Component {
|
||||
|
||||
#ifdef USE_TIME
|
||||
time::RealTimeClock *time_id_{nullptr};
|
||||
uint16_t time_device_address_;
|
||||
uint32_t time_device_address_;
|
||||
bool send_time_requested_;
|
||||
bool do_send_time_();
|
||||
#endif
|
||||
@@ -104,7 +102,7 @@ class UponorSmatrixComponent : public uart::UARTDevice, public Component {
|
||||
|
||||
class UponorSmatrixDevice : public Parented<UponorSmatrixComponent> {
|
||||
public:
|
||||
void set_device_address(uint16_t address) { this->address_ = address; }
|
||||
void set_address(uint32_t address) { this->address_ = address; }
|
||||
|
||||
virtual void on_device_data(const UponorSmatrixData *data, size_t data_len) = 0;
|
||||
bool send(const UponorSmatrixData *data, size_t data_len) {
|
||||
@@ -113,7 +111,7 @@ class UponorSmatrixDevice : public Parented<UponorSmatrixComponent> {
|
||||
|
||||
protected:
|
||||
friend UponorSmatrixComponent;
|
||||
uint16_t address_;
|
||||
uint32_t address_;
|
||||
};
|
||||
|
||||
inline float raw_to_celsius(uint16_t raw) {
|
||||
|
||||
@@ -136,6 +136,18 @@ def _final_validate_sorting(config: ConfigType) -> ConfigType:
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate_sorting
|
||||
|
||||
|
||||
def _consume_web_server_sockets(config: ConfigType) -> ConfigType:
|
||||
"""Register socket needs for web_server component."""
|
||||
from esphome.components import socket
|
||||
|
||||
# Web server needs 1 listening socket + typically 2 concurrent client connections
|
||||
# (browser makes 2 connections for page + event stream)
|
||||
sockets_needed = 3
|
||||
socket.consume_sockets(sockets_needed, "web_server")(config)
|
||||
return config
|
||||
|
||||
|
||||
sorting_group = {
|
||||
cv.Required(CONF_ID): cv.declare_id(cg.int_),
|
||||
cv.Required(CONF_NAME): cv.string,
|
||||
@@ -205,6 +217,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
validate_local,
|
||||
validate_sorting_groups,
|
||||
validate_ota,
|
||||
_consume_web_server_sockets,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1325,7 +1325,7 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf
|
||||
root["max_temp"] = value_accuracy_to_string(traits.get_visual_max_temperature(), target_accuracy);
|
||||
root["min_temp"] = value_accuracy_to_string(traits.get_visual_min_temperature(), target_accuracy);
|
||||
root["step"] = traits.get_visual_target_temperature_step();
|
||||
if (traits.get_supports_action()) {
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) {
|
||||
root["action"] = PSTR_LOCAL(climate_action_to_string(obj->action));
|
||||
root["state"] = root["action"];
|
||||
has_state = true;
|
||||
@@ -1345,14 +1345,15 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf
|
||||
if (traits.get_supports_swing_modes()) {
|
||||
root["swing_mode"] = PSTR_LOCAL(climate_swing_mode_to_string(obj->swing_mode));
|
||||
}
|
||||
if (traits.get_supports_current_temperature()) {
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) {
|
||||
if (!std::isnan(obj->current_temperature)) {
|
||||
root["current_temperature"] = value_accuracy_to_string(obj->current_temperature, current_accuracy);
|
||||
} else {
|
||||
root["current_temperature"] = "NA";
|
||||
}
|
||||
}
|
||||
if (traits.get_supports_two_point_target_temperature()) {
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
|
||||
climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
|
||||
root["target_temperature_low"] = value_accuracy_to_string(obj->target_temperature_low, target_accuracy);
|
||||
root["target_temperature_high"] = value_accuracy_to_string(obj->target_temperature_high, target_accuracy);
|
||||
if (!has_state) {
|
||||
|
||||
@@ -407,7 +407,8 @@ async def to_code(config):
|
||||
|
||||
cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
|
||||
cg.add(var.set_power_save_mode(config[CONF_POWER_SAVE_MODE]))
|
||||
cg.add(var.set_fast_connect(config[CONF_FAST_CONNECT]))
|
||||
if config[CONF_FAST_CONNECT]:
|
||||
cg.add_define("USE_WIFI_FAST_CONNECT")
|
||||
cg.add(var.set_passive_scan(config[CONF_PASSIVE_SCAN]))
|
||||
if CONF_OUTPUT_POWER in config:
|
||||
cg.add(var.set_output_power(config[CONF_OUTPUT_POWER]))
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user