mirror of
https://github.com/esphome/esphome.git
synced 2025-08-05 18:07:47 +00:00
Merge branch 'reduce_api_size' into integration
This commit is contained in:
commit
d8545ef946
1
.clang-tidy.hash
Normal file
1
.clang-tidy.hash
Normal file
@ -0,0 +1 @@
|
|||||||
|
a3cdfc378d28b53b416a1d5bf0ab9077ee18867f0d39436ea8013cf5a4ead87a
|
2
.github/actions/restore-python/action.yml
vendored
2
.github/actions/restore-python/action.yml
vendored
@ -41,7 +41,7 @@ runs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
python -m venv venv
|
python -m venv venv
|
||||||
./venv/Scripts/activate
|
source ./venv/Scripts/activate
|
||||||
python --version
|
python --version
|
||||||
pip install -r requirements.txt -r requirements_test.txt
|
pip install -r requirements.txt -r requirements_test.txt
|
||||||
pip install -e .
|
pip install -e .
|
||||||
|
76
.github/workflows/ci-clang-tidy-hash.yml
vendored
Normal file
76
.github/workflows/ci-clang-tidy-hash.yml
vendored
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
name: Clang-tidy Hash CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- ".clang-tidy"
|
||||||
|
- "platformio.ini"
|
||||||
|
- "requirements_dev.txt"
|
||||||
|
- ".clang-tidy.hash"
|
||||||
|
- "script/clang_tidy_hash.py"
|
||||||
|
- ".github/workflows/ci-clang-tidy-hash.yml"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
verify-hash:
|
||||||
|
name: Verify clang-tidy hash
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5.6.0
|
||||||
|
with:
|
||||||
|
python-version: "3.11"
|
||||||
|
|
||||||
|
- name: Verify hash
|
||||||
|
run: |
|
||||||
|
python script/clang_tidy_hash.py --verify
|
||||||
|
|
||||||
|
- if: failure()
|
||||||
|
name: Show hash details
|
||||||
|
run: |
|
||||||
|
python script/clang_tidy_hash.py
|
||||||
|
echo "## Job Failed" | tee -a $GITHUB_STEP_SUMMARY
|
||||||
|
echo "You have modified clang-tidy configuration but have not updated the hash." | tee -a $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Please run 'script/clang_tidy_hash.py --update' and commit the changes." | tee -a $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
- if: failure()
|
||||||
|
name: Request changes
|
||||||
|
uses: actions/github-script@v7.0.1
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
await github.rest.pulls.createReview({
|
||||||
|
pull_number: context.issue.number,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
event: 'REQUEST_CHANGES',
|
||||||
|
body: 'You have modified clang-tidy configuration but have not updated the hash.\nPlease run `script/clang_tidy_hash.py --update` and commit the changes.'
|
||||||
|
})
|
||||||
|
|
||||||
|
- if: success()
|
||||||
|
name: Dismiss review
|
||||||
|
uses: actions/github-script@v7.0.1
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
let reviews = await github.rest.pulls.listReviews({
|
||||||
|
pull_number: context.issue.number,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo
|
||||||
|
});
|
||||||
|
for (let review of reviews.data) {
|
||||||
|
if (review.user.login === 'github-actions[bot]' && review.state === 'CHANGES_REQUESTED') {
|
||||||
|
await github.rest.pulls.dismissReview({
|
||||||
|
pull_number: context.issue.number,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
review_id: review.id,
|
||||||
|
message: 'Clang-tidy hash now matches configuration.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
192
.github/workflows/ci.yml
vendored
192
.github/workflows/ci.yml
vendored
@ -66,6 +66,8 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
needs:
|
needs:
|
||||||
- common
|
- common
|
||||||
|
- determine-jobs
|
||||||
|
if: needs.determine-jobs.outputs.python-linters == 'true'
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
@ -87,6 +89,8 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
needs:
|
needs:
|
||||||
- common
|
- common
|
||||||
|
- determine-jobs
|
||||||
|
if: needs.determine-jobs.outputs.python-linters == 'true'
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
@ -108,6 +112,8 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
needs:
|
needs:
|
||||||
- common
|
- common
|
||||||
|
- determine-jobs
|
||||||
|
if: needs.determine-jobs.outputs.python-linters == 'true'
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
@ -129,6 +135,8 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
needs:
|
needs:
|
||||||
- common
|
- common
|
||||||
|
- determine-jobs
|
||||||
|
if: needs.determine-jobs.outputs.python-linters == 'true'
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
@ -204,6 +212,7 @@ jobs:
|
|||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Restore Python
|
- name: Restore Python
|
||||||
|
id: restore-python
|
||||||
uses: ./.github/actions/restore-python
|
uses: ./.github/actions/restore-python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
@ -213,23 +222,108 @@ jobs:
|
|||||||
- name: Run pytest
|
- name: Run pytest
|
||||||
if: matrix.os == 'windows-latest'
|
if: matrix.os == 'windows-latest'
|
||||||
run: |
|
run: |
|
||||||
./venv/Scripts/activate
|
. ./venv/Scripts/activate.ps1
|
||||||
pytest -vv --cov-report=xml --tb=native -n auto tests
|
pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/
|
||||||
- name: Run pytest
|
- name: Run pytest
|
||||||
if: matrix.os == 'ubuntu-latest' || matrix.os == 'macOS-latest'
|
if: matrix.os == 'ubuntu-latest' || matrix.os == 'macOS-latest'
|
||||||
run: |
|
run: |
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
pytest -vv --cov-report=xml --tb=native -n auto tests
|
pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
uses: codecov/codecov-action@v5.4.3
|
uses: codecov/codecov-action@v5.4.3
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
- name: Save Python virtual environment cache
|
||||||
|
if: github.ref == 'refs/heads/dev'
|
||||||
|
uses: actions/cache/save@v4.2.3
|
||||||
|
with:
|
||||||
|
path: venv
|
||||||
|
key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
|
||||||
|
|
||||||
|
determine-jobs:
|
||||||
|
name: Determine which jobs to run
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
needs:
|
||||||
|
- common
|
||||||
|
outputs:
|
||||||
|
integration-tests: ${{ steps.determine.outputs.integration-tests }}
|
||||||
|
clang-tidy: ${{ steps.determine.outputs.clang-tidy }}
|
||||||
|
clang-format: ${{ steps.determine.outputs.clang-format }}
|
||||||
|
python-linters: ${{ steps.determine.outputs.python-linters }}
|
||||||
|
changed-components: ${{ steps.determine.outputs.changed-components }}
|
||||||
|
component-test-count: ${{ steps.determine.outputs.component-test-count }}
|
||||||
|
steps:
|
||||||
|
- name: Check out code from GitHub
|
||||||
|
uses: actions/checkout@v4.2.2
|
||||||
|
with:
|
||||||
|
# Fetch enough history to find the merge base
|
||||||
|
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: Determine which tests to run
|
||||||
|
id: determine
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
output=$(python script/determine-jobs.py)
|
||||||
|
echo "Test determination output:"
|
||||||
|
echo "$output" | jq
|
||||||
|
|
||||||
|
# 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-format=$(echo "$output" | jq -r '.clang_format')" >> $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 "component-test-count=$(echo "$output" | jq -r '.component_test_count')" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
integration-tests:
|
||||||
|
name: Run integration tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- common
|
||||||
|
- determine-jobs
|
||||||
|
if: needs.determine-jobs.outputs.integration-tests == 'true'
|
||||||
|
steps:
|
||||||
|
- name: Check out code from GitHub
|
||||||
|
uses: actions/checkout@v4.2.2
|
||||||
|
- name: Set up Python 3.13
|
||||||
|
id: python
|
||||||
|
uses: actions/setup-python@v5.6.0
|
||||||
|
with:
|
||||||
|
python-version: "3.13"
|
||||||
|
- name: Restore Python virtual environment
|
||||||
|
id: cache-venv
|
||||||
|
uses: actions/cache@v4.2.3
|
||||||
|
with:
|
||||||
|
path: venv
|
||||||
|
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
|
||||||
|
- name: Create Python virtual environment
|
||||||
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
python -m venv venv
|
||||||
|
. venv/bin/activate
|
||||||
|
python --version
|
||||||
|
pip install -r requirements.txt -r requirements_test.txt
|
||||||
|
pip install -e .
|
||||||
|
- name: Register matcher
|
||||||
|
run: echo "::add-matcher::.github/workflows/matchers/pytest.json"
|
||||||
|
- name: Run integration tests
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
pytest -vv --no-cov --tb=native -n auto tests/integration/
|
||||||
|
|
||||||
clang-format:
|
clang-format:
|
||||||
name: Check clang-format
|
name: Check clang-format
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
needs:
|
needs:
|
||||||
- common
|
- common
|
||||||
|
- determine-jobs
|
||||||
|
if: needs.determine-jobs.outputs.clang-format == 'true'
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
@ -263,6 +357,10 @@ jobs:
|
|||||||
- pylint
|
- pylint
|
||||||
- pytest
|
- pytest
|
||||||
- pyupgrade
|
- pyupgrade
|
||||||
|
- determine-jobs
|
||||||
|
if: needs.determine-jobs.outputs.clang-tidy == 'true'
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
max-parallel: 2
|
max-parallel: 2
|
||||||
@ -301,6 +399,10 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
|
with:
|
||||||
|
# Need history for HEAD~1 to work for checking changed files
|
||||||
|
fetch-depth: 2
|
||||||
|
|
||||||
- name: Restore Python
|
- name: Restore Python
|
||||||
uses: ./.github/actions/restore-python
|
uses: ./.github/actions/restore-python
|
||||||
with:
|
with:
|
||||||
@ -312,14 +414,14 @@ jobs:
|
|||||||
uses: actions/cache@v4.2.3
|
uses: actions/cache@v4.2.3
|
||||||
with:
|
with:
|
||||||
path: ~/.platformio
|
path: ~/.platformio
|
||||||
key: platformio-${{ matrix.pio_cache_key }}
|
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
||||||
|
|
||||||
- name: Cache platformio
|
- name: Cache platformio
|
||||||
if: github.ref != 'refs/heads/dev'
|
if: github.ref != 'refs/heads/dev'
|
||||||
uses: actions/cache/restore@v4.2.3
|
uses: actions/cache/restore@v4.2.3
|
||||||
with:
|
with:
|
||||||
path: ~/.platformio
|
path: ~/.platformio
|
||||||
key: platformio-${{ matrix.pio_cache_key }}
|
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
||||||
|
|
||||||
- name: Register problem matchers
|
- name: Register problem matchers
|
||||||
run: |
|
run: |
|
||||||
@ -333,10 +435,28 @@ jobs:
|
|||||||
mkdir -p .temp
|
mkdir -p .temp
|
||||||
pio run --list-targets -e esp32-idf-tidy
|
pio run --list-targets -e esp32-idf-tidy
|
||||||
|
|
||||||
|
- 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
|
- name: Run clang-tidy
|
||||||
run: |
|
run: |
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
script/clang-tidy --all-headers --fix ${{ matrix.options }} ${{ matrix.ignore_errors && '|| true' || '' }}
|
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 }} ${{ matrix.ignore_errors && '|| true' || '' }}
|
||||||
|
else
|
||||||
|
echo "Running clang-tidy on changed files only"
|
||||||
|
script/clang-tidy --all-headers --fix --changed ${{ matrix.options }} ${{ matrix.ignore_errors && '|| true' || '' }}
|
||||||
|
fi
|
||||||
env:
|
env:
|
||||||
# Also cache libdeps, store them in a ~/.platformio subfolder
|
# Also cache libdeps, store them in a ~/.platformio subfolder
|
||||||
PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps
|
PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps
|
||||||
@ -346,59 +466,18 @@ jobs:
|
|||||||
# yamllint disable-line rule:line-length
|
# yamllint disable-line rule:line-length
|
||||||
if: always()
|
if: always()
|
||||||
|
|
||||||
list-components:
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
needs:
|
|
||||||
- common
|
|
||||||
if: github.event_name == 'pull_request'
|
|
||||||
outputs:
|
|
||||||
components: ${{ steps.list-components.outputs.components }}
|
|
||||||
count: ${{ steps.list-components.outputs.count }}
|
|
||||||
steps:
|
|
||||||
- name: Check out code from GitHub
|
|
||||||
uses: actions/checkout@v4.2.2
|
|
||||||
with:
|
|
||||||
# Fetch enough history so `git merge-base refs/remotes/origin/dev HEAD` works.
|
|
||||||
fetch-depth: 500
|
|
||||||
- name: Get target branch
|
|
||||||
id: target-branch
|
|
||||||
run: |
|
|
||||||
echo "branch=${{ github.event.pull_request.base.ref }}" >> $GITHUB_OUTPUT
|
|
||||||
- name: Fetch ${{ steps.target-branch.outputs.branch }} branch
|
|
||||||
run: |
|
|
||||||
git -c protocol.version=2 fetch --no-tags --prune --no-recurse-submodules --depth=1 origin +refs/heads/${{ steps.target-branch.outputs.branch }}:refs/remotes/origin/${{ steps.target-branch.outputs.branch }}
|
|
||||||
git merge-base refs/remotes/origin/${{ steps.target-branch.outputs.branch }} HEAD
|
|
||||||
- name: Restore Python
|
|
||||||
uses: ./.github/actions/restore-python
|
|
||||||
with:
|
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
|
||||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
|
||||||
- name: Find changed components
|
|
||||||
id: list-components
|
|
||||||
run: |
|
|
||||||
. venv/bin/activate
|
|
||||||
components=$(script/list-components.py --changed --branch ${{ steps.target-branch.outputs.branch }})
|
|
||||||
output_components=$(echo "$components" | jq -R -s -c 'split("\n")[:-1] | map(select(length > 0))')
|
|
||||||
count=$(echo "$output_components" | jq length)
|
|
||||||
|
|
||||||
echo "components=$output_components" >> $GITHUB_OUTPUT
|
|
||||||
echo "count=$count" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
echo "$count Components:"
|
|
||||||
echo "$output_components" | jq
|
|
||||||
|
|
||||||
test-build-components:
|
test-build-components:
|
||||||
name: Component test ${{ matrix.file }}
|
name: Component test ${{ matrix.file }}
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
needs:
|
needs:
|
||||||
- common
|
- common
|
||||||
- list-components
|
- determine-jobs
|
||||||
if: github.event_name == 'pull_request' && fromJSON(needs.list-components.outputs.count) > 0 && fromJSON(needs.list-components.outputs.count) < 100
|
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0 && fromJSON(needs.determine-jobs.outputs.component-test-count) < 100
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
max-parallel: 2
|
max-parallel: 2
|
||||||
matrix:
|
matrix:
|
||||||
file: ${{ fromJson(needs.list-components.outputs.components) }}
|
file: ${{ fromJson(needs.determine-jobs.outputs.changed-components) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
@ -426,8 +505,8 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
needs:
|
needs:
|
||||||
- common
|
- common
|
||||||
- list-components
|
- determine-jobs
|
||||||
if: github.event_name == 'pull_request' && fromJSON(needs.list-components.outputs.count) >= 100
|
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) >= 100
|
||||||
outputs:
|
outputs:
|
||||||
matrix: ${{ steps.split.outputs.components }}
|
matrix: ${{ steps.split.outputs.components }}
|
||||||
steps:
|
steps:
|
||||||
@ -436,7 +515,7 @@ jobs:
|
|||||||
- name: Split components into 20 groups
|
- name: Split components into 20 groups
|
||||||
id: split
|
id: split
|
||||||
run: |
|
run: |
|
||||||
components=$(echo '${{ needs.list-components.outputs.components }}' | jq -c '.[]' | shuf | jq -s -c '[_nwise(20) | join(" ")]')
|
components=$(echo '${{ needs.determine-jobs.outputs.changed-components }}' | jq -c '.[]' | shuf | jq -s -c '[_nwise(20) | join(" ")]')
|
||||||
echo "components=$components" >> $GITHUB_OUTPUT
|
echo "components=$components" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
test-build-components-split:
|
test-build-components-split:
|
||||||
@ -444,9 +523,9 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
needs:
|
needs:
|
||||||
- common
|
- common
|
||||||
- list-components
|
- determine-jobs
|
||||||
- test-build-components-splitter
|
- test-build-components-splitter
|
||||||
if: github.event_name == 'pull_request' && fromJSON(needs.list-components.outputs.count) >= 100
|
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) >= 100
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
max-parallel: 4
|
max-parallel: 4
|
||||||
@ -494,9 +573,10 @@ jobs:
|
|||||||
- flake8
|
- flake8
|
||||||
- pylint
|
- pylint
|
||||||
- pytest
|
- pytest
|
||||||
|
- integration-tests
|
||||||
- pyupgrade
|
- pyupgrade
|
||||||
- clang-tidy
|
- clang-tidy
|
||||||
- list-components
|
- determine-jobs
|
||||||
- test-build-components
|
- test-build-components
|
||||||
- test-build-components-splitter
|
- test-build-components-splitter
|
||||||
- test-build-components-split
|
- test-build-components-split
|
||||||
|
@ -48,3 +48,10 @@ repos:
|
|||||||
entry: python3 script/run-in-env.py pylint
|
entry: python3 script/run-in-env.py pylint
|
||||||
language: system
|
language: system
|
||||||
types: [python]
|
types: [python]
|
||||||
|
- id: clang-tidy-hash
|
||||||
|
name: Update clang-tidy hash
|
||||||
|
entry: python script/clang_tidy_hash.py --update-if-changed
|
||||||
|
language: python
|
||||||
|
files: ^(\.clang-tidy|platformio\.ini|requirements_dev\.txt)$
|
||||||
|
pass_filenames: false
|
||||||
|
additional_dependencies: []
|
||||||
|
@ -28,7 +28,7 @@ esphome/components/aic3204/* @kbx81
|
|||||||
esphome/components/airthings_ble/* @jeromelaban
|
esphome/components/airthings_ble/* @jeromelaban
|
||||||
esphome/components/airthings_wave_base/* @jeromelaban @kpfleming @ncareau
|
esphome/components/airthings_wave_base/* @jeromelaban @kpfleming @ncareau
|
||||||
esphome/components/airthings_wave_mini/* @ncareau
|
esphome/components/airthings_wave_mini/* @ncareau
|
||||||
esphome/components/airthings_wave_plus/* @jeromelaban
|
esphome/components/airthings_wave_plus/* @jeromelaban @precurse
|
||||||
esphome/components/alarm_control_panel/* @grahambrown11 @hwstar
|
esphome/components/alarm_control_panel/* @grahambrown11 @hwstar
|
||||||
esphome/components/alpha3/* @jan-hofmeier
|
esphome/components/alpha3/* @jan-hofmeier
|
||||||
esphome/components/am2315c/* @swoboda1337
|
esphome/components/am2315c/* @swoboda1337
|
||||||
@ -170,6 +170,7 @@ esphome/components/ft5x06/* @clydebarrow
|
|||||||
esphome/components/ft63x6/* @gpambrozio
|
esphome/components/ft63x6/* @gpambrozio
|
||||||
esphome/components/gcja5/* @gcormier
|
esphome/components/gcja5/* @gcormier
|
||||||
esphome/components/gdk101/* @Szewcson
|
esphome/components/gdk101/* @Szewcson
|
||||||
|
esphome/components/gl_r01_i2c/* @pkejval
|
||||||
esphome/components/globals/* @esphome/core
|
esphome/components/globals/* @esphome/core
|
||||||
esphome/components/gp2y1010au0f/* @zry98
|
esphome/components/gp2y1010au0f/* @zry98
|
||||||
esphome/components/gp8403/* @jesserockz
|
esphome/components/gp8403/* @jesserockz
|
||||||
@ -254,6 +255,7 @@ esphome/components/ln882x/* @lamauny
|
|||||||
esphome/components/lock/* @esphome/core
|
esphome/components/lock/* @esphome/core
|
||||||
esphome/components/logger/* @esphome/core
|
esphome/components/logger/* @esphome/core
|
||||||
esphome/components/logger/select/* @clydebarrow
|
esphome/components/logger/select/* @clydebarrow
|
||||||
|
esphome/components/lps22/* @nagisa
|
||||||
esphome/components/ltr390/* @latonita @sjtrny
|
esphome/components/ltr390/* @latonita @sjtrny
|
||||||
esphome/components/ltr501/* @latonita
|
esphome/components/ltr501/* @latonita
|
||||||
esphome/components/ltr_als_ps/* @latonita
|
esphome/components/ltr_als_ps/* @latonita
|
||||||
|
2
Doxyfile
2
Doxyfile
@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
|||||||
# could be handy for archiving the generated documentation or if some version
|
# could be handy for archiving the generated documentation or if some version
|
||||||
# control system is used.
|
# control system is used.
|
||||||
|
|
||||||
PROJECT_NUMBER = 2025.7.0-dev
|
PROJECT_NUMBER = 2025.8.0-dev
|
||||||
|
|
||||||
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
||||||
# for a project that appears at the top of each page and should give viewer a
|
# for a project that appears at the top of each page and should give viewer a
|
||||||
|
@ -1 +1 @@
|
|||||||
CODEOWNERS = ["@jeromelaban"]
|
CODEOWNERS = ["@jeromelaban", "@precurse"]
|
||||||
|
@ -73,11 +73,29 @@ void AirthingsWavePlus::dump_config() {
|
|||||||
LOG_SENSOR(" ", "Illuminance", this->illuminance_sensor_);
|
LOG_SENSOR(" ", "Illuminance", this->illuminance_sensor_);
|
||||||
}
|
}
|
||||||
|
|
||||||
AirthingsWavePlus::AirthingsWavePlus() {
|
void AirthingsWavePlus::setup() {
|
||||||
this->service_uuid_ = espbt::ESPBTUUID::from_raw(SERVICE_UUID);
|
const char *service_uuid;
|
||||||
this->sensors_data_characteristic_uuid_ = espbt::ESPBTUUID::from_raw(CHARACTERISTIC_UUID);
|
const char *characteristic_uuid;
|
||||||
|
const char *access_control_point_characteristic_uuid;
|
||||||
|
|
||||||
|
// Change UUIDs for Wave Radon Gen2
|
||||||
|
switch (this->wave_device_type_) {
|
||||||
|
case WaveDeviceType::WAVE_GEN2:
|
||||||
|
service_uuid = SERVICE_UUID_WAVE_RADON_GEN2;
|
||||||
|
characteristic_uuid = CHARACTERISTIC_UUID_WAVE_RADON_GEN2;
|
||||||
|
access_control_point_characteristic_uuid = ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID_WAVE_RADON_GEN2;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Wave Plus
|
||||||
|
service_uuid = SERVICE_UUID;
|
||||||
|
characteristic_uuid = CHARACTERISTIC_UUID;
|
||||||
|
access_control_point_characteristic_uuid = ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->service_uuid_ = espbt::ESPBTUUID::from_raw(service_uuid);
|
||||||
|
this->sensors_data_characteristic_uuid_ = espbt::ESPBTUUID::from_raw(characteristic_uuid);
|
||||||
this->access_control_point_characteristic_uuid_ =
|
this->access_control_point_characteristic_uuid_ =
|
||||||
espbt::ESPBTUUID::from_raw(ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID);
|
espbt::ESPBTUUID::from_raw(access_control_point_characteristic_uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace airthings_wave_plus
|
} // namespace airthings_wave_plus
|
||||||
|
@ -9,13 +9,20 @@ namespace airthings_wave_plus {
|
|||||||
|
|
||||||
namespace espbt = esphome::esp32_ble_tracker;
|
namespace espbt = esphome::esp32_ble_tracker;
|
||||||
|
|
||||||
|
enum WaveDeviceType : uint8_t { WAVE_PLUS = 0, WAVE_GEN2 = 1 };
|
||||||
|
|
||||||
static const char *const SERVICE_UUID = "b42e1c08-ade7-11e4-89d3-123b93f75cba";
|
static const char *const SERVICE_UUID = "b42e1c08-ade7-11e4-89d3-123b93f75cba";
|
||||||
static const char *const CHARACTERISTIC_UUID = "b42e2a68-ade7-11e4-89d3-123b93f75cba";
|
static const char *const CHARACTERISTIC_UUID = "b42e2a68-ade7-11e4-89d3-123b93f75cba";
|
||||||
static const char *const ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID = "b42e2d06-ade7-11e4-89d3-123b93f75cba";
|
static const char *const ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID = "b42e2d06-ade7-11e4-89d3-123b93f75cba";
|
||||||
|
|
||||||
|
static const char *const SERVICE_UUID_WAVE_RADON_GEN2 = "b42e4a8e-ade7-11e4-89d3-123b93f75cba";
|
||||||
|
static const char *const CHARACTERISTIC_UUID_WAVE_RADON_GEN2 = "b42e4dcc-ade7-11e4-89d3-123b93f75cba";
|
||||||
|
static const char *const ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID_WAVE_RADON_GEN2 =
|
||||||
|
"b42e50d8-ade7-11e4-89d3-123b93f75cba";
|
||||||
|
|
||||||
class AirthingsWavePlus : public airthings_wave_base::AirthingsWaveBase {
|
class AirthingsWavePlus : public airthings_wave_base::AirthingsWaveBase {
|
||||||
public:
|
public:
|
||||||
AirthingsWavePlus();
|
void setup() override;
|
||||||
|
|
||||||
void dump_config() override;
|
void dump_config() override;
|
||||||
|
|
||||||
@ -23,12 +30,14 @@ class AirthingsWavePlus : public airthings_wave_base::AirthingsWaveBase {
|
|||||||
void set_radon_long_term(sensor::Sensor *radon_long_term) { radon_long_term_sensor_ = radon_long_term; }
|
void set_radon_long_term(sensor::Sensor *radon_long_term) { radon_long_term_sensor_ = radon_long_term; }
|
||||||
void set_co2(sensor::Sensor *co2) { co2_sensor_ = co2; }
|
void set_co2(sensor::Sensor *co2) { co2_sensor_ = co2; }
|
||||||
void set_illuminance(sensor::Sensor *illuminance) { illuminance_sensor_ = illuminance; }
|
void set_illuminance(sensor::Sensor *illuminance) { illuminance_sensor_ = illuminance; }
|
||||||
|
void set_device_type(WaveDeviceType wave_device_type) { wave_device_type_ = wave_device_type; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
bool is_valid_radon_value_(uint16_t radon);
|
bool is_valid_radon_value_(uint16_t radon);
|
||||||
bool is_valid_co2_value_(uint16_t co2);
|
bool is_valid_co2_value_(uint16_t co2);
|
||||||
|
|
||||||
void read_sensors(uint8_t *raw_value, uint16_t value_len) override;
|
void read_sensors(uint8_t *raw_value, uint16_t value_len) override;
|
||||||
|
WaveDeviceType wave_device_type_{WaveDeviceType::WAVE_PLUS};
|
||||||
|
|
||||||
sensor::Sensor *radon_sensor_{nullptr};
|
sensor::Sensor *radon_sensor_{nullptr};
|
||||||
sensor::Sensor *radon_long_term_sensor_{nullptr};
|
sensor::Sensor *radon_long_term_sensor_{nullptr};
|
||||||
|
@ -7,6 +7,7 @@ from esphome.const import (
|
|||||||
CONF_ILLUMINANCE,
|
CONF_ILLUMINANCE,
|
||||||
CONF_RADON,
|
CONF_RADON,
|
||||||
CONF_RADON_LONG_TERM,
|
CONF_RADON_LONG_TERM,
|
||||||
|
CONF_TVOC,
|
||||||
DEVICE_CLASS_CARBON_DIOXIDE,
|
DEVICE_CLASS_CARBON_DIOXIDE,
|
||||||
DEVICE_CLASS_ILLUMINANCE,
|
DEVICE_CLASS_ILLUMINANCE,
|
||||||
ICON_RADIOACTIVE,
|
ICON_RADIOACTIVE,
|
||||||
@ -15,6 +16,7 @@ from esphome.const import (
|
|||||||
UNIT_LUX,
|
UNIT_LUX,
|
||||||
UNIT_PARTS_PER_MILLION,
|
UNIT_PARTS_PER_MILLION,
|
||||||
)
|
)
|
||||||
|
from esphome.types import ConfigType
|
||||||
|
|
||||||
DEPENDENCIES = airthings_wave_base.DEPENDENCIES
|
DEPENDENCIES = airthings_wave_base.DEPENDENCIES
|
||||||
|
|
||||||
@ -25,35 +27,59 @@ AirthingsWavePlus = airthings_wave_plus_ns.class_(
|
|||||||
"AirthingsWavePlus", airthings_wave_base.AirthingsWaveBase
|
"AirthingsWavePlus", airthings_wave_base.AirthingsWaveBase
|
||||||
)
|
)
|
||||||
|
|
||||||
|
CONF_DEVICE_TYPE = "device_type"
|
||||||
|
WaveDeviceType = airthings_wave_plus_ns.enum("WaveDeviceType")
|
||||||
|
DEVICE_TYPES = {
|
||||||
|
"WAVE_PLUS": WaveDeviceType.WAVE_PLUS,
|
||||||
|
"WAVE_GEN2": WaveDeviceType.WAVE_GEN2,
|
||||||
|
}
|
||||||
|
|
||||||
CONFIG_SCHEMA = airthings_wave_base.BASE_SCHEMA.extend(
|
|
||||||
{
|
def validate_wave_gen2_config(config: ConfigType) -> ConfigType:
|
||||||
cv.GenerateID(): cv.declare_id(AirthingsWavePlus),
|
"""Validate that Wave Gen2 devices don't have CO2 or TVOC sensors."""
|
||||||
cv.Optional(CONF_RADON): sensor.sensor_schema(
|
if config[CONF_DEVICE_TYPE] == "WAVE_GEN2":
|
||||||
unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER,
|
if CONF_CO2 in config:
|
||||||
icon=ICON_RADIOACTIVE,
|
raise cv.Invalid("Wave Gen2 devices do not support CO2 sensor")
|
||||||
accuracy_decimals=0,
|
# Check for TVOC in the base schema config
|
||||||
state_class=STATE_CLASS_MEASUREMENT,
|
if CONF_TVOC in config:
|
||||||
),
|
raise cv.Invalid("Wave Gen2 devices do not support TVOC sensor")
|
||||||
cv.Optional(CONF_RADON_LONG_TERM): sensor.sensor_schema(
|
return config
|
||||||
unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER,
|
|
||||||
icon=ICON_RADIOACTIVE,
|
|
||||||
accuracy_decimals=0,
|
CONFIG_SCHEMA = cv.All(
|
||||||
state_class=STATE_CLASS_MEASUREMENT,
|
airthings_wave_base.BASE_SCHEMA.extend(
|
||||||
),
|
{
|
||||||
cv.Optional(CONF_CO2): sensor.sensor_schema(
|
cv.GenerateID(): cv.declare_id(AirthingsWavePlus),
|
||||||
unit_of_measurement=UNIT_PARTS_PER_MILLION,
|
cv.Optional(CONF_RADON): sensor.sensor_schema(
|
||||||
accuracy_decimals=0,
|
unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER,
|
||||||
device_class=DEVICE_CLASS_CARBON_DIOXIDE,
|
icon=ICON_RADIOACTIVE,
|
||||||
state_class=STATE_CLASS_MEASUREMENT,
|
accuracy_decimals=0,
|
||||||
),
|
state_class=STATE_CLASS_MEASUREMENT,
|
||||||
cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema(
|
),
|
||||||
unit_of_measurement=UNIT_LUX,
|
cv.Optional(CONF_RADON_LONG_TERM): sensor.sensor_schema(
|
||||||
accuracy_decimals=0,
|
unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER,
|
||||||
device_class=DEVICE_CLASS_ILLUMINANCE,
|
icon=ICON_RADIOACTIVE,
|
||||||
state_class=STATE_CLASS_MEASUREMENT,
|
accuracy_decimals=0,
|
||||||
),
|
state_class=STATE_CLASS_MEASUREMENT,
|
||||||
}
|
),
|
||||||
|
cv.Optional(CONF_CO2): sensor.sensor_schema(
|
||||||
|
unit_of_measurement=UNIT_PARTS_PER_MILLION,
|
||||||
|
accuracy_decimals=0,
|
||||||
|
device_class=DEVICE_CLASS_CARBON_DIOXIDE,
|
||||||
|
state_class=STATE_CLASS_MEASUREMENT,
|
||||||
|
),
|
||||||
|
cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema(
|
||||||
|
unit_of_measurement=UNIT_LUX,
|
||||||
|
accuracy_decimals=0,
|
||||||
|
device_class=DEVICE_CLASS_ILLUMINANCE,
|
||||||
|
state_class=STATE_CLASS_MEASUREMENT,
|
||||||
|
),
|
||||||
|
cv.Optional(CONF_DEVICE_TYPE, default="WAVE_PLUS"): cv.enum(
|
||||||
|
DEVICE_TYPES, upper=True
|
||||||
|
),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
validate_wave_gen2_config,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -73,3 +99,4 @@ async def to_code(config):
|
|||||||
if config_illuminance := config.get(CONF_ILLUMINANCE):
|
if config_illuminance := config.get(CONF_ILLUMINANCE):
|
||||||
sens = await sensor.new_sensor(config_illuminance)
|
sens = await sensor.new_sensor(config_illuminance)
|
||||||
cg.add(var.set_illuminance(sens))
|
cg.add(var.set_illuminance(sens))
|
||||||
|
cg.add(var.set_device_type(config[CONF_DEVICE_TYPE]))
|
||||||
|
@ -23,7 +23,7 @@ void APDS9960::setup() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (id != 0xAB && id != 0x9C && id != 0xA8) { // APDS9960 all should have one of these IDs
|
if (id != 0xAB && id != 0x9C && id != 0xA8 && id != 0x9E) { // APDS9960 all should have one of these IDs
|
||||||
this->error_code_ = WRONG_ID;
|
this->error_code_ = WRONG_ID;
|
||||||
this->mark_failed();
|
this->mark_failed();
|
||||||
return;
|
return;
|
||||||
|
@ -374,6 +374,7 @@ message CoverCommandRequest {
|
|||||||
option (source) = SOURCE_CLIENT;
|
option (source) = SOURCE_CLIENT;
|
||||||
option (ifdef) = "USE_COVER";
|
option (ifdef) = "USE_COVER";
|
||||||
option (no_delay) = true;
|
option (no_delay) = true;
|
||||||
|
option (base_class) = "CommandProtoMessage";
|
||||||
|
|
||||||
fixed32 key = 1;
|
fixed32 key = 1;
|
||||||
|
|
||||||
@ -387,6 +388,7 @@ message CoverCommandRequest {
|
|||||||
bool has_tilt = 6;
|
bool has_tilt = 6;
|
||||||
float tilt = 7;
|
float tilt = 7;
|
||||||
bool stop = 8;
|
bool stop = 8;
|
||||||
|
uint32 device_id = 9;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== FAN ====================
|
// ==================== FAN ====================
|
||||||
@ -441,6 +443,7 @@ message FanCommandRequest {
|
|||||||
option (source) = SOURCE_CLIENT;
|
option (source) = SOURCE_CLIENT;
|
||||||
option (ifdef) = "USE_FAN";
|
option (ifdef) = "USE_FAN";
|
||||||
option (no_delay) = true;
|
option (no_delay) = true;
|
||||||
|
option (base_class) = "CommandProtoMessage";
|
||||||
|
|
||||||
fixed32 key = 1;
|
fixed32 key = 1;
|
||||||
bool has_state = 2;
|
bool has_state = 2;
|
||||||
@ -455,6 +458,7 @@ message FanCommandRequest {
|
|||||||
int32 speed_level = 11;
|
int32 speed_level = 11;
|
||||||
bool has_preset_mode = 12;
|
bool has_preset_mode = 12;
|
||||||
string preset_mode = 13;
|
string preset_mode = 13;
|
||||||
|
uint32 device_id = 14;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== LIGHT ====================
|
// ==================== LIGHT ====================
|
||||||
@ -523,6 +527,7 @@ message LightCommandRequest {
|
|||||||
option (source) = SOURCE_CLIENT;
|
option (source) = SOURCE_CLIENT;
|
||||||
option (ifdef) = "USE_LIGHT";
|
option (ifdef) = "USE_LIGHT";
|
||||||
option (no_delay) = true;
|
option (no_delay) = true;
|
||||||
|
option (base_class) = "CommandProtoMessage";
|
||||||
|
|
||||||
fixed32 key = 1;
|
fixed32 key = 1;
|
||||||
bool has_state = 2;
|
bool has_state = 2;
|
||||||
@ -551,6 +556,7 @@ message LightCommandRequest {
|
|||||||
uint32 flash_length = 17;
|
uint32 flash_length = 17;
|
||||||
bool has_effect = 18;
|
bool has_effect = 18;
|
||||||
string effect = 19;
|
string effect = 19;
|
||||||
|
uint32 device_id = 28;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== SENSOR ====================
|
// ==================== SENSOR ====================
|
||||||
@ -640,9 +646,11 @@ message SwitchCommandRequest {
|
|||||||
option (source) = SOURCE_CLIENT;
|
option (source) = SOURCE_CLIENT;
|
||||||
option (ifdef) = "USE_SWITCH";
|
option (ifdef) = "USE_SWITCH";
|
||||||
option (no_delay) = true;
|
option (no_delay) = true;
|
||||||
|
option (base_class) = "CommandProtoMessage";
|
||||||
|
|
||||||
fixed32 key = 1;
|
fixed32 key = 1;
|
||||||
bool state = 2;
|
bool state = 2;
|
||||||
|
uint32 device_id = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== TEXT SENSOR ====================
|
// ==================== TEXT SENSOR ====================
|
||||||
@ -850,12 +858,14 @@ message ListEntitiesCameraResponse {
|
|||||||
|
|
||||||
message CameraImageResponse {
|
message CameraImageResponse {
|
||||||
option (id) = 44;
|
option (id) = 44;
|
||||||
|
option (base_class) = "StateResponseProtoMessage";
|
||||||
option (source) = SOURCE_SERVER;
|
option (source) = SOURCE_SERVER;
|
||||||
option (ifdef) = "USE_CAMERA";
|
option (ifdef) = "USE_CAMERA";
|
||||||
|
|
||||||
fixed32 key = 1;
|
fixed32 key = 1;
|
||||||
bytes data = 2;
|
bytes data = 2;
|
||||||
bool done = 3;
|
bool done = 3;
|
||||||
|
uint32 device_id = 4;
|
||||||
}
|
}
|
||||||
message CameraImageRequest {
|
message CameraImageRequest {
|
||||||
option (id) = 45;
|
option (id) = 45;
|
||||||
@ -980,6 +990,7 @@ message ClimateCommandRequest {
|
|||||||
option (source) = SOURCE_CLIENT;
|
option (source) = SOURCE_CLIENT;
|
||||||
option (ifdef) = "USE_CLIMATE";
|
option (ifdef) = "USE_CLIMATE";
|
||||||
option (no_delay) = true;
|
option (no_delay) = true;
|
||||||
|
option (base_class) = "CommandProtoMessage";
|
||||||
|
|
||||||
fixed32 key = 1;
|
fixed32 key = 1;
|
||||||
bool has_mode = 2;
|
bool has_mode = 2;
|
||||||
@ -1005,6 +1016,7 @@ message ClimateCommandRequest {
|
|||||||
string custom_preset = 21;
|
string custom_preset = 21;
|
||||||
bool has_target_humidity = 22;
|
bool has_target_humidity = 22;
|
||||||
float target_humidity = 23;
|
float target_humidity = 23;
|
||||||
|
uint32 device_id = 24;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== NUMBER ====================
|
// ==================== NUMBER ====================
|
||||||
@ -1054,9 +1066,11 @@ message NumberCommandRequest {
|
|||||||
option (source) = SOURCE_CLIENT;
|
option (source) = SOURCE_CLIENT;
|
||||||
option (ifdef) = "USE_NUMBER";
|
option (ifdef) = "USE_NUMBER";
|
||||||
option (no_delay) = true;
|
option (no_delay) = true;
|
||||||
|
option (base_class) = "CommandProtoMessage";
|
||||||
|
|
||||||
fixed32 key = 1;
|
fixed32 key = 1;
|
||||||
float state = 2;
|
float state = 2;
|
||||||
|
uint32 device_id = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== SELECT ====================
|
// ==================== SELECT ====================
|
||||||
@ -1096,9 +1110,11 @@ message SelectCommandRequest {
|
|||||||
option (source) = SOURCE_CLIENT;
|
option (source) = SOURCE_CLIENT;
|
||||||
option (ifdef) = "USE_SELECT";
|
option (ifdef) = "USE_SELECT";
|
||||||
option (no_delay) = true;
|
option (no_delay) = true;
|
||||||
|
option (base_class) = "CommandProtoMessage";
|
||||||
|
|
||||||
fixed32 key = 1;
|
fixed32 key = 1;
|
||||||
string state = 2;
|
string state = 2;
|
||||||
|
uint32 device_id = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== SIREN ====================
|
// ==================== SIREN ====================
|
||||||
@ -1137,6 +1153,7 @@ message SirenCommandRequest {
|
|||||||
option (source) = SOURCE_CLIENT;
|
option (source) = SOURCE_CLIENT;
|
||||||
option (ifdef) = "USE_SIREN";
|
option (ifdef) = "USE_SIREN";
|
||||||
option (no_delay) = true;
|
option (no_delay) = true;
|
||||||
|
option (base_class) = "CommandProtoMessage";
|
||||||
|
|
||||||
fixed32 key = 1;
|
fixed32 key = 1;
|
||||||
bool has_state = 2;
|
bool has_state = 2;
|
||||||
@ -1147,6 +1164,7 @@ message SirenCommandRequest {
|
|||||||
uint32 duration = 7;
|
uint32 duration = 7;
|
||||||
bool has_volume = 8;
|
bool has_volume = 8;
|
||||||
float volume = 9;
|
float volume = 9;
|
||||||
|
uint32 device_id = 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== LOCK ====================
|
// ==================== LOCK ====================
|
||||||
@ -1201,12 +1219,14 @@ message LockCommandRequest {
|
|||||||
option (source) = SOURCE_CLIENT;
|
option (source) = SOURCE_CLIENT;
|
||||||
option (ifdef) = "USE_LOCK";
|
option (ifdef) = "USE_LOCK";
|
||||||
option (no_delay) = true;
|
option (no_delay) = true;
|
||||||
|
option (base_class) = "CommandProtoMessage";
|
||||||
fixed32 key = 1;
|
fixed32 key = 1;
|
||||||
LockCommand command = 2;
|
LockCommand command = 2;
|
||||||
|
|
||||||
// Not yet implemented:
|
// Not yet implemented:
|
||||||
bool has_code = 3;
|
bool has_code = 3;
|
||||||
string code = 4;
|
string code = 4;
|
||||||
|
uint32 device_id = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== BUTTON ====================
|
// ==================== BUTTON ====================
|
||||||
@ -1232,8 +1252,10 @@ message ButtonCommandRequest {
|
|||||||
option (source) = SOURCE_CLIENT;
|
option (source) = SOURCE_CLIENT;
|
||||||
option (ifdef) = "USE_BUTTON";
|
option (ifdef) = "USE_BUTTON";
|
||||||
option (no_delay) = true;
|
option (no_delay) = true;
|
||||||
|
option (base_class) = "CommandProtoMessage";
|
||||||
|
|
||||||
fixed32 key = 1;
|
fixed32 key = 1;
|
||||||
|
uint32 device_id = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== MEDIA PLAYER ====================
|
// ==================== MEDIA PLAYER ====================
|
||||||
@ -1301,6 +1323,7 @@ message MediaPlayerCommandRequest {
|
|||||||
option (source) = SOURCE_CLIENT;
|
option (source) = SOURCE_CLIENT;
|
||||||
option (ifdef) = "USE_MEDIA_PLAYER";
|
option (ifdef) = "USE_MEDIA_PLAYER";
|
||||||
option (no_delay) = true;
|
option (no_delay) = true;
|
||||||
|
option (base_class) = "CommandProtoMessage";
|
||||||
|
|
||||||
fixed32 key = 1;
|
fixed32 key = 1;
|
||||||
|
|
||||||
@ -1315,6 +1338,7 @@ message MediaPlayerCommandRequest {
|
|||||||
|
|
||||||
bool has_announcement = 8;
|
bool has_announcement = 8;
|
||||||
bool announcement = 9;
|
bool announcement = 9;
|
||||||
|
uint32 device_id = 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== BLUETOOTH ====================
|
// ==================== BLUETOOTH ====================
|
||||||
@ -1843,9 +1867,11 @@ message AlarmControlPanelCommandRequest {
|
|||||||
option (source) = SOURCE_CLIENT;
|
option (source) = SOURCE_CLIENT;
|
||||||
option (ifdef) = "USE_ALARM_CONTROL_PANEL";
|
option (ifdef) = "USE_ALARM_CONTROL_PANEL";
|
||||||
option (no_delay) = true;
|
option (no_delay) = true;
|
||||||
|
option (base_class) = "CommandProtoMessage";
|
||||||
fixed32 key = 1;
|
fixed32 key = 1;
|
||||||
AlarmControlPanelStateCommand command = 2;
|
AlarmControlPanelStateCommand command = 2;
|
||||||
string code = 3;
|
string code = 3;
|
||||||
|
uint32 device_id = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===================== TEXT =====================
|
// ===================== TEXT =====================
|
||||||
@ -1892,9 +1918,11 @@ message TextCommandRequest {
|
|||||||
option (source) = SOURCE_CLIENT;
|
option (source) = SOURCE_CLIENT;
|
||||||
option (ifdef) = "USE_TEXT";
|
option (ifdef) = "USE_TEXT";
|
||||||
option (no_delay) = true;
|
option (no_delay) = true;
|
||||||
|
option (base_class) = "CommandProtoMessage";
|
||||||
|
|
||||||
fixed32 key = 1;
|
fixed32 key = 1;
|
||||||
string state = 2;
|
string state = 2;
|
||||||
|
uint32 device_id = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -1936,11 +1964,13 @@ message DateCommandRequest {
|
|||||||
option (source) = SOURCE_CLIENT;
|
option (source) = SOURCE_CLIENT;
|
||||||
option (ifdef) = "USE_DATETIME_DATE";
|
option (ifdef) = "USE_DATETIME_DATE";
|
||||||
option (no_delay) = true;
|
option (no_delay) = true;
|
||||||
|
option (base_class) = "CommandProtoMessage";
|
||||||
|
|
||||||
fixed32 key = 1;
|
fixed32 key = 1;
|
||||||
uint32 year = 2;
|
uint32 year = 2;
|
||||||
uint32 month = 3;
|
uint32 month = 3;
|
||||||
uint32 day = 4;
|
uint32 day = 4;
|
||||||
|
uint32 device_id = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== DATETIME TIME ====================
|
// ==================== DATETIME TIME ====================
|
||||||
@ -1981,11 +2011,13 @@ message TimeCommandRequest {
|
|||||||
option (source) = SOURCE_CLIENT;
|
option (source) = SOURCE_CLIENT;
|
||||||
option (ifdef) = "USE_DATETIME_TIME";
|
option (ifdef) = "USE_DATETIME_TIME";
|
||||||
option (no_delay) = true;
|
option (no_delay) = true;
|
||||||
|
option (base_class) = "CommandProtoMessage";
|
||||||
|
|
||||||
fixed32 key = 1;
|
fixed32 key = 1;
|
||||||
uint32 hour = 2;
|
uint32 hour = 2;
|
||||||
uint32 minute = 3;
|
uint32 minute = 3;
|
||||||
uint32 second = 4;
|
uint32 second = 4;
|
||||||
|
uint32 device_id = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== EVENT ====================
|
// ==================== EVENT ====================
|
||||||
@ -2065,11 +2097,13 @@ message ValveCommandRequest {
|
|||||||
option (source) = SOURCE_CLIENT;
|
option (source) = SOURCE_CLIENT;
|
||||||
option (ifdef) = "USE_VALVE";
|
option (ifdef) = "USE_VALVE";
|
||||||
option (no_delay) = true;
|
option (no_delay) = true;
|
||||||
|
option (base_class) = "CommandProtoMessage";
|
||||||
|
|
||||||
fixed32 key = 1;
|
fixed32 key = 1;
|
||||||
bool has_position = 2;
|
bool has_position = 2;
|
||||||
float position = 3;
|
float position = 3;
|
||||||
bool stop = 4;
|
bool stop = 4;
|
||||||
|
uint32 device_id = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== DATETIME DATETIME ====================
|
// ==================== DATETIME DATETIME ====================
|
||||||
@ -2108,9 +2142,11 @@ message DateTimeCommandRequest {
|
|||||||
option (source) = SOURCE_CLIENT;
|
option (source) = SOURCE_CLIENT;
|
||||||
option (ifdef) = "USE_DATETIME_DATETIME";
|
option (ifdef) = "USE_DATETIME_DATETIME";
|
||||||
option (no_delay) = true;
|
option (no_delay) = true;
|
||||||
|
option (base_class) = "CommandProtoMessage";
|
||||||
|
|
||||||
fixed32 key = 1;
|
fixed32 key = 1;
|
||||||
fixed32 epoch_seconds = 2;
|
fixed32 epoch_seconds = 2;
|
||||||
|
uint32 device_id = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== UPDATE ====================
|
// ==================== UPDATE ====================
|
||||||
@ -2160,7 +2196,9 @@ message UpdateCommandRequest {
|
|||||||
option (source) = SOURCE_CLIENT;
|
option (source) = SOURCE_CLIENT;
|
||||||
option (ifdef) = "USE_UPDATE";
|
option (ifdef) = "USE_UPDATE";
|
||||||
option (no_delay) = true;
|
option (no_delay) = true;
|
||||||
|
option (base_class) = "CommandProtoMessage";
|
||||||
|
|
||||||
fixed32 key = 1;
|
fixed32 key = 1;
|
||||||
UpdateCommand command = 2;
|
UpdateCommand command = 2;
|
||||||
|
uint32 device_id = 3;
|
||||||
}
|
}
|
||||||
|
@ -193,14 +193,15 @@ void APIConnection::loop() {
|
|||||||
// If we can't send the ping request directly (tx_buffer full),
|
// If we can't send the ping request directly (tx_buffer full),
|
||||||
// schedule it at the front of the batch so it will be sent with priority
|
// schedule it at the front of the batch so it will be sent with priority
|
||||||
ESP_LOGW(TAG, "Buffer full, ping queued");
|
ESP_LOGW(TAG, "Buffer full, ping queued");
|
||||||
this->schedule_message_front_(nullptr, &APIConnection::try_send_ping_request, PingRequest::MESSAGE_TYPE);
|
this->schedule_message_front_(nullptr, &APIConnection::try_send_ping_request, PingRequest::MESSAGE_TYPE,
|
||||||
|
PingRequest::ESTIMATED_SIZE);
|
||||||
this->flags_.sent_ping = true; // Mark as sent to avoid scheduling multiple pings
|
this->flags_.sent_ping = true; // Mark as sent to avoid scheduling multiple pings
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef USE_CAMERA
|
#ifdef USE_CAMERA
|
||||||
if (this->image_reader_ && this->image_reader_->available() && this->helper_->can_write_without_blocking()) {
|
if (this->image_reader_ && this->image_reader_->available() && this->helper_->can_write_without_blocking()) {
|
||||||
uint32_t to_send = std::min((size_t) MAX_PACKET_SIZE, this->image_reader_->available());
|
uint32_t to_send = std::min((size_t) MAX_BATCH_PACKET_SIZE, this->image_reader_->available());
|
||||||
bool done = this->image_reader_->available() == to_send;
|
bool done = this->image_reader_->available() == to_send;
|
||||||
uint32_t msg_size = 0;
|
uint32_t msg_size = 0;
|
||||||
ProtoSize::add_fixed_field<4>(msg_size, 1, true);
|
ProtoSize::add_fixed_field<4>(msg_size, 1, true);
|
||||||
@ -265,7 +266,7 @@ void APIConnection::on_disconnect_response(const DisconnectResponse &value) {
|
|||||||
|
|
||||||
// Encodes a message to the buffer and returns the total number of bytes used,
|
// Encodes a message to the buffer and returns the total number of bytes used,
|
||||||
// including header and footer overhead. Returns 0 if the message doesn't fit.
|
// including header and footer overhead. Returns 0 if the message doesn't fit.
|
||||||
uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint16_t message_type, APIConnection *conn,
|
uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint8_t message_type, APIConnection *conn,
|
||||||
uint32_t remaining_size, bool is_single) {
|
uint32_t remaining_size, bool is_single) {
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
// If in log-only mode, just log and return
|
// If in log-only mode, just log and return
|
||||||
@ -316,7 +317,7 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint16_t mes
|
|||||||
#ifdef USE_BINARY_SENSOR
|
#ifdef USE_BINARY_SENSOR
|
||||||
bool APIConnection::send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor) {
|
bool APIConnection::send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor) {
|
||||||
return this->send_message_smart_(binary_sensor, &APIConnection::try_send_binary_sensor_state,
|
return this->send_message_smart_(binary_sensor, &APIConnection::try_send_binary_sensor_state,
|
||||||
BinarySensorStateResponse::MESSAGE_TYPE);
|
BinarySensorStateResponse::MESSAGE_TYPE, BinarySensorStateResponse::ESTIMATED_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t APIConnection::try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
uint16_t APIConnection::try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||||
@ -343,7 +344,8 @@ uint16_t APIConnection::try_send_binary_sensor_info(EntityBase *entity, APIConne
|
|||||||
|
|
||||||
#ifdef USE_COVER
|
#ifdef USE_COVER
|
||||||
bool APIConnection::send_cover_state(cover::Cover *cover) {
|
bool APIConnection::send_cover_state(cover::Cover *cover) {
|
||||||
return this->send_message_smart_(cover, &APIConnection::try_send_cover_state, CoverStateResponse::MESSAGE_TYPE);
|
return this->send_message_smart_(cover, &APIConnection::try_send_cover_state, CoverStateResponse::MESSAGE_TYPE,
|
||||||
|
CoverStateResponse::ESTIMATED_SIZE);
|
||||||
}
|
}
|
||||||
uint16_t APIConnection::try_send_cover_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
uint16_t APIConnection::try_send_cover_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||||
bool is_single) {
|
bool is_single) {
|
||||||
@ -400,7 +402,8 @@ void APIConnection::cover_command(const CoverCommandRequest &msg) {
|
|||||||
|
|
||||||
#ifdef USE_FAN
|
#ifdef USE_FAN
|
||||||
bool APIConnection::send_fan_state(fan::Fan *fan) {
|
bool APIConnection::send_fan_state(fan::Fan *fan) {
|
||||||
return this->send_message_smart_(fan, &APIConnection::try_send_fan_state, FanStateResponse::MESSAGE_TYPE);
|
return this->send_message_smart_(fan, &APIConnection::try_send_fan_state, FanStateResponse::MESSAGE_TYPE,
|
||||||
|
FanStateResponse::ESTIMATED_SIZE);
|
||||||
}
|
}
|
||||||
uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||||
bool is_single) {
|
bool is_single) {
|
||||||
@ -455,7 +458,8 @@ void APIConnection::fan_command(const FanCommandRequest &msg) {
|
|||||||
|
|
||||||
#ifdef USE_LIGHT
|
#ifdef USE_LIGHT
|
||||||
bool APIConnection::send_light_state(light::LightState *light) {
|
bool APIConnection::send_light_state(light::LightState *light) {
|
||||||
return this->send_message_smart_(light, &APIConnection::try_send_light_state, LightStateResponse::MESSAGE_TYPE);
|
return this->send_message_smart_(light, &APIConnection::try_send_light_state, LightStateResponse::MESSAGE_TYPE,
|
||||||
|
LightStateResponse::ESTIMATED_SIZE);
|
||||||
}
|
}
|
||||||
uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||||
bool is_single) {
|
bool is_single) {
|
||||||
@ -543,7 +547,8 @@ void APIConnection::light_command(const LightCommandRequest &msg) {
|
|||||||
|
|
||||||
#ifdef USE_SENSOR
|
#ifdef USE_SENSOR
|
||||||
bool APIConnection::send_sensor_state(sensor::Sensor *sensor) {
|
bool APIConnection::send_sensor_state(sensor::Sensor *sensor) {
|
||||||
return this->send_message_smart_(sensor, &APIConnection::try_send_sensor_state, SensorStateResponse::MESSAGE_TYPE);
|
return this->send_message_smart_(sensor, &APIConnection::try_send_sensor_state, SensorStateResponse::MESSAGE_TYPE,
|
||||||
|
SensorStateResponse::ESTIMATED_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t APIConnection::try_send_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
uint16_t APIConnection::try_send_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||||
@ -575,7 +580,8 @@ uint16_t APIConnection::try_send_sensor_info(EntityBase *entity, APIConnection *
|
|||||||
|
|
||||||
#ifdef USE_SWITCH
|
#ifdef USE_SWITCH
|
||||||
bool APIConnection::send_switch_state(switch_::Switch *a_switch) {
|
bool APIConnection::send_switch_state(switch_::Switch *a_switch) {
|
||||||
return this->send_message_smart_(a_switch, &APIConnection::try_send_switch_state, SwitchStateResponse::MESSAGE_TYPE);
|
return this->send_message_smart_(a_switch, &APIConnection::try_send_switch_state, SwitchStateResponse::MESSAGE_TYPE,
|
||||||
|
SwitchStateResponse::ESTIMATED_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t APIConnection::try_send_switch_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
uint16_t APIConnection::try_send_switch_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||||
@ -611,7 +617,7 @@ void APIConnection::switch_command(const SwitchCommandRequest &msg) {
|
|||||||
#ifdef USE_TEXT_SENSOR
|
#ifdef USE_TEXT_SENSOR
|
||||||
bool APIConnection::send_text_sensor_state(text_sensor::TextSensor *text_sensor) {
|
bool APIConnection::send_text_sensor_state(text_sensor::TextSensor *text_sensor) {
|
||||||
return this->send_message_smart_(text_sensor, &APIConnection::try_send_text_sensor_state,
|
return this->send_message_smart_(text_sensor, &APIConnection::try_send_text_sensor_state,
|
||||||
TextSensorStateResponse::MESSAGE_TYPE);
|
TextSensorStateResponse::MESSAGE_TYPE, TextSensorStateResponse::ESTIMATED_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t APIConnection::try_send_text_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
uint16_t APIConnection::try_send_text_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||||
@ -638,7 +644,8 @@ uint16_t APIConnection::try_send_text_sensor_info(EntityBase *entity, APIConnect
|
|||||||
|
|
||||||
#ifdef USE_CLIMATE
|
#ifdef USE_CLIMATE
|
||||||
bool APIConnection::send_climate_state(climate::Climate *climate) {
|
bool APIConnection::send_climate_state(climate::Climate *climate) {
|
||||||
return this->send_message_smart_(climate, &APIConnection::try_send_climate_state, ClimateStateResponse::MESSAGE_TYPE);
|
return this->send_message_smart_(climate, &APIConnection::try_send_climate_state, ClimateStateResponse::MESSAGE_TYPE,
|
||||||
|
ClimateStateResponse::ESTIMATED_SIZE);
|
||||||
}
|
}
|
||||||
uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||||
bool is_single) {
|
bool is_single) {
|
||||||
@ -734,7 +741,8 @@ void APIConnection::climate_command(const ClimateCommandRequest &msg) {
|
|||||||
|
|
||||||
#ifdef USE_NUMBER
|
#ifdef USE_NUMBER
|
||||||
bool APIConnection::send_number_state(number::Number *number) {
|
bool APIConnection::send_number_state(number::Number *number) {
|
||||||
return this->send_message_smart_(number, &APIConnection::try_send_number_state, NumberStateResponse::MESSAGE_TYPE);
|
return this->send_message_smart_(number, &APIConnection::try_send_number_state, NumberStateResponse::MESSAGE_TYPE,
|
||||||
|
NumberStateResponse::ESTIMATED_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t APIConnection::try_send_number_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
uint16_t APIConnection::try_send_number_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||||
@ -770,7 +778,8 @@ void APIConnection::number_command(const NumberCommandRequest &msg) {
|
|||||||
|
|
||||||
#ifdef USE_DATETIME_DATE
|
#ifdef USE_DATETIME_DATE
|
||||||
bool APIConnection::send_date_state(datetime::DateEntity *date) {
|
bool APIConnection::send_date_state(datetime::DateEntity *date) {
|
||||||
return this->send_message_smart_(date, &APIConnection::try_send_date_state, DateStateResponse::MESSAGE_TYPE);
|
return this->send_message_smart_(date, &APIConnection::try_send_date_state, DateStateResponse::MESSAGE_TYPE,
|
||||||
|
DateStateResponse::ESTIMATED_SIZE);
|
||||||
}
|
}
|
||||||
uint16_t APIConnection::try_send_date_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
uint16_t APIConnection::try_send_date_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||||
bool is_single) {
|
bool is_single) {
|
||||||
@ -800,7 +809,8 @@ void APIConnection::date_command(const DateCommandRequest &msg) {
|
|||||||
|
|
||||||
#ifdef USE_DATETIME_TIME
|
#ifdef USE_DATETIME_TIME
|
||||||
bool APIConnection::send_time_state(datetime::TimeEntity *time) {
|
bool APIConnection::send_time_state(datetime::TimeEntity *time) {
|
||||||
return this->send_message_smart_(time, &APIConnection::try_send_time_state, TimeStateResponse::MESSAGE_TYPE);
|
return this->send_message_smart_(time, &APIConnection::try_send_time_state, TimeStateResponse::MESSAGE_TYPE,
|
||||||
|
TimeStateResponse::ESTIMATED_SIZE);
|
||||||
}
|
}
|
||||||
uint16_t APIConnection::try_send_time_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
uint16_t APIConnection::try_send_time_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||||
bool is_single) {
|
bool is_single) {
|
||||||
@ -831,7 +841,7 @@ void APIConnection::time_command(const TimeCommandRequest &msg) {
|
|||||||
#ifdef USE_DATETIME_DATETIME
|
#ifdef USE_DATETIME_DATETIME
|
||||||
bool APIConnection::send_datetime_state(datetime::DateTimeEntity *datetime) {
|
bool APIConnection::send_datetime_state(datetime::DateTimeEntity *datetime) {
|
||||||
return this->send_message_smart_(datetime, &APIConnection::try_send_datetime_state,
|
return this->send_message_smart_(datetime, &APIConnection::try_send_datetime_state,
|
||||||
DateTimeStateResponse::MESSAGE_TYPE);
|
DateTimeStateResponse::MESSAGE_TYPE, DateTimeStateResponse::ESTIMATED_SIZE);
|
||||||
}
|
}
|
||||||
uint16_t APIConnection::try_send_datetime_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
uint16_t APIConnection::try_send_datetime_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||||
bool is_single) {
|
bool is_single) {
|
||||||
@ -862,7 +872,8 @@ void APIConnection::datetime_command(const DateTimeCommandRequest &msg) {
|
|||||||
|
|
||||||
#ifdef USE_TEXT
|
#ifdef USE_TEXT
|
||||||
bool APIConnection::send_text_state(text::Text *text) {
|
bool APIConnection::send_text_state(text::Text *text) {
|
||||||
return this->send_message_smart_(text, &APIConnection::try_send_text_state, TextStateResponse::MESSAGE_TYPE);
|
return this->send_message_smart_(text, &APIConnection::try_send_text_state, TextStateResponse::MESSAGE_TYPE,
|
||||||
|
TextStateResponse::ESTIMATED_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t APIConnection::try_send_text_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
uint16_t APIConnection::try_send_text_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||||
@ -896,7 +907,8 @@ void APIConnection::text_command(const TextCommandRequest &msg) {
|
|||||||
|
|
||||||
#ifdef USE_SELECT
|
#ifdef USE_SELECT
|
||||||
bool APIConnection::send_select_state(select::Select *select) {
|
bool APIConnection::send_select_state(select::Select *select) {
|
||||||
return this->send_message_smart_(select, &APIConnection::try_send_select_state, SelectStateResponse::MESSAGE_TYPE);
|
return this->send_message_smart_(select, &APIConnection::try_send_select_state, SelectStateResponse::MESSAGE_TYPE,
|
||||||
|
SelectStateResponse::ESTIMATED_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||||
@ -944,7 +956,8 @@ void esphome::api::APIConnection::button_command(const ButtonCommandRequest &msg
|
|||||||
|
|
||||||
#ifdef USE_LOCK
|
#ifdef USE_LOCK
|
||||||
bool APIConnection::send_lock_state(lock::Lock *a_lock) {
|
bool APIConnection::send_lock_state(lock::Lock *a_lock) {
|
||||||
return this->send_message_smart_(a_lock, &APIConnection::try_send_lock_state, LockStateResponse::MESSAGE_TYPE);
|
return this->send_message_smart_(a_lock, &APIConnection::try_send_lock_state, LockStateResponse::MESSAGE_TYPE,
|
||||||
|
LockStateResponse::ESTIMATED_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t APIConnection::try_send_lock_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
uint16_t APIConnection::try_send_lock_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||||
@ -986,7 +999,8 @@ void APIConnection::lock_command(const LockCommandRequest &msg) {
|
|||||||
|
|
||||||
#ifdef USE_VALVE
|
#ifdef USE_VALVE
|
||||||
bool APIConnection::send_valve_state(valve::Valve *valve) {
|
bool APIConnection::send_valve_state(valve::Valve *valve) {
|
||||||
return this->send_message_smart_(valve, &APIConnection::try_send_valve_state, ValveStateResponse::MESSAGE_TYPE);
|
return this->send_message_smart_(valve, &APIConnection::try_send_valve_state, ValveStateResponse::MESSAGE_TYPE,
|
||||||
|
ValveStateResponse::ESTIMATED_SIZE);
|
||||||
}
|
}
|
||||||
uint16_t APIConnection::try_send_valve_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
uint16_t APIConnection::try_send_valve_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||||
bool is_single) {
|
bool is_single) {
|
||||||
@ -1023,7 +1037,7 @@ void APIConnection::valve_command(const ValveCommandRequest &msg) {
|
|||||||
#ifdef USE_MEDIA_PLAYER
|
#ifdef USE_MEDIA_PLAYER
|
||||||
bool APIConnection::send_media_player_state(media_player::MediaPlayer *media_player) {
|
bool APIConnection::send_media_player_state(media_player::MediaPlayer *media_player) {
|
||||||
return this->send_message_smart_(media_player, &APIConnection::try_send_media_player_state,
|
return this->send_message_smart_(media_player, &APIConnection::try_send_media_player_state,
|
||||||
MediaPlayerStateResponse::MESSAGE_TYPE);
|
MediaPlayerStateResponse::MESSAGE_TYPE, MediaPlayerStateResponse::ESTIMATED_SIZE);
|
||||||
}
|
}
|
||||||
uint16_t APIConnection::try_send_media_player_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
uint16_t APIConnection::try_send_media_player_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||||
bool is_single) {
|
bool is_single) {
|
||||||
@ -1262,7 +1276,8 @@ void APIConnection::voice_assistant_set_configuration(const VoiceAssistantSetCon
|
|||||||
#ifdef USE_ALARM_CONTROL_PANEL
|
#ifdef USE_ALARM_CONTROL_PANEL
|
||||||
bool APIConnection::send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) {
|
bool APIConnection::send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) {
|
||||||
return this->send_message_smart_(a_alarm_control_panel, &APIConnection::try_send_alarm_control_panel_state,
|
return this->send_message_smart_(a_alarm_control_panel, &APIConnection::try_send_alarm_control_panel_state,
|
||||||
AlarmControlPanelStateResponse::MESSAGE_TYPE);
|
AlarmControlPanelStateResponse::MESSAGE_TYPE,
|
||||||
|
AlarmControlPanelStateResponse::ESTIMATED_SIZE);
|
||||||
}
|
}
|
||||||
uint16_t APIConnection::try_send_alarm_control_panel_state(EntityBase *entity, APIConnection *conn,
|
uint16_t APIConnection::try_send_alarm_control_panel_state(EntityBase *entity, APIConnection *conn,
|
||||||
uint32_t remaining_size, bool is_single) {
|
uint32_t remaining_size, bool is_single) {
|
||||||
@ -1316,7 +1331,8 @@ void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRe
|
|||||||
|
|
||||||
#ifdef USE_EVENT
|
#ifdef USE_EVENT
|
||||||
void APIConnection::send_event(event::Event *event, const std::string &event_type) {
|
void APIConnection::send_event(event::Event *event, const std::string &event_type) {
|
||||||
this->schedule_message_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE);
|
this->schedule_message_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE,
|
||||||
|
EventResponse::ESTIMATED_SIZE);
|
||||||
}
|
}
|
||||||
uint16_t APIConnection::try_send_event_response(event::Event *event, const std::string &event_type, APIConnection *conn,
|
uint16_t APIConnection::try_send_event_response(event::Event *event, const std::string &event_type, APIConnection *conn,
|
||||||
uint32_t remaining_size, bool is_single) {
|
uint32_t remaining_size, bool is_single) {
|
||||||
@ -1341,7 +1357,8 @@ uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *c
|
|||||||
|
|
||||||
#ifdef USE_UPDATE
|
#ifdef USE_UPDATE
|
||||||
bool APIConnection::send_update_state(update::UpdateEntity *update) {
|
bool APIConnection::send_update_state(update::UpdateEntity *update) {
|
||||||
return this->send_message_smart_(update, &APIConnection::try_send_update_state, UpdateStateResponse::MESSAGE_TYPE);
|
return this->send_message_smart_(update, &APIConnection::try_send_update_state, UpdateStateResponse::MESSAGE_TYPE,
|
||||||
|
UpdateStateResponse::ESTIMATED_SIZE);
|
||||||
}
|
}
|
||||||
uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||||
bool is_single) {
|
bool is_single) {
|
||||||
@ -1588,7 +1605,7 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint16_t message_type) {
|
bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
|
||||||
if (!this->try_to_clear_buffer(message_type != SubscribeLogsResponse::MESSAGE_TYPE)) { // SubscribeLogsResponse
|
if (!this->try_to_clear_buffer(message_type != SubscribeLogsResponse::MESSAGE_TYPE)) { // SubscribeLogsResponse
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -1622,7 +1639,8 @@ void APIConnection::on_fatal_error() {
|
|||||||
this->flags_.remove = true;
|
this->flags_.remove = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void APIConnection::DeferredBatch::add_item(EntityBase *entity, MessageCreator creator, uint16_t message_type) {
|
void APIConnection::DeferredBatch::add_item(EntityBase *entity, MessageCreator creator, uint8_t message_type,
|
||||||
|
uint8_t estimated_size) {
|
||||||
// Check if we already have a message of this type for this entity
|
// Check if we already have a message of this type for this entity
|
||||||
// This provides deduplication per entity/message_type combination
|
// This provides deduplication per entity/message_type combination
|
||||||
// O(n) but optimized for RAM and not performance.
|
// O(n) but optimized for RAM and not performance.
|
||||||
@ -1637,12 +1655,13 @@ void APIConnection::DeferredBatch::add_item(EntityBase *entity, MessageCreator c
|
|||||||
}
|
}
|
||||||
|
|
||||||
// No existing item found, add new one
|
// No existing item found, add new one
|
||||||
items.emplace_back(entity, std::move(creator), message_type);
|
items.emplace_back(entity, std::move(creator), message_type, estimated_size);
|
||||||
}
|
}
|
||||||
|
|
||||||
void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, MessageCreator creator, uint16_t message_type) {
|
void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, MessageCreator creator, uint8_t message_type,
|
||||||
|
uint8_t estimated_size) {
|
||||||
// Insert at front for high priority messages (no deduplication check)
|
// Insert at front for high priority messages (no deduplication check)
|
||||||
items.insert(items.begin(), BatchItem(entity, std::move(creator), message_type));
|
items.insert(items.begin(), BatchItem(entity, std::move(creator), message_type, estimated_size));
|
||||||
}
|
}
|
||||||
|
|
||||||
bool APIConnection::schedule_batch_() {
|
bool APIConnection::schedule_batch_() {
|
||||||
@ -1714,7 +1733,7 @@ void APIConnection::process_batch_() {
|
|||||||
uint32_t total_estimated_size = 0;
|
uint32_t total_estimated_size = 0;
|
||||||
for (size_t i = 0; i < this->deferred_batch_.size(); i++) {
|
for (size_t i = 0; i < this->deferred_batch_.size(); i++) {
|
||||||
const auto &item = this->deferred_batch_[i];
|
const auto &item = this->deferred_batch_[i];
|
||||||
total_estimated_size += get_estimated_message_size(item.message_type);
|
total_estimated_size += item.estimated_size;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate total overhead for all messages
|
// Calculate total overhead for all messages
|
||||||
@ -1752,9 +1771,9 @@ void APIConnection::process_batch_() {
|
|||||||
|
|
||||||
// Update tracking variables
|
// Update tracking variables
|
||||||
items_processed++;
|
items_processed++;
|
||||||
// After first message, set remaining size to MAX_PACKET_SIZE to avoid fragmentation
|
// After first message, set remaining size to MAX_BATCH_PACKET_SIZE to avoid fragmentation
|
||||||
if (items_processed == 1) {
|
if (items_processed == 1) {
|
||||||
remaining_size = MAX_PACKET_SIZE;
|
remaining_size = MAX_BATCH_PACKET_SIZE;
|
||||||
}
|
}
|
||||||
remaining_size -= payload_size;
|
remaining_size -= payload_size;
|
||||||
// Calculate where the next message's header padding will start
|
// Calculate where the next message's header padding will start
|
||||||
@ -1808,7 +1827,7 @@ void APIConnection::process_batch_() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
uint16_t APIConnection::MessageCreator::operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
uint16_t APIConnection::MessageCreator::operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||||
bool is_single, uint16_t message_type) const {
|
bool is_single, uint8_t message_type) const {
|
||||||
#ifdef USE_EVENT
|
#ifdef USE_EVENT
|
||||||
// Special case: EventResponse uses string pointer
|
// Special case: EventResponse uses string pointer
|
||||||
if (message_type == EventResponse::MESSAGE_TYPE) {
|
if (message_type == EventResponse::MESSAGE_TYPE) {
|
||||||
@ -1839,149 +1858,6 @@ uint16_t APIConnection::try_send_ping_request(EntityBase *entity, APIConnection
|
|||||||
return encode_message_to_buffer(req, PingRequest::MESSAGE_TYPE, conn, remaining_size, is_single);
|
return encode_message_to_buffer(req, PingRequest::MESSAGE_TYPE, conn, remaining_size, is_single);
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t APIConnection::get_estimated_message_size(uint16_t message_type) {
|
|
||||||
// Use generated ESTIMATED_SIZE constants from each message type
|
|
||||||
switch (message_type) {
|
|
||||||
#ifdef USE_BINARY_SENSOR
|
|
||||||
case BinarySensorStateResponse::MESSAGE_TYPE:
|
|
||||||
return BinarySensorStateResponse::ESTIMATED_SIZE;
|
|
||||||
case ListEntitiesBinarySensorResponse::MESSAGE_TYPE:
|
|
||||||
return ListEntitiesBinarySensorResponse::ESTIMATED_SIZE;
|
|
||||||
#endif
|
|
||||||
#ifdef USE_SENSOR
|
|
||||||
case SensorStateResponse::MESSAGE_TYPE:
|
|
||||||
return SensorStateResponse::ESTIMATED_SIZE;
|
|
||||||
case ListEntitiesSensorResponse::MESSAGE_TYPE:
|
|
||||||
return ListEntitiesSensorResponse::ESTIMATED_SIZE;
|
|
||||||
#endif
|
|
||||||
#ifdef USE_SWITCH
|
|
||||||
case SwitchStateResponse::MESSAGE_TYPE:
|
|
||||||
return SwitchStateResponse::ESTIMATED_SIZE;
|
|
||||||
case ListEntitiesSwitchResponse::MESSAGE_TYPE:
|
|
||||||
return ListEntitiesSwitchResponse::ESTIMATED_SIZE;
|
|
||||||
#endif
|
|
||||||
#ifdef USE_TEXT_SENSOR
|
|
||||||
case TextSensorStateResponse::MESSAGE_TYPE:
|
|
||||||
return TextSensorStateResponse::ESTIMATED_SIZE;
|
|
||||||
case ListEntitiesTextSensorResponse::MESSAGE_TYPE:
|
|
||||||
return ListEntitiesTextSensorResponse::ESTIMATED_SIZE;
|
|
||||||
#endif
|
|
||||||
#ifdef USE_NUMBER
|
|
||||||
case NumberStateResponse::MESSAGE_TYPE:
|
|
||||||
return NumberStateResponse::ESTIMATED_SIZE;
|
|
||||||
case ListEntitiesNumberResponse::MESSAGE_TYPE:
|
|
||||||
return ListEntitiesNumberResponse::ESTIMATED_SIZE;
|
|
||||||
#endif
|
|
||||||
#ifdef USE_TEXT
|
|
||||||
case TextStateResponse::MESSAGE_TYPE:
|
|
||||||
return TextStateResponse::ESTIMATED_SIZE;
|
|
||||||
case ListEntitiesTextResponse::MESSAGE_TYPE:
|
|
||||||
return ListEntitiesTextResponse::ESTIMATED_SIZE;
|
|
||||||
#endif
|
|
||||||
#ifdef USE_SELECT
|
|
||||||
case SelectStateResponse::MESSAGE_TYPE:
|
|
||||||
return SelectStateResponse::ESTIMATED_SIZE;
|
|
||||||
case ListEntitiesSelectResponse::MESSAGE_TYPE:
|
|
||||||
return ListEntitiesSelectResponse::ESTIMATED_SIZE;
|
|
||||||
#endif
|
|
||||||
#ifdef USE_LOCK
|
|
||||||
case LockStateResponse::MESSAGE_TYPE:
|
|
||||||
return LockStateResponse::ESTIMATED_SIZE;
|
|
||||||
case ListEntitiesLockResponse::MESSAGE_TYPE:
|
|
||||||
return ListEntitiesLockResponse::ESTIMATED_SIZE;
|
|
||||||
#endif
|
|
||||||
#ifdef USE_EVENT
|
|
||||||
case EventResponse::MESSAGE_TYPE:
|
|
||||||
return EventResponse::ESTIMATED_SIZE;
|
|
||||||
case ListEntitiesEventResponse::MESSAGE_TYPE:
|
|
||||||
return ListEntitiesEventResponse::ESTIMATED_SIZE;
|
|
||||||
#endif
|
|
||||||
#ifdef USE_COVER
|
|
||||||
case CoverStateResponse::MESSAGE_TYPE:
|
|
||||||
return CoverStateResponse::ESTIMATED_SIZE;
|
|
||||||
case ListEntitiesCoverResponse::MESSAGE_TYPE:
|
|
||||||
return ListEntitiesCoverResponse::ESTIMATED_SIZE;
|
|
||||||
#endif
|
|
||||||
#ifdef USE_FAN
|
|
||||||
case FanStateResponse::MESSAGE_TYPE:
|
|
||||||
return FanStateResponse::ESTIMATED_SIZE;
|
|
||||||
case ListEntitiesFanResponse::MESSAGE_TYPE:
|
|
||||||
return ListEntitiesFanResponse::ESTIMATED_SIZE;
|
|
||||||
#endif
|
|
||||||
#ifdef USE_LIGHT
|
|
||||||
case LightStateResponse::MESSAGE_TYPE:
|
|
||||||
return LightStateResponse::ESTIMATED_SIZE;
|
|
||||||
case ListEntitiesLightResponse::MESSAGE_TYPE:
|
|
||||||
return ListEntitiesLightResponse::ESTIMATED_SIZE;
|
|
||||||
#endif
|
|
||||||
#ifdef USE_CLIMATE
|
|
||||||
case ClimateStateResponse::MESSAGE_TYPE:
|
|
||||||
return ClimateStateResponse::ESTIMATED_SIZE;
|
|
||||||
case ListEntitiesClimateResponse::MESSAGE_TYPE:
|
|
||||||
return ListEntitiesClimateResponse::ESTIMATED_SIZE;
|
|
||||||
#endif
|
|
||||||
#ifdef USE_ESP32_CAMERA
|
|
||||||
case ListEntitiesCameraResponse::MESSAGE_TYPE:
|
|
||||||
return ListEntitiesCameraResponse::ESTIMATED_SIZE;
|
|
||||||
#endif
|
|
||||||
#ifdef USE_BUTTON
|
|
||||||
case ListEntitiesButtonResponse::MESSAGE_TYPE:
|
|
||||||
return ListEntitiesButtonResponse::ESTIMATED_SIZE;
|
|
||||||
#endif
|
|
||||||
#ifdef USE_MEDIA_PLAYER
|
|
||||||
case MediaPlayerStateResponse::MESSAGE_TYPE:
|
|
||||||
return MediaPlayerStateResponse::ESTIMATED_SIZE;
|
|
||||||
case ListEntitiesMediaPlayerResponse::MESSAGE_TYPE:
|
|
||||||
return ListEntitiesMediaPlayerResponse::ESTIMATED_SIZE;
|
|
||||||
#endif
|
|
||||||
#ifdef USE_ALARM_CONTROL_PANEL
|
|
||||||
case AlarmControlPanelStateResponse::MESSAGE_TYPE:
|
|
||||||
return AlarmControlPanelStateResponse::ESTIMATED_SIZE;
|
|
||||||
case ListEntitiesAlarmControlPanelResponse::MESSAGE_TYPE:
|
|
||||||
return ListEntitiesAlarmControlPanelResponse::ESTIMATED_SIZE;
|
|
||||||
#endif
|
|
||||||
#ifdef USE_DATETIME_DATE
|
|
||||||
case DateStateResponse::MESSAGE_TYPE:
|
|
||||||
return DateStateResponse::ESTIMATED_SIZE;
|
|
||||||
case ListEntitiesDateResponse::MESSAGE_TYPE:
|
|
||||||
return ListEntitiesDateResponse::ESTIMATED_SIZE;
|
|
||||||
#endif
|
|
||||||
#ifdef USE_DATETIME_TIME
|
|
||||||
case TimeStateResponse::MESSAGE_TYPE:
|
|
||||||
return TimeStateResponse::ESTIMATED_SIZE;
|
|
||||||
case ListEntitiesTimeResponse::MESSAGE_TYPE:
|
|
||||||
return ListEntitiesTimeResponse::ESTIMATED_SIZE;
|
|
||||||
#endif
|
|
||||||
#ifdef USE_DATETIME_DATETIME
|
|
||||||
case DateTimeStateResponse::MESSAGE_TYPE:
|
|
||||||
return DateTimeStateResponse::ESTIMATED_SIZE;
|
|
||||||
case ListEntitiesDateTimeResponse::MESSAGE_TYPE:
|
|
||||||
return ListEntitiesDateTimeResponse::ESTIMATED_SIZE;
|
|
||||||
#endif
|
|
||||||
#ifdef USE_VALVE
|
|
||||||
case ValveStateResponse::MESSAGE_TYPE:
|
|
||||||
return ValveStateResponse::ESTIMATED_SIZE;
|
|
||||||
case ListEntitiesValveResponse::MESSAGE_TYPE:
|
|
||||||
return ListEntitiesValveResponse::ESTIMATED_SIZE;
|
|
||||||
#endif
|
|
||||||
#ifdef USE_UPDATE
|
|
||||||
case UpdateStateResponse::MESSAGE_TYPE:
|
|
||||||
return UpdateStateResponse::ESTIMATED_SIZE;
|
|
||||||
case ListEntitiesUpdateResponse::MESSAGE_TYPE:
|
|
||||||
return ListEntitiesUpdateResponse::ESTIMATED_SIZE;
|
|
||||||
#endif
|
|
||||||
case ListEntitiesServicesResponse::MESSAGE_TYPE:
|
|
||||||
return ListEntitiesServicesResponse::ESTIMATED_SIZE;
|
|
||||||
case ListEntitiesDoneResponse::MESSAGE_TYPE:
|
|
||||||
return ListEntitiesDoneResponse::ESTIMATED_SIZE;
|
|
||||||
case DisconnectRequest::MESSAGE_TYPE:
|
|
||||||
return DisconnectRequest::ESTIMATED_SIZE;
|
|
||||||
default:
|
|
||||||
// Fallback for unknown message types
|
|
||||||
return 24;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace api
|
} // namespace api
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
#endif
|
#endif
|
||||||
|
@ -33,7 +33,7 @@ class APIConnection : public APIServerConnection {
|
|||||||
|
|
||||||
bool send_list_info_done() {
|
bool send_list_info_done() {
|
||||||
return this->schedule_message_(nullptr, &APIConnection::try_send_list_info_done,
|
return this->schedule_message_(nullptr, &APIConnection::try_send_list_info_done,
|
||||||
ListEntitiesDoneResponse::MESSAGE_TYPE);
|
ListEntitiesDoneResponse::MESSAGE_TYPE, ListEntitiesDoneResponse::ESTIMATED_SIZE);
|
||||||
}
|
}
|
||||||
#ifdef USE_BINARY_SENSOR
|
#ifdef USE_BINARY_SENSOR
|
||||||
bool send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor);
|
bool send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor);
|
||||||
@ -256,7 +256,7 @@ class APIConnection : public APIServerConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool try_to_clear_buffer(bool log_out_of_space);
|
bool try_to_clear_buffer(bool log_out_of_space);
|
||||||
bool send_buffer(ProtoWriteBuffer buffer, uint16_t message_type) override;
|
bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override;
|
||||||
|
|
||||||
std::string get_client_combined_info() const {
|
std::string get_client_combined_info() const {
|
||||||
if (this->client_info_ == this->client_peername_) {
|
if (this->client_info_ == this->client_peername_) {
|
||||||
@ -298,7 +298,7 @@ class APIConnection : public APIServerConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Non-template helper to encode any ProtoMessage
|
// Non-template helper to encode any ProtoMessage
|
||||||
static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint16_t message_type, APIConnection *conn,
|
static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint8_t message_type, APIConnection *conn,
|
||||||
uint32_t remaining_size, bool is_single);
|
uint32_t remaining_size, bool is_single);
|
||||||
|
|
||||||
#ifdef USE_VOICE_ASSISTANT
|
#ifdef USE_VOICE_ASSISTANT
|
||||||
@ -443,9 +443,6 @@ class APIConnection : public APIServerConnection {
|
|||||||
static uint16_t try_send_disconnect_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
static uint16_t try_send_disconnect_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||||
bool is_single);
|
bool is_single);
|
||||||
|
|
||||||
// Helper function to get estimated message size for buffer pre-allocation
|
|
||||||
static uint16_t get_estimated_message_size(uint16_t message_type);
|
|
||||||
|
|
||||||
// Batch message method for ping requests
|
// Batch message method for ping requests
|
||||||
static uint16_t try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
static uint16_t try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||||
bool is_single);
|
bool is_single);
|
||||||
@ -505,10 +502,10 @@ class APIConnection : public APIServerConnection {
|
|||||||
|
|
||||||
// Call operator - uses message_type to determine union type
|
// Call operator - uses message_type to determine union type
|
||||||
uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single,
|
uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single,
|
||||||
uint16_t message_type) const;
|
uint8_t message_type) const;
|
||||||
|
|
||||||
// Manual cleanup method - must be called before destruction for string types
|
// Manual cleanup method - must be called before destruction for string types
|
||||||
void cleanup(uint16_t message_type) {
|
void cleanup(uint8_t message_type) {
|
||||||
#ifdef USE_EVENT
|
#ifdef USE_EVENT
|
||||||
if (message_type == EventResponse::MESSAGE_TYPE && data_.string_ptr != nullptr) {
|
if (message_type == EventResponse::MESSAGE_TYPE && data_.string_ptr != nullptr) {
|
||||||
delete data_.string_ptr;
|
delete data_.string_ptr;
|
||||||
@ -529,11 +526,12 @@ class APIConnection : public APIServerConnection {
|
|||||||
struct BatchItem {
|
struct BatchItem {
|
||||||
EntityBase *entity; // Entity pointer
|
EntityBase *entity; // Entity pointer
|
||||||
MessageCreator creator; // Function that creates the message when needed
|
MessageCreator creator; // Function that creates the message when needed
|
||||||
uint16_t message_type; // Message type for overhead calculation
|
uint8_t message_type; // Message type for overhead calculation (max 255)
|
||||||
|
uint8_t estimated_size; // Estimated message size (max 255 bytes)
|
||||||
|
|
||||||
// Constructor for creating BatchItem
|
// Constructor for creating BatchItem
|
||||||
BatchItem(EntityBase *entity, MessageCreator creator, uint16_t message_type)
|
BatchItem(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size)
|
||||||
: entity(entity), creator(std::move(creator)), message_type(message_type) {}
|
: entity(entity), creator(std::move(creator)), message_type(message_type), estimated_size(estimated_size) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
std::vector<BatchItem> items;
|
std::vector<BatchItem> items;
|
||||||
@ -559,9 +557,9 @@ class APIConnection : public APIServerConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add item to the batch
|
// Add item to the batch
|
||||||
void add_item(EntityBase *entity, MessageCreator creator, uint16_t message_type);
|
void add_item(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size);
|
||||||
// Add item to the front of the batch (for high priority messages like ping)
|
// Add item to the front of the batch (for high priority messages like ping)
|
||||||
void add_item_front(EntityBase *entity, MessageCreator creator, uint16_t message_type);
|
void add_item_front(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size);
|
||||||
|
|
||||||
// Clear all items with proper cleanup
|
// Clear all items with proper cleanup
|
||||||
void clear() {
|
void clear() {
|
||||||
@ -630,7 +628,7 @@ class APIConnection : public APIServerConnection {
|
|||||||
// to send in one go. This is the maximum size of a single packet
|
// to send in one go. This is the maximum size of a single packet
|
||||||
// that can be sent over the network.
|
// that can be sent over the network.
|
||||||
// This is to avoid fragmentation of the packet.
|
// This is to avoid fragmentation of the packet.
|
||||||
static constexpr size_t MAX_PACKET_SIZE = 1390; // MTU
|
static constexpr size_t MAX_BATCH_PACKET_SIZE = 1390; // MTU
|
||||||
|
|
||||||
bool schedule_batch_();
|
bool schedule_batch_();
|
||||||
void process_batch_();
|
void process_batch_();
|
||||||
@ -641,9 +639,9 @@ class APIConnection : public APIServerConnection {
|
|||||||
|
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
// Helper to log a proto message from a MessageCreator object
|
// Helper to log a proto message from a MessageCreator object
|
||||||
void log_proto_message_(EntityBase *entity, const MessageCreator &creator, uint16_t message_type) {
|
void log_proto_message_(EntityBase *entity, const MessageCreator &creator, uint8_t message_type) {
|
||||||
this->flags_.log_only_mode = true;
|
this->flags_.log_only_mode = true;
|
||||||
creator(entity, this, MAX_PACKET_SIZE, true, message_type);
|
creator(entity, this, MAX_BATCH_PACKET_SIZE, true, message_type);
|
||||||
this->flags_.log_only_mode = false;
|
this->flags_.log_only_mode = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -654,7 +652,8 @@ class APIConnection : public APIServerConnection {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Helper method to send a message either immediately or via batching
|
// Helper method to send a message either immediately or via batching
|
||||||
bool send_message_smart_(EntityBase *entity, MessageCreatorPtr creator, uint16_t message_type) {
|
bool send_message_smart_(EntityBase *entity, MessageCreatorPtr creator, uint8_t message_type,
|
||||||
|
uint8_t estimated_size) {
|
||||||
// Try to send immediately if:
|
// Try to send immediately if:
|
||||||
// 1. We should try to send immediately (should_try_send_immediately = true)
|
// 1. We should try to send immediately (should_try_send_immediately = true)
|
||||||
// 2. Batch delay is 0 (user has opted in to immediate sending)
|
// 2. Batch delay is 0 (user has opted in to immediate sending)
|
||||||
@ -662,7 +661,7 @@ class APIConnection : public APIServerConnection {
|
|||||||
if (this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0 &&
|
if (this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0 &&
|
||||||
this->helper_->can_write_without_blocking()) {
|
this->helper_->can_write_without_blocking()) {
|
||||||
// Now actually encode and send
|
// Now actually encode and send
|
||||||
if (creator(entity, this, MAX_PACKET_SIZE, true) &&
|
if (creator(entity, this, MAX_BATCH_PACKET_SIZE, true) &&
|
||||||
this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) {
|
this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) {
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
// Log the message in verbose mode
|
// Log the message in verbose mode
|
||||||
@ -675,23 +674,25 @@ class APIConnection : public APIServerConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fall back to scheduled batching
|
// Fall back to scheduled batching
|
||||||
return this->schedule_message_(entity, creator, message_type);
|
return this->schedule_message_(entity, creator, message_type, estimated_size);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to schedule a deferred message with known message type
|
// Helper function to schedule a deferred message with known message type
|
||||||
bool schedule_message_(EntityBase *entity, MessageCreator creator, uint16_t message_type) {
|
bool schedule_message_(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) {
|
||||||
this->deferred_batch_.add_item(entity, std::move(creator), message_type);
|
this->deferred_batch_.add_item(entity, std::move(creator), message_type, estimated_size);
|
||||||
return this->schedule_batch_();
|
return this->schedule_batch_();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overload for function pointers (for info messages and current state reads)
|
// Overload for function pointers (for info messages and current state reads)
|
||||||
bool schedule_message_(EntityBase *entity, MessageCreatorPtr function_ptr, uint16_t message_type) {
|
bool schedule_message_(EntityBase *entity, MessageCreatorPtr function_ptr, uint8_t message_type,
|
||||||
return schedule_message_(entity, MessageCreator(function_ptr), message_type);
|
uint8_t estimated_size) {
|
||||||
|
return schedule_message_(entity, MessageCreator(function_ptr), message_type, estimated_size);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to schedule a high priority message at the front of the batch
|
// Helper function to schedule a high priority message at the front of the batch
|
||||||
bool schedule_message_front_(EntityBase *entity, MessageCreatorPtr function_ptr, uint16_t message_type) {
|
bool schedule_message_front_(EntityBase *entity, MessageCreatorPtr function_ptr, uint8_t message_type,
|
||||||
this->deferred_batch_.add_item_front(entity, MessageCreator(function_ptr), message_type);
|
uint8_t estimated_size) {
|
||||||
|
this->deferred_batch_.add_item_front(entity, MessageCreator(function_ptr), message_type, estimated_size);
|
||||||
return this->schedule_batch_();
|
return this->schedule_batch_();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -613,11 +613,13 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
|||||||
buffer->type = type;
|
buffer->type = type;
|
||||||
return APIError::OK;
|
return APIError::OK;
|
||||||
}
|
}
|
||||||
APIError APINoiseFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) {
|
APIError APINoiseFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
|
||||||
// Resize to include MAC space (required for Noise encryption)
|
// Resize to include MAC space (required for Noise encryption)
|
||||||
buffer.get_buffer()->resize(buffer.get_buffer()->size() + frame_footer_size_);
|
buffer.get_buffer()->resize(buffer.get_buffer()->size() + frame_footer_size_);
|
||||||
PacketInfo packet{type, 0,
|
uint16_t payload_size =
|
||||||
static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)};
|
static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_);
|
||||||
|
|
||||||
|
PacketInfo packet{type, 0, payload_size};
|
||||||
return write_protobuf_packets(buffer, std::span<const PacketInfo>(&packet, 1));
|
return write_protobuf_packets(buffer, std::span<const PacketInfo>(&packet, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1002,8 +1004,10 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
|||||||
buffer->type = rx_header_parsed_type_;
|
buffer->type = rx_header_parsed_type_;
|
||||||
return APIError::OK;
|
return APIError::OK;
|
||||||
}
|
}
|
||||||
APIError APIPlaintextFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) {
|
APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
|
||||||
PacketInfo packet{type, 0, static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_)};
|
uint16_t payload_size = static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_);
|
||||||
|
|
||||||
|
PacketInfo packet{type, 0, payload_size};
|
||||||
return write_protobuf_packets(buffer, std::span<const PacketInfo>(&packet, 1));
|
return write_protobuf_packets(buffer, std::span<const PacketInfo>(&packet, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,13 +30,11 @@ struct ReadPacketBuffer {
|
|||||||
|
|
||||||
// Packed packet info structure to minimize memory usage
|
// Packed packet info structure to minimize memory usage
|
||||||
struct PacketInfo {
|
struct PacketInfo {
|
||||||
uint16_t message_type; // 2 bytes
|
uint16_t offset; // Offset in buffer where message starts
|
||||||
uint16_t offset; // 2 bytes (sufficient for packet size ~1460 bytes)
|
uint16_t payload_size; // Size of the message payload
|
||||||
uint16_t payload_size; // 2 bytes (up to 65535 bytes)
|
uint8_t message_type; // Message type (0-255)
|
||||||
uint16_t padding; // 2 byte (for alignment)
|
|
||||||
|
|
||||||
PacketInfo(uint16_t type, uint16_t off, uint16_t size)
|
PacketInfo(uint8_t type, uint16_t off, uint16_t size) : offset(off), payload_size(size), message_type(type) {}
|
||||||
: message_type(type), offset(off), payload_size(size), padding(0) {}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class APIError : uint16_t {
|
enum class APIError : uint16_t {
|
||||||
@ -98,7 +96,7 @@ class APIFrameHelper {
|
|||||||
}
|
}
|
||||||
// Give this helper a name for logging
|
// Give this helper a name for logging
|
||||||
void set_log_info(std::string info) { info_ = std::move(info); }
|
void set_log_info(std::string info) { info_ = std::move(info); }
|
||||||
virtual APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) = 0;
|
virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0;
|
||||||
// Write multiple protobuf packets in a single operation
|
// Write multiple protobuf packets in a single operation
|
||||||
// packets contains (message_type, offset, length) for each message in the buffer
|
// packets contains (message_type, offset, length) for each message in the buffer
|
||||||
// The buffer contains all messages with appropriate padding before each
|
// The buffer contains all messages with appropriate padding before each
|
||||||
@ -197,7 +195,7 @@ class APINoiseFrameHelper : public APIFrameHelper {
|
|||||||
APIError init() override;
|
APIError init() override;
|
||||||
APIError loop() override;
|
APIError loop() override;
|
||||||
APIError read_packet(ReadPacketBuffer *buffer) override;
|
APIError read_packet(ReadPacketBuffer *buffer) override;
|
||||||
APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) override;
|
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
|
||||||
APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override;
|
APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override;
|
||||||
// Get the frame header padding required by this protocol
|
// Get the frame header padding required by this protocol
|
||||||
uint8_t frame_header_padding() override { return frame_header_padding_; }
|
uint8_t frame_header_padding() override { return frame_header_padding_; }
|
||||||
@ -251,7 +249,7 @@ class APIPlaintextFrameHelper : public APIFrameHelper {
|
|||||||
APIError init() override;
|
APIError init() override;
|
||||||
APIError loop() override;
|
APIError loop() override;
|
||||||
APIError read_packet(ReadPacketBuffer *buffer) override;
|
APIError read_packet(ReadPacketBuffer *buffer) override;
|
||||||
APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) override;
|
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
|
||||||
APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override;
|
APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override;
|
||||||
uint8_t frame_header_padding() override { return frame_header_padding_; }
|
uint8_t frame_header_padding() override { return frame_header_padding_; }
|
||||||
// Get the frame footer size required by this protocol
|
// Get the frame footer size required by this protocol
|
||||||
|
@ -623,6 +623,10 @@ bool CoverCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
|||||||
this->stop = value.as_bool();
|
this->stop = value.as_bool();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
case 9: {
|
||||||
|
this->device_id = value.as_uint32();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -654,6 +658,7 @@ void CoverCommandRequest::encode(ProtoWriteBuffer buffer) const {
|
|||||||
buffer.encode_bool(6, this->has_tilt);
|
buffer.encode_bool(6, this->has_tilt);
|
||||||
buffer.encode_float(7, this->tilt);
|
buffer.encode_float(7, this->tilt);
|
||||||
buffer.encode_bool(8, this->stop);
|
buffer.encode_bool(8, this->stop);
|
||||||
|
buffer.encode_uint32(9, this->device_id);
|
||||||
}
|
}
|
||||||
void CoverCommandRequest::calculate_size(uint32_t &total_size) const {
|
void CoverCommandRequest::calculate_size(uint32_t &total_size) const {
|
||||||
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
|
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
|
||||||
@ -664,6 +669,7 @@ void CoverCommandRequest::calculate_size(uint32_t &total_size) const {
|
|||||||
ProtoSize::add_bool_field(total_size, 1, this->has_tilt, false);
|
ProtoSize::add_bool_field(total_size, 1, this->has_tilt, false);
|
||||||
ProtoSize::add_fixed_field<4>(total_size, 1, this->tilt != 0.0f, false);
|
ProtoSize::add_fixed_field<4>(total_size, 1, this->tilt != 0.0f, false);
|
||||||
ProtoSize::add_bool_field(total_size, 1, this->stop, false);
|
ProtoSize::add_bool_field(total_size, 1, this->stop, false);
|
||||||
|
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_FAN
|
#ifdef USE_FAN
|
||||||
@ -889,6 +895,10 @@ bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
|||||||
this->has_preset_mode = value.as_bool();
|
this->has_preset_mode = value.as_bool();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
case 14: {
|
||||||
|
this->device_id = value.as_uint32();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -927,6 +937,7 @@ void FanCommandRequest::encode(ProtoWriteBuffer buffer) const {
|
|||||||
buffer.encode_int32(11, this->speed_level);
|
buffer.encode_int32(11, this->speed_level);
|
||||||
buffer.encode_bool(12, this->has_preset_mode);
|
buffer.encode_bool(12, this->has_preset_mode);
|
||||||
buffer.encode_string(13, this->preset_mode);
|
buffer.encode_string(13, this->preset_mode);
|
||||||
|
buffer.encode_uint32(14, this->device_id);
|
||||||
}
|
}
|
||||||
void FanCommandRequest::calculate_size(uint32_t &total_size) const {
|
void FanCommandRequest::calculate_size(uint32_t &total_size) const {
|
||||||
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
|
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
|
||||||
@ -942,6 +953,7 @@ void FanCommandRequest::calculate_size(uint32_t &total_size) const {
|
|||||||
ProtoSize::add_int32_field(total_size, 1, this->speed_level, false);
|
ProtoSize::add_int32_field(total_size, 1, this->speed_level, false);
|
||||||
ProtoSize::add_bool_field(total_size, 1, this->has_preset_mode, false);
|
ProtoSize::add_bool_field(total_size, 1, this->has_preset_mode, false);
|
||||||
ProtoSize::add_string_field(total_size, 1, this->preset_mode, false);
|
ProtoSize::add_string_field(total_size, 1, this->preset_mode, false);
|
||||||
|
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_LIGHT
|
#ifdef USE_LIGHT
|
||||||
@ -1247,6 +1259,10 @@ bool LightCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
|||||||
this->has_effect = value.as_bool();
|
this->has_effect = value.as_bool();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
case 28: {
|
||||||
|
this->device_id = value.as_uint32();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -1335,6 +1351,7 @@ void LightCommandRequest::encode(ProtoWriteBuffer buffer) const {
|
|||||||
buffer.encode_uint32(17, this->flash_length);
|
buffer.encode_uint32(17, this->flash_length);
|
||||||
buffer.encode_bool(18, this->has_effect);
|
buffer.encode_bool(18, this->has_effect);
|
||||||
buffer.encode_string(19, this->effect);
|
buffer.encode_string(19, this->effect);
|
||||||
|
buffer.encode_uint32(28, this->device_id);
|
||||||
}
|
}
|
||||||
void LightCommandRequest::calculate_size(uint32_t &total_size) const {
|
void LightCommandRequest::calculate_size(uint32_t &total_size) const {
|
||||||
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
|
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
|
||||||
@ -1364,6 +1381,7 @@ void LightCommandRequest::calculate_size(uint32_t &total_size) const {
|
|||||||
ProtoSize::add_uint32_field(total_size, 2, this->flash_length, false);
|
ProtoSize::add_uint32_field(total_size, 2, this->flash_length, false);
|
||||||
ProtoSize::add_bool_field(total_size, 2, this->has_effect, false);
|
ProtoSize::add_bool_field(total_size, 2, this->has_effect, false);
|
||||||
ProtoSize::add_string_field(total_size, 2, this->effect, false);
|
ProtoSize::add_string_field(total_size, 2, this->effect, false);
|
||||||
|
ProtoSize::add_uint32_field(total_size, 2, this->device_id, false);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_SENSOR
|
#ifdef USE_SENSOR
|
||||||
@ -1637,6 +1655,10 @@ bool SwitchCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
|||||||
this->state = value.as_bool();
|
this->state = value.as_bool();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
case 3: {
|
||||||
|
this->device_id = value.as_uint32();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -1654,10 +1676,12 @@ bool SwitchCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
|
|||||||
void SwitchCommandRequest::encode(ProtoWriteBuffer buffer) const {
|
void SwitchCommandRequest::encode(ProtoWriteBuffer buffer) const {
|
||||||
buffer.encode_fixed32(1, this->key);
|
buffer.encode_fixed32(1, this->key);
|
||||||
buffer.encode_bool(2, this->state);
|
buffer.encode_bool(2, this->state);
|
||||||
|
buffer.encode_uint32(3, this->device_id);
|
||||||
}
|
}
|
||||||
void SwitchCommandRequest::calculate_size(uint32_t &total_size) const {
|
void SwitchCommandRequest::calculate_size(uint32_t &total_size) const {
|
||||||
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
|
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
|
||||||
ProtoSize::add_bool_field(total_size, 1, this->state, false);
|
ProtoSize::add_bool_field(total_size, 1, this->state, false);
|
||||||
|
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_TEXT_SENSOR
|
#ifdef USE_TEXT_SENSOR
|
||||||
@ -2293,6 +2317,10 @@ bool CameraImageResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
|||||||
this->done = value.as_bool();
|
this->done = value.as_bool();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
case 4: {
|
||||||
|
this->device_id = value.as_uint32();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -2321,11 +2349,13 @@ void CameraImageResponse::encode(ProtoWriteBuffer buffer) const {
|
|||||||
buffer.encode_fixed32(1, this->key);
|
buffer.encode_fixed32(1, this->key);
|
||||||
buffer.encode_bytes(2, reinterpret_cast<const uint8_t *>(this->data.data()), this->data.size());
|
buffer.encode_bytes(2, reinterpret_cast<const uint8_t *>(this->data.data()), this->data.size());
|
||||||
buffer.encode_bool(3, this->done);
|
buffer.encode_bool(3, this->done);
|
||||||
|
buffer.encode_uint32(4, this->device_id);
|
||||||
}
|
}
|
||||||
void CameraImageResponse::calculate_size(uint32_t &total_size) const {
|
void CameraImageResponse::calculate_size(uint32_t &total_size) const {
|
||||||
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
|
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
|
||||||
ProtoSize::add_string_field(total_size, 1, this->data, false);
|
ProtoSize::add_string_field(total_size, 1, this->data, false);
|
||||||
ProtoSize::add_bool_field(total_size, 1, this->done, false);
|
ProtoSize::add_bool_field(total_size, 1, this->done, false);
|
||||||
|
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
|
||||||
}
|
}
|
||||||
bool CameraImageRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
bool CameraImageRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||||
switch (field_id) {
|
switch (field_id) {
|
||||||
@ -2749,6 +2779,10 @@ bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value)
|
|||||||
this->has_target_humidity = value.as_bool();
|
this->has_target_humidity = value.as_bool();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
case 24: {
|
||||||
|
this->device_id = value.as_uint32();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -2817,6 +2851,7 @@ void ClimateCommandRequest::encode(ProtoWriteBuffer buffer) const {
|
|||||||
buffer.encode_string(21, this->custom_preset);
|
buffer.encode_string(21, this->custom_preset);
|
||||||
buffer.encode_bool(22, this->has_target_humidity);
|
buffer.encode_bool(22, this->has_target_humidity);
|
||||||
buffer.encode_float(23, this->target_humidity);
|
buffer.encode_float(23, this->target_humidity);
|
||||||
|
buffer.encode_uint32(24, this->device_id);
|
||||||
}
|
}
|
||||||
void ClimateCommandRequest::calculate_size(uint32_t &total_size) const {
|
void ClimateCommandRequest::calculate_size(uint32_t &total_size) const {
|
||||||
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
|
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
|
||||||
@ -2842,6 +2877,7 @@ void ClimateCommandRequest::calculate_size(uint32_t &total_size) const {
|
|||||||
ProtoSize::add_string_field(total_size, 2, this->custom_preset, false);
|
ProtoSize::add_string_field(total_size, 2, this->custom_preset, false);
|
||||||
ProtoSize::add_bool_field(total_size, 2, this->has_target_humidity, false);
|
ProtoSize::add_bool_field(total_size, 2, this->has_target_humidity, false);
|
||||||
ProtoSize::add_fixed_field<4>(total_size, 2, this->target_humidity != 0.0f, false);
|
ProtoSize::add_fixed_field<4>(total_size, 2, this->target_humidity != 0.0f, false);
|
||||||
|
ProtoSize::add_uint32_field(total_size, 2, this->device_id, false);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_NUMBER
|
#ifdef USE_NUMBER
|
||||||
@ -2991,6 +3027,16 @@ void NumberStateResponse::calculate_size(uint32_t &total_size) const {
|
|||||||
ProtoSize::add_bool_field(total_size, 1, this->missing_state, false);
|
ProtoSize::add_bool_field(total_size, 1, this->missing_state, false);
|
||||||
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
|
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
|
||||||
}
|
}
|
||||||
|
bool NumberCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||||
|
switch (field_id) {
|
||||||
|
case 3: {
|
||||||
|
this->device_id = value.as_uint32();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
bool NumberCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
|
bool NumberCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
|
||||||
switch (field_id) {
|
switch (field_id) {
|
||||||
case 1: {
|
case 1: {
|
||||||
@ -3008,10 +3054,12 @@ bool NumberCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
|
|||||||
void NumberCommandRequest::encode(ProtoWriteBuffer buffer) const {
|
void NumberCommandRequest::encode(ProtoWriteBuffer buffer) const {
|
||||||
buffer.encode_fixed32(1, this->key);
|
buffer.encode_fixed32(1, this->key);
|
||||||
buffer.encode_float(2, this->state);
|
buffer.encode_float(2, this->state);
|
||||||
|
buffer.encode_uint32(3, this->device_id);
|
||||||
}
|
}
|
||||||
void NumberCommandRequest::calculate_size(uint32_t &total_size) const {
|
void NumberCommandRequest::calculate_size(uint32_t &total_size) const {
|
||||||
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
|
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
|
||||||
ProtoSize::add_fixed_field<4>(total_size, 1, this->state != 0.0f, false);
|
ProtoSize::add_fixed_field<4>(total_size, 1, this->state != 0.0f, false);
|
||||||
|
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_SELECT
|
#ifdef USE_SELECT
|
||||||
@ -3143,6 +3191,16 @@ void SelectStateResponse::calculate_size(uint32_t &total_size) const {
|
|||||||
ProtoSize::add_bool_field(total_size, 1, this->missing_state, false);
|
ProtoSize::add_bool_field(total_size, 1, this->missing_state, false);
|
||||||
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
|
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
|
||||||
}
|
}
|
||||||
|
bool SelectCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||||
|
switch (field_id) {
|
||||||
|
case 3: {
|
||||||
|
this->device_id = value.as_uint32();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
bool SelectCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
bool SelectCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||||
switch (field_id) {
|
switch (field_id) {
|
||||||
case 2: {
|
case 2: {
|
||||||
@ -3166,10 +3224,12 @@ bool SelectCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
|
|||||||
void SelectCommandRequest::encode(ProtoWriteBuffer buffer) const {
|
void SelectCommandRequest::encode(ProtoWriteBuffer buffer) const {
|
||||||
buffer.encode_fixed32(1, this->key);
|
buffer.encode_fixed32(1, this->key);
|
||||||
buffer.encode_string(2, this->state);
|
buffer.encode_string(2, this->state);
|
||||||
|
buffer.encode_uint32(3, this->device_id);
|
||||||
}
|
}
|
||||||
void SelectCommandRequest::calculate_size(uint32_t &total_size) const {
|
void SelectCommandRequest::calculate_size(uint32_t &total_size) const {
|
||||||
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
|
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
|
||||||
ProtoSize::add_string_field(total_size, 1, this->state, false);
|
ProtoSize::add_string_field(total_size, 1, this->state, false);
|
||||||
|
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_SIREN
|
#ifdef USE_SIREN
|
||||||
@ -3327,6 +3387,10 @@ bool SirenCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
|||||||
this->has_volume = value.as_bool();
|
this->has_volume = value.as_bool();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
case 10: {
|
||||||
|
this->device_id = value.as_uint32();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -3365,6 +3429,7 @@ void SirenCommandRequest::encode(ProtoWriteBuffer buffer) const {
|
|||||||
buffer.encode_uint32(7, this->duration);
|
buffer.encode_uint32(7, this->duration);
|
||||||
buffer.encode_bool(8, this->has_volume);
|
buffer.encode_bool(8, this->has_volume);
|
||||||
buffer.encode_float(9, this->volume);
|
buffer.encode_float(9, this->volume);
|
||||||
|
buffer.encode_uint32(10, this->device_id);
|
||||||
}
|
}
|
||||||
void SirenCommandRequest::calculate_size(uint32_t &total_size) const {
|
void SirenCommandRequest::calculate_size(uint32_t &total_size) const {
|
||||||
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
|
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
|
||||||
@ -3376,6 +3441,7 @@ void SirenCommandRequest::calculate_size(uint32_t &total_size) const {
|
|||||||
ProtoSize::add_uint32_field(total_size, 1, this->duration, false);
|
ProtoSize::add_uint32_field(total_size, 1, this->duration, false);
|
||||||
ProtoSize::add_bool_field(total_size, 1, this->has_volume, false);
|
ProtoSize::add_bool_field(total_size, 1, this->has_volume, false);
|
||||||
ProtoSize::add_fixed_field<4>(total_size, 1, this->volume != 0.0f, false);
|
ProtoSize::add_fixed_field<4>(total_size, 1, this->volume != 0.0f, false);
|
||||||
|
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_LOCK
|
#ifdef USE_LOCK
|
||||||
@ -3517,6 +3583,10 @@ bool LockCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
|||||||
this->has_code = value.as_bool();
|
this->has_code = value.as_bool();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
case 5: {
|
||||||
|
this->device_id = value.as_uint32();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -3546,12 +3616,14 @@ void LockCommandRequest::encode(ProtoWriteBuffer buffer) const {
|
|||||||
buffer.encode_enum<enums::LockCommand>(2, this->command);
|
buffer.encode_enum<enums::LockCommand>(2, this->command);
|
||||||
buffer.encode_bool(3, this->has_code);
|
buffer.encode_bool(3, this->has_code);
|
||||||
buffer.encode_string(4, this->code);
|
buffer.encode_string(4, this->code);
|
||||||
|
buffer.encode_uint32(5, this->device_id);
|
||||||
}
|
}
|
||||||
void LockCommandRequest::calculate_size(uint32_t &total_size) const {
|
void LockCommandRequest::calculate_size(uint32_t &total_size) const {
|
||||||
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
|
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
|
||||||
ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->command), false);
|
ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->command), false);
|
||||||
ProtoSize::add_bool_field(total_size, 1, this->has_code, false);
|
ProtoSize::add_bool_field(total_size, 1, this->has_code, false);
|
||||||
ProtoSize::add_string_field(total_size, 1, this->code, false);
|
ProtoSize::add_string_field(total_size, 1, this->code, false);
|
||||||
|
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_BUTTON
|
#ifdef USE_BUTTON
|
||||||
@ -3631,6 +3703,16 @@ void ListEntitiesButtonResponse::calculate_size(uint32_t &total_size) const {
|
|||||||
ProtoSize::add_string_field(total_size, 1, this->device_class, false);
|
ProtoSize::add_string_field(total_size, 1, this->device_class, false);
|
||||||
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
|
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
|
||||||
}
|
}
|
||||||
|
bool ButtonCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||||
|
switch (field_id) {
|
||||||
|
case 2: {
|
||||||
|
this->device_id = value.as_uint32();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
bool ButtonCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
|
bool ButtonCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
|
||||||
switch (field_id) {
|
switch (field_id) {
|
||||||
case 1: {
|
case 1: {
|
||||||
@ -3641,9 +3723,13 @@ bool ButtonCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
void ButtonCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); }
|
void ButtonCommandRequest::encode(ProtoWriteBuffer buffer) const {
|
||||||
|
buffer.encode_fixed32(1, this->key);
|
||||||
|
buffer.encode_uint32(2, this->device_id);
|
||||||
|
}
|
||||||
void ButtonCommandRequest::calculate_size(uint32_t &total_size) const {
|
void ButtonCommandRequest::calculate_size(uint32_t &total_size) const {
|
||||||
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
|
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
|
||||||
|
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_MEDIA_PLAYER
|
#ifdef USE_MEDIA_PLAYER
|
||||||
@ -3849,6 +3935,10 @@ bool MediaPlayerCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt val
|
|||||||
this->announcement = value.as_bool();
|
this->announcement = value.as_bool();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
case 10: {
|
||||||
|
this->device_id = value.as_uint32();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -3887,6 +3977,7 @@ void MediaPlayerCommandRequest::encode(ProtoWriteBuffer buffer) const {
|
|||||||
buffer.encode_string(7, this->media_url);
|
buffer.encode_string(7, this->media_url);
|
||||||
buffer.encode_bool(8, this->has_announcement);
|
buffer.encode_bool(8, this->has_announcement);
|
||||||
buffer.encode_bool(9, this->announcement);
|
buffer.encode_bool(9, this->announcement);
|
||||||
|
buffer.encode_uint32(10, this->device_id);
|
||||||
}
|
}
|
||||||
void MediaPlayerCommandRequest::calculate_size(uint32_t &total_size) const {
|
void MediaPlayerCommandRequest::calculate_size(uint32_t &total_size) const {
|
||||||
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
|
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
|
||||||
@ -3898,6 +3989,7 @@ void MediaPlayerCommandRequest::calculate_size(uint32_t &total_size) const {
|
|||||||
ProtoSize::add_string_field(total_size, 1, this->media_url, false);
|
ProtoSize::add_string_field(total_size, 1, this->media_url, false);
|
||||||
ProtoSize::add_bool_field(total_size, 1, this->has_announcement, false);
|
ProtoSize::add_bool_field(total_size, 1, this->has_announcement, false);
|
||||||
ProtoSize::add_bool_field(total_size, 1, this->announcement, false);
|
ProtoSize::add_bool_field(total_size, 1, this->announcement, false);
|
||||||
|
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_BLUETOOTH_PROXY
|
#ifdef USE_BLUETOOTH_PROXY
|
||||||
@ -5311,6 +5403,10 @@ bool AlarmControlPanelCommandRequest::decode_varint(uint32_t field_id, ProtoVarI
|
|||||||
this->command = value.as_enum<enums::AlarmControlPanelStateCommand>();
|
this->command = value.as_enum<enums::AlarmControlPanelStateCommand>();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
case 4: {
|
||||||
|
this->device_id = value.as_uint32();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -5339,11 +5435,13 @@ void AlarmControlPanelCommandRequest::encode(ProtoWriteBuffer buffer) const {
|
|||||||
buffer.encode_fixed32(1, this->key);
|
buffer.encode_fixed32(1, this->key);
|
||||||
buffer.encode_enum<enums::AlarmControlPanelStateCommand>(2, this->command);
|
buffer.encode_enum<enums::AlarmControlPanelStateCommand>(2, this->command);
|
||||||
buffer.encode_string(3, this->code);
|
buffer.encode_string(3, this->code);
|
||||||
|
buffer.encode_uint32(4, this->device_id);
|
||||||
}
|
}
|
||||||
void AlarmControlPanelCommandRequest::calculate_size(uint32_t &total_size) const {
|
void AlarmControlPanelCommandRequest::calculate_size(uint32_t &total_size) const {
|
||||||
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
|
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
|
||||||
ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->command), false);
|
ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->command), false);
|
||||||
ProtoSize::add_string_field(total_size, 1, this->code, false);
|
ProtoSize::add_string_field(total_size, 1, this->code, false);
|
||||||
|
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_TEXT
|
#ifdef USE_TEXT
|
||||||
@ -5487,6 +5585,16 @@ void TextStateResponse::calculate_size(uint32_t &total_size) const {
|
|||||||
ProtoSize::add_bool_field(total_size, 1, this->missing_state, false);
|
ProtoSize::add_bool_field(total_size, 1, this->missing_state, false);
|
||||||
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
|
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
|
||||||
}
|
}
|
||||||
|
bool TextCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||||
|
switch (field_id) {
|
||||||
|
case 3: {
|
||||||
|
this->device_id = value.as_uint32();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
bool TextCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
bool TextCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||||
switch (field_id) {
|
switch (field_id) {
|
||||||
case 2: {
|
case 2: {
|
||||||
@ -5510,10 +5618,12 @@ bool TextCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
|
|||||||
void TextCommandRequest::encode(ProtoWriteBuffer buffer) const {
|
void TextCommandRequest::encode(ProtoWriteBuffer buffer) const {
|
||||||
buffer.encode_fixed32(1, this->key);
|
buffer.encode_fixed32(1, this->key);
|
||||||
buffer.encode_string(2, this->state);
|
buffer.encode_string(2, this->state);
|
||||||
|
buffer.encode_uint32(3, this->device_id);
|
||||||
}
|
}
|
||||||
void TextCommandRequest::calculate_size(uint32_t &total_size) const {
|
void TextCommandRequest::calculate_size(uint32_t &total_size) const {
|
||||||
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
|
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
|
||||||
ProtoSize::add_string_field(total_size, 1, this->state, false);
|
ProtoSize::add_string_field(total_size, 1, this->state, false);
|
||||||
|
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_DATETIME_DATE
|
#ifdef USE_DATETIME_DATE
|
||||||
@ -5653,6 +5763,10 @@ bool DateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
|||||||
this->day = value.as_uint32();
|
this->day = value.as_uint32();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
case 5: {
|
||||||
|
this->device_id = value.as_uint32();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -5672,12 +5786,14 @@ void DateCommandRequest::encode(ProtoWriteBuffer buffer) const {
|
|||||||
buffer.encode_uint32(2, this->year);
|
buffer.encode_uint32(2, this->year);
|
||||||
buffer.encode_uint32(3, this->month);
|
buffer.encode_uint32(3, this->month);
|
||||||
buffer.encode_uint32(4, this->day);
|
buffer.encode_uint32(4, this->day);
|
||||||
|
buffer.encode_uint32(5, this->device_id);
|
||||||
}
|
}
|
||||||
void DateCommandRequest::calculate_size(uint32_t &total_size) const {
|
void DateCommandRequest::calculate_size(uint32_t &total_size) const {
|
||||||
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
|
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
|
||||||
ProtoSize::add_uint32_field(total_size, 1, this->year, false);
|
ProtoSize::add_uint32_field(total_size, 1, this->year, false);
|
||||||
ProtoSize::add_uint32_field(total_size, 1, this->month, false);
|
ProtoSize::add_uint32_field(total_size, 1, this->month, false);
|
||||||
ProtoSize::add_uint32_field(total_size, 1, this->day, false);
|
ProtoSize::add_uint32_field(total_size, 1, this->day, false);
|
||||||
|
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_DATETIME_TIME
|
#ifdef USE_DATETIME_TIME
|
||||||
@ -5817,6 +5933,10 @@ bool TimeCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
|||||||
this->second = value.as_uint32();
|
this->second = value.as_uint32();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
case 5: {
|
||||||
|
this->device_id = value.as_uint32();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -5836,12 +5956,14 @@ void TimeCommandRequest::encode(ProtoWriteBuffer buffer) const {
|
|||||||
buffer.encode_uint32(2, this->hour);
|
buffer.encode_uint32(2, this->hour);
|
||||||
buffer.encode_uint32(3, this->minute);
|
buffer.encode_uint32(3, this->minute);
|
||||||
buffer.encode_uint32(4, this->second);
|
buffer.encode_uint32(4, this->second);
|
||||||
|
buffer.encode_uint32(5, this->device_id);
|
||||||
}
|
}
|
||||||
void TimeCommandRequest::calculate_size(uint32_t &total_size) const {
|
void TimeCommandRequest::calculate_size(uint32_t &total_size) const {
|
||||||
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
|
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
|
||||||
ProtoSize::add_uint32_field(total_size, 1, this->hour, false);
|
ProtoSize::add_uint32_field(total_size, 1, this->hour, false);
|
||||||
ProtoSize::add_uint32_field(total_size, 1, this->minute, false);
|
ProtoSize::add_uint32_field(total_size, 1, this->minute, false);
|
||||||
ProtoSize::add_uint32_field(total_size, 1, this->second, false);
|
ProtoSize::add_uint32_field(total_size, 1, this->second, false);
|
||||||
|
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_EVENT
|
#ifdef USE_EVENT
|
||||||
@ -6119,6 +6241,10 @@ bool ValveCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
|||||||
this->stop = value.as_bool();
|
this->stop = value.as_bool();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
case 5: {
|
||||||
|
this->device_id = value.as_uint32();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -6142,12 +6268,14 @@ void ValveCommandRequest::encode(ProtoWriteBuffer buffer) const {
|
|||||||
buffer.encode_bool(2, this->has_position);
|
buffer.encode_bool(2, this->has_position);
|
||||||
buffer.encode_float(3, this->position);
|
buffer.encode_float(3, this->position);
|
||||||
buffer.encode_bool(4, this->stop);
|
buffer.encode_bool(4, this->stop);
|
||||||
|
buffer.encode_uint32(5, this->device_id);
|
||||||
}
|
}
|
||||||
void ValveCommandRequest::calculate_size(uint32_t &total_size) const {
|
void ValveCommandRequest::calculate_size(uint32_t &total_size) const {
|
||||||
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
|
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
|
||||||
ProtoSize::add_bool_field(total_size, 1, this->has_position, false);
|
ProtoSize::add_bool_field(total_size, 1, this->has_position, false);
|
||||||
ProtoSize::add_fixed_field<4>(total_size, 1, this->position != 0.0f, false);
|
ProtoSize::add_fixed_field<4>(total_size, 1, this->position != 0.0f, false);
|
||||||
ProtoSize::add_bool_field(total_size, 1, this->stop, false);
|
ProtoSize::add_bool_field(total_size, 1, this->stop, false);
|
||||||
|
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_DATETIME_DATETIME
|
#ifdef USE_DATETIME_DATETIME
|
||||||
@ -6261,6 +6389,16 @@ void DateTimeStateResponse::calculate_size(uint32_t &total_size) const {
|
|||||||
ProtoSize::add_fixed_field<4>(total_size, 1, this->epoch_seconds != 0, false);
|
ProtoSize::add_fixed_field<4>(total_size, 1, this->epoch_seconds != 0, false);
|
||||||
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
|
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
|
||||||
}
|
}
|
||||||
|
bool DateTimeCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||||
|
switch (field_id) {
|
||||||
|
case 3: {
|
||||||
|
this->device_id = value.as_uint32();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
bool DateTimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
|
bool DateTimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
|
||||||
switch (field_id) {
|
switch (field_id) {
|
||||||
case 1: {
|
case 1: {
|
||||||
@ -6278,10 +6416,12 @@ bool DateTimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
|
|||||||
void DateTimeCommandRequest::encode(ProtoWriteBuffer buffer) const {
|
void DateTimeCommandRequest::encode(ProtoWriteBuffer buffer) const {
|
||||||
buffer.encode_fixed32(1, this->key);
|
buffer.encode_fixed32(1, this->key);
|
||||||
buffer.encode_fixed32(2, this->epoch_seconds);
|
buffer.encode_fixed32(2, this->epoch_seconds);
|
||||||
|
buffer.encode_uint32(3, this->device_id);
|
||||||
}
|
}
|
||||||
void DateTimeCommandRequest::calculate_size(uint32_t &total_size) const {
|
void DateTimeCommandRequest::calculate_size(uint32_t &total_size) const {
|
||||||
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
|
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
|
||||||
ProtoSize::add_fixed_field<4>(total_size, 1, this->epoch_seconds != 0, false);
|
ProtoSize::add_fixed_field<4>(total_size, 1, this->epoch_seconds != 0, false);
|
||||||
|
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_UPDATE
|
#ifdef USE_UPDATE
|
||||||
@ -6455,6 +6595,10 @@ bool UpdateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
|||||||
this->command = value.as_enum<enums::UpdateCommand>();
|
this->command = value.as_enum<enums::UpdateCommand>();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
case 3: {
|
||||||
|
this->device_id = value.as_uint32();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -6472,10 +6616,12 @@ bool UpdateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
|
|||||||
void UpdateCommandRequest::encode(ProtoWriteBuffer buffer) const {
|
void UpdateCommandRequest::encode(ProtoWriteBuffer buffer) const {
|
||||||
buffer.encode_fixed32(1, this->key);
|
buffer.encode_fixed32(1, this->key);
|
||||||
buffer.encode_enum<enums::UpdateCommand>(2, this->command);
|
buffer.encode_enum<enums::UpdateCommand>(2, this->command);
|
||||||
|
buffer.encode_uint32(3, this->device_id);
|
||||||
}
|
}
|
||||||
void UpdateCommandRequest::calculate_size(uint32_t &total_size) const {
|
void UpdateCommandRequest::calculate_size(uint32_t &total_size) const {
|
||||||
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
|
ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false);
|
||||||
ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->command), false);
|
ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->command), false);
|
||||||
|
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -986,6 +986,11 @@ void CoverCommandRequest::dump_to(std::string &out) const {
|
|||||||
out.append(" stop: ");
|
out.append(" stop: ");
|
||||||
out.append(YESNO(this->stop));
|
out.append(YESNO(this->stop));
|
||||||
out.append("\n");
|
out.append("\n");
|
||||||
|
|
||||||
|
out.append(" device_id: ");
|
||||||
|
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
|
||||||
|
out.append(buffer);
|
||||||
|
out.append("\n");
|
||||||
out.append("}");
|
out.append("}");
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@ -1146,6 +1151,11 @@ void FanCommandRequest::dump_to(std::string &out) const {
|
|||||||
out.append(" preset_mode: ");
|
out.append(" preset_mode: ");
|
||||||
out.append("'").append(this->preset_mode).append("'");
|
out.append("'").append(this->preset_mode).append("'");
|
||||||
out.append("\n");
|
out.append("\n");
|
||||||
|
|
||||||
|
out.append(" device_id: ");
|
||||||
|
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
|
||||||
|
out.append(buffer);
|
||||||
|
out.append("\n");
|
||||||
out.append("}");
|
out.append("}");
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@ -1419,6 +1429,11 @@ void LightCommandRequest::dump_to(std::string &out) const {
|
|||||||
out.append(" effect: ");
|
out.append(" effect: ");
|
||||||
out.append("'").append(this->effect).append("'");
|
out.append("'").append(this->effect).append("'");
|
||||||
out.append("\n");
|
out.append("\n");
|
||||||
|
|
||||||
|
out.append(" device_id: ");
|
||||||
|
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
|
||||||
|
out.append(buffer);
|
||||||
|
out.append("\n");
|
||||||
out.append("}");
|
out.append("}");
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@ -1586,6 +1601,11 @@ void SwitchCommandRequest::dump_to(std::string &out) const {
|
|||||||
out.append(" state: ");
|
out.append(" state: ");
|
||||||
out.append(YESNO(this->state));
|
out.append(YESNO(this->state));
|
||||||
out.append("\n");
|
out.append("\n");
|
||||||
|
|
||||||
|
out.append(" device_id: ");
|
||||||
|
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
|
||||||
|
out.append(buffer);
|
||||||
|
out.append("\n");
|
||||||
out.append("}");
|
out.append("}");
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@ -1944,6 +1964,11 @@ void CameraImageResponse::dump_to(std::string &out) const {
|
|||||||
out.append(" done: ");
|
out.append(" done: ");
|
||||||
out.append(YESNO(this->done));
|
out.append(YESNO(this->done));
|
||||||
out.append("\n");
|
out.append("\n");
|
||||||
|
|
||||||
|
out.append(" device_id: ");
|
||||||
|
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
|
||||||
|
out.append(buffer);
|
||||||
|
out.append("\n");
|
||||||
out.append("}");
|
out.append("}");
|
||||||
}
|
}
|
||||||
void CameraImageRequest::dump_to(std::string &out) const {
|
void CameraImageRequest::dump_to(std::string &out) const {
|
||||||
@ -2263,6 +2288,11 @@ void ClimateCommandRequest::dump_to(std::string &out) const {
|
|||||||
snprintf(buffer, sizeof(buffer), "%g", this->target_humidity);
|
snprintf(buffer, sizeof(buffer), "%g", this->target_humidity);
|
||||||
out.append(buffer);
|
out.append(buffer);
|
||||||
out.append("\n");
|
out.append("\n");
|
||||||
|
|
||||||
|
out.append(" device_id: ");
|
||||||
|
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
|
||||||
|
out.append(buffer);
|
||||||
|
out.append("\n");
|
||||||
out.append("}");
|
out.append("}");
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@ -2367,6 +2397,11 @@ void NumberCommandRequest::dump_to(std::string &out) const {
|
|||||||
snprintf(buffer, sizeof(buffer), "%g", this->state);
|
snprintf(buffer, sizeof(buffer), "%g", this->state);
|
||||||
out.append(buffer);
|
out.append(buffer);
|
||||||
out.append("\n");
|
out.append("\n");
|
||||||
|
|
||||||
|
out.append(" device_id: ");
|
||||||
|
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
|
||||||
|
out.append(buffer);
|
||||||
|
out.append("\n");
|
||||||
out.append("}");
|
out.append("}");
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@ -2448,6 +2483,11 @@ void SelectCommandRequest::dump_to(std::string &out) const {
|
|||||||
out.append(" state: ");
|
out.append(" state: ");
|
||||||
out.append("'").append(this->state).append("'");
|
out.append("'").append(this->state).append("'");
|
||||||
out.append("\n");
|
out.append("\n");
|
||||||
|
|
||||||
|
out.append(" device_id: ");
|
||||||
|
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
|
||||||
|
out.append(buffer);
|
||||||
|
out.append("\n");
|
||||||
out.append("}");
|
out.append("}");
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@ -2563,6 +2603,11 @@ void SirenCommandRequest::dump_to(std::string &out) const {
|
|||||||
snprintf(buffer, sizeof(buffer), "%g", this->volume);
|
snprintf(buffer, sizeof(buffer), "%g", this->volume);
|
||||||
out.append(buffer);
|
out.append(buffer);
|
||||||
out.append("\n");
|
out.append("\n");
|
||||||
|
|
||||||
|
out.append(" device_id: ");
|
||||||
|
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
|
||||||
|
out.append(buffer);
|
||||||
|
out.append("\n");
|
||||||
out.append("}");
|
out.append("}");
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@ -2658,6 +2703,11 @@ void LockCommandRequest::dump_to(std::string &out) const {
|
|||||||
out.append(" code: ");
|
out.append(" code: ");
|
||||||
out.append("'").append(this->code).append("'");
|
out.append("'").append(this->code).append("'");
|
||||||
out.append("\n");
|
out.append("\n");
|
||||||
|
|
||||||
|
out.append(" device_id: ");
|
||||||
|
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
|
||||||
|
out.append(buffer);
|
||||||
|
out.append("\n");
|
||||||
out.append("}");
|
out.append("}");
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@ -2711,6 +2761,11 @@ void ButtonCommandRequest::dump_to(std::string &out) const {
|
|||||||
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
|
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key);
|
||||||
out.append(buffer);
|
out.append(buffer);
|
||||||
out.append("\n");
|
out.append("\n");
|
||||||
|
|
||||||
|
out.append(" device_id: ");
|
||||||
|
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
|
||||||
|
out.append(buffer);
|
||||||
|
out.append("\n");
|
||||||
out.append("}");
|
out.append("}");
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@ -2857,6 +2912,11 @@ void MediaPlayerCommandRequest::dump_to(std::string &out) const {
|
|||||||
out.append(" announcement: ");
|
out.append(" announcement: ");
|
||||||
out.append(YESNO(this->announcement));
|
out.append(YESNO(this->announcement));
|
||||||
out.append("\n");
|
out.append("\n");
|
||||||
|
|
||||||
|
out.append(" device_id: ");
|
||||||
|
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
|
||||||
|
out.append(buffer);
|
||||||
|
out.append("\n");
|
||||||
out.append("}");
|
out.append("}");
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@ -3682,6 +3742,11 @@ void AlarmControlPanelCommandRequest::dump_to(std::string &out) const {
|
|||||||
out.append(" code: ");
|
out.append(" code: ");
|
||||||
out.append("'").append(this->code).append("'");
|
out.append("'").append(this->code).append("'");
|
||||||
out.append("\n");
|
out.append("\n");
|
||||||
|
|
||||||
|
out.append(" device_id: ");
|
||||||
|
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
|
||||||
|
out.append(buffer);
|
||||||
|
out.append("\n");
|
||||||
out.append("}");
|
out.append("}");
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@ -3775,6 +3840,11 @@ void TextCommandRequest::dump_to(std::string &out) const {
|
|||||||
out.append(" state: ");
|
out.append(" state: ");
|
||||||
out.append("'").append(this->state).append("'");
|
out.append("'").append(this->state).append("'");
|
||||||
out.append("\n");
|
out.append("\n");
|
||||||
|
|
||||||
|
out.append(" device_id: ");
|
||||||
|
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
|
||||||
|
out.append(buffer);
|
||||||
|
out.append("\n");
|
||||||
out.append("}");
|
out.append("}");
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@ -3872,6 +3942,11 @@ void DateCommandRequest::dump_to(std::string &out) const {
|
|||||||
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->day);
|
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->day);
|
||||||
out.append(buffer);
|
out.append(buffer);
|
||||||
out.append("\n");
|
out.append("\n");
|
||||||
|
|
||||||
|
out.append(" device_id: ");
|
||||||
|
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
|
||||||
|
out.append(buffer);
|
||||||
|
out.append("\n");
|
||||||
out.append("}");
|
out.append("}");
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@ -3969,6 +4044,11 @@ void TimeCommandRequest::dump_to(std::string &out) const {
|
|||||||
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->second);
|
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->second);
|
||||||
out.append(buffer);
|
out.append(buffer);
|
||||||
out.append("\n");
|
out.append("\n");
|
||||||
|
|
||||||
|
out.append(" device_id: ");
|
||||||
|
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
|
||||||
|
out.append(buffer);
|
||||||
|
out.append("\n");
|
||||||
out.append("}");
|
out.append("}");
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@ -4138,6 +4218,11 @@ void ValveCommandRequest::dump_to(std::string &out) const {
|
|||||||
out.append(" stop: ");
|
out.append(" stop: ");
|
||||||
out.append(YESNO(this->stop));
|
out.append(YESNO(this->stop));
|
||||||
out.append("\n");
|
out.append("\n");
|
||||||
|
|
||||||
|
out.append(" device_id: ");
|
||||||
|
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
|
||||||
|
out.append(buffer);
|
||||||
|
out.append("\n");
|
||||||
out.append("}");
|
out.append("}");
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@ -4215,6 +4300,11 @@ void DateTimeCommandRequest::dump_to(std::string &out) const {
|
|||||||
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->epoch_seconds);
|
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->epoch_seconds);
|
||||||
out.append(buffer);
|
out.append(buffer);
|
||||||
out.append("\n");
|
out.append("\n");
|
||||||
|
|
||||||
|
out.append(" device_id: ");
|
||||||
|
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
|
||||||
|
out.append(buffer);
|
||||||
|
out.append("\n");
|
||||||
out.append("}");
|
out.append("}");
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@ -4323,6 +4413,11 @@ void UpdateCommandRequest::dump_to(std::string &out) const {
|
|||||||
out.append(" command: ");
|
out.append(" command: ");
|
||||||
out.append(proto_enum_to_string<enums::UpdateCommand>(this->command));
|
out.append(proto_enum_to_string<enums::UpdateCommand>(this->command));
|
||||||
out.append("\n");
|
out.append("\n");
|
||||||
|
|
||||||
|
out.append(" device_id: ");
|
||||||
|
snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id);
|
||||||
|
out.append(buffer);
|
||||||
|
out.append("\n");
|
||||||
out.append("}");
|
out.append("}");
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
@ -475,7 +475,8 @@ void APIServer::on_shutdown() {
|
|||||||
if (!c->send_message(DisconnectRequest())) {
|
if (!c->send_message(DisconnectRequest())) {
|
||||||
// If we can't send the disconnect request directly (tx_buffer full),
|
// If we can't send the disconnect request directly (tx_buffer full),
|
||||||
// schedule it at the front of the batch so it will be sent with priority
|
// schedule it at the front of the batch so it will be sent with priority
|
||||||
c->schedule_message_front_(nullptr, &APIConnection::try_send_disconnect_request, DisconnectRequest::MESSAGE_TYPE);
|
c->schedule_message_front_(nullptr, &APIConnection::try_send_disconnect_request, DisconnectRequest::MESSAGE_TYPE,
|
||||||
|
DisconnectRequest::ESTIMATED_SIZE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ class APIConnection;
|
|||||||
#define LIST_ENTITIES_HANDLER(entity_type, EntityClass, ResponseType) \
|
#define LIST_ENTITIES_HANDLER(entity_type, EntityClass, ResponseType) \
|
||||||
bool ListEntitiesIterator::on_##entity_type(EntityClass *entity) { /* NOLINT(bugprone-macro-parentheses) */ \
|
bool ListEntitiesIterator::on_##entity_type(EntityClass *entity) { /* NOLINT(bugprone-macro-parentheses) */ \
|
||||||
return this->client_->schedule_message_(entity, &APIConnection::try_send_##entity_type##_info, \
|
return this->client_->schedule_message_(entity, &APIConnection::try_send_##entity_type##_info, \
|
||||||
ResponseType::MESSAGE_TYPE); \
|
ResponseType::MESSAGE_TYPE, ResponseType::ESTIMATED_SIZE); \
|
||||||
}
|
}
|
||||||
|
|
||||||
class ListEntitiesIterator : public ComponentIterator {
|
class ListEntitiesIterator : public ComponentIterator {
|
||||||
|
@ -363,11 +363,11 @@ class ProtoService {
|
|||||||
* @return A ProtoWriteBuffer object with the reserved size.
|
* @return A ProtoWriteBuffer object with the reserved size.
|
||||||
*/
|
*/
|
||||||
virtual ProtoWriteBuffer create_buffer(uint32_t reserve_size) = 0;
|
virtual ProtoWriteBuffer create_buffer(uint32_t reserve_size) = 0;
|
||||||
virtual bool send_buffer(ProtoWriteBuffer buffer, uint16_t message_type) = 0;
|
virtual bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) = 0;
|
||||||
virtual void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) = 0;
|
virtual void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) = 0;
|
||||||
|
|
||||||
// Optimized method that pre-allocates buffer based on message size
|
// Optimized method that pre-allocates buffer based on message size
|
||||||
bool send_message_(const ProtoMessage &msg, uint16_t message_type) {
|
bool send_message_(const ProtoMessage &msg, uint8_t message_type) {
|
||||||
uint32_t msg_size = 0;
|
uint32_t msg_size = 0;
|
||||||
msg.calculate_size(msg_size);
|
msg.calculate_size(msg_size);
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
CODEOWNERS = ["@esphome/core"]
|
CODEOWNERS = ["@esphome/core"]
|
||||||
|
|
||||||
|
CONF_BYTE_ORDER = "byte_order"
|
||||||
CONF_DRAW_ROUNDING = "draw_rounding"
|
CONF_DRAW_ROUNDING = "draw_rounding"
|
||||||
CONF_ON_STATE_CHANGE = "on_state_change"
|
CONF_ON_STATE_CHANGE = "on_state_change"
|
||||||
CONF_REQUEST_HEADERS = "request_headers"
|
CONF_REQUEST_HEADERS = "request_headers"
|
||||||
|
@ -53,6 +53,7 @@ void DebugComponent::on_shutdown() {
|
|||||||
auto pref = global_preferences->make_preference(REBOOT_MAX_LEN, fnv1_hash(REBOOT_KEY + App.get_name()));
|
auto pref = global_preferences->make_preference(REBOOT_MAX_LEN, fnv1_hash(REBOOT_KEY + App.get_name()));
|
||||||
if (component != nullptr) {
|
if (component != nullptr) {
|
||||||
strncpy(buffer, component->get_component_source(), REBOOT_MAX_LEN - 1);
|
strncpy(buffer, component->get_component_source(), REBOOT_MAX_LEN - 1);
|
||||||
|
buffer[REBOOT_MAX_LEN - 1] = '\0';
|
||||||
}
|
}
|
||||||
ESP_LOGD(TAG, "Storing reboot source: %s", buffer);
|
ESP_LOGD(TAG, "Storing reboot source: %s", buffer);
|
||||||
pref.save(&buffer);
|
pref.save(&buffer);
|
||||||
@ -68,6 +69,7 @@ std::string DebugComponent::get_reset_reason_() {
|
|||||||
auto pref = global_preferences->make_preference(REBOOT_MAX_LEN, fnv1_hash(REBOOT_KEY + App.get_name()));
|
auto pref = global_preferences->make_preference(REBOOT_MAX_LEN, fnv1_hash(REBOOT_KEY + App.get_name()));
|
||||||
char buffer[REBOOT_MAX_LEN]{};
|
char buffer[REBOOT_MAX_LEN]{};
|
||||||
if (pref.load(&buffer)) {
|
if (pref.load(&buffer)) {
|
||||||
|
buffer[REBOOT_MAX_LEN - 1] = '\0';
|
||||||
reset_reason = "Reboot request from " + std::string(buffer);
|
reset_reason = "Reboot request from " + std::string(buffer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from esphome import automation, pins
|
from esphome import automation, pins
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components import time
|
from esphome.components import esp32, time
|
||||||
from esphome.components.esp32 import get_esp32_variant
|
from esphome.components.esp32 import get_esp32_variant
|
||||||
from esphome.components.esp32.const import (
|
from esphome.components.esp32.const import (
|
||||||
VARIANT_ESP32,
|
VARIANT_ESP32,
|
||||||
@ -116,12 +116,20 @@ def validate_pin_number(value):
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
def validate_config(config):
|
def _validate_ex1_wakeup_mode(value):
|
||||||
if get_esp32_variant() == VARIANT_ESP32C3 and CONF_ESP32_EXT1_WAKEUP in config:
|
if value == "ALL_LOW":
|
||||||
raise cv.Invalid("ESP32-C3 does not support wakeup from touch.")
|
esp32.only_on_variant(supported=[VARIANT_ESP32], msg_prefix="ALL_LOW")(value)
|
||||||
if get_esp32_variant() == VARIANT_ESP32C3 and CONF_TOUCH_WAKEUP in config:
|
if value == "ANY_LOW":
|
||||||
raise cv.Invalid("ESP32-C3 does not support wakeup from ext1")
|
esp32.only_on_variant(
|
||||||
return config
|
supported=[
|
||||||
|
VARIANT_ESP32S2,
|
||||||
|
VARIANT_ESP32S3,
|
||||||
|
VARIANT_ESP32C6,
|
||||||
|
VARIANT_ESP32H2,
|
||||||
|
],
|
||||||
|
msg_prefix="ANY_LOW",
|
||||||
|
)(value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
deep_sleep_ns = cg.esphome_ns.namespace("deep_sleep")
|
deep_sleep_ns = cg.esphome_ns.namespace("deep_sleep")
|
||||||
@ -148,6 +156,7 @@ WAKEUP_PIN_MODES = {
|
|||||||
esp_sleep_ext1_wakeup_mode_t = cg.global_ns.enum("esp_sleep_ext1_wakeup_mode_t")
|
esp_sleep_ext1_wakeup_mode_t = cg.global_ns.enum("esp_sleep_ext1_wakeup_mode_t")
|
||||||
Ext1Wakeup = deep_sleep_ns.struct("Ext1Wakeup")
|
Ext1Wakeup = deep_sleep_ns.struct("Ext1Wakeup")
|
||||||
EXT1_WAKEUP_MODES = {
|
EXT1_WAKEUP_MODES = {
|
||||||
|
"ANY_LOW": esp_sleep_ext1_wakeup_mode_t.ESP_EXT1_WAKEUP_ANY_LOW,
|
||||||
"ALL_LOW": esp_sleep_ext1_wakeup_mode_t.ESP_EXT1_WAKEUP_ALL_LOW,
|
"ALL_LOW": esp_sleep_ext1_wakeup_mode_t.ESP_EXT1_WAKEUP_ALL_LOW,
|
||||||
"ANY_HIGH": esp_sleep_ext1_wakeup_mode_t.ESP_EXT1_WAKEUP_ANY_HIGH,
|
"ANY_HIGH": esp_sleep_ext1_wakeup_mode_t.ESP_EXT1_WAKEUP_ANY_HIGH,
|
||||||
}
|
}
|
||||||
@ -187,16 +196,28 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
),
|
),
|
||||||
cv.Optional(CONF_ESP32_EXT1_WAKEUP): cv.All(
|
cv.Optional(CONF_ESP32_EXT1_WAKEUP): cv.All(
|
||||||
cv.only_on_esp32,
|
cv.only_on_esp32,
|
||||||
|
esp32.only_on_variant(
|
||||||
|
unsupported=[VARIANT_ESP32C3], msg_prefix="Wakeup from ext1"
|
||||||
|
),
|
||||||
cv.Schema(
|
cv.Schema(
|
||||||
{
|
{
|
||||||
cv.Required(CONF_PINS): cv.ensure_list(
|
cv.Required(CONF_PINS): cv.ensure_list(
|
||||||
pins.internal_gpio_input_pin_schema, validate_pin_number
|
pins.internal_gpio_input_pin_schema, validate_pin_number
|
||||||
),
|
),
|
||||||
cv.Required(CONF_MODE): cv.enum(EXT1_WAKEUP_MODES, upper=True),
|
cv.Required(CONF_MODE): cv.All(
|
||||||
|
cv.enum(EXT1_WAKEUP_MODES, upper=True),
|
||||||
|
_validate_ex1_wakeup_mode,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
cv.Optional(CONF_TOUCH_WAKEUP): cv.All(cv.only_on_esp32, cv.boolean),
|
cv.Optional(CONF_TOUCH_WAKEUP): cv.All(
|
||||||
|
cv.only_on_esp32,
|
||||||
|
esp32.only_on_variant(
|
||||||
|
unsupported=[VARIANT_ESP32C3], msg_prefix="Wakeup from touch"
|
||||||
|
),
|
||||||
|
cv.boolean,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
).extend(cv.COMPONENT_SCHEMA),
|
).extend(cv.COMPONENT_SCHEMA),
|
||||||
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266]),
|
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266]),
|
||||||
|
@ -189,7 +189,7 @@ def get_download_types(storage_json):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def only_on_variant(*, supported=None, unsupported=None):
|
def only_on_variant(*, supported=None, unsupported=None, msg_prefix="This feature"):
|
||||||
"""Config validator for features only available on some ESP32 variants."""
|
"""Config validator for features only available on some ESP32 variants."""
|
||||||
if supported is not None and not isinstance(supported, list):
|
if supported is not None and not isinstance(supported, list):
|
||||||
supported = [supported]
|
supported = [supported]
|
||||||
@ -200,11 +200,11 @@ def only_on_variant(*, supported=None, unsupported=None):
|
|||||||
variant = get_esp32_variant()
|
variant = get_esp32_variant()
|
||||||
if supported is not None and variant not in supported:
|
if supported is not None and variant not in supported:
|
||||||
raise cv.Invalid(
|
raise cv.Invalid(
|
||||||
f"This feature is only available on {', '.join(supported)}"
|
f"{msg_prefix} is only available on {', '.join(supported)}"
|
||||||
)
|
)
|
||||||
if unsupported is not None and variant in unsupported:
|
if unsupported is not None and variant in unsupported:
|
||||||
raise cv.Invalid(
|
raise cv.Invalid(
|
||||||
f"This feature is not available on {', '.join(unsupported)}"
|
f"{msg_prefix} is not available on {', '.join(unsupported)}"
|
||||||
)
|
)
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
@ -707,6 +707,7 @@ async def to_code(config):
|
|||||||
cg.add_define("ESPHOME_VARIANT", VARIANT_FRIENDLY[config[CONF_VARIANT]])
|
cg.add_define("ESPHOME_VARIANT", VARIANT_FRIENDLY[config[CONF_VARIANT]])
|
||||||
|
|
||||||
cg.add_platformio_option("lib_ldf_mode", "off")
|
cg.add_platformio_option("lib_ldf_mode", "off")
|
||||||
|
cg.add_platformio_option("lib_compat_mode", "strict")
|
||||||
|
|
||||||
framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
|
framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
|
||||||
|
|
||||||
|
@ -114,7 +114,6 @@ void ESP32InternalGPIOPin::setup() {
|
|||||||
if (flags_ & gpio::FLAG_OUTPUT) {
|
if (flags_ & gpio::FLAG_OUTPUT) {
|
||||||
gpio_set_drive_capability(pin_, drive_strength_);
|
gpio_set_drive_capability(pin_, drive_strength_);
|
||||||
}
|
}
|
||||||
ESP_LOGD(TAG, "rtc: %d", SOC_GPIO_SUPPORT_RTC_INDEPENDENT);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ESP32InternalGPIOPin::pin_mode(gpio::Flags flags) {
|
void ESP32InternalGPIOPin::pin_mode(gpio::Flags flags) {
|
||||||
|
@ -308,7 +308,7 @@ async def to_code(config):
|
|||||||
cg.add(var.set_frame_buffer_count(config[CONF_FRAME_BUFFER_COUNT]))
|
cg.add(var.set_frame_buffer_count(config[CONF_FRAME_BUFFER_COUNT]))
|
||||||
cg.add(var.set_frame_size(config[CONF_RESOLUTION]))
|
cg.add(var.set_frame_size(config[CONF_RESOLUTION]))
|
||||||
|
|
||||||
cg.add_define("USE_ESP32_CAMERA")
|
cg.add_define("USE_CAMERA")
|
||||||
|
|
||||||
if CORE.using_esp_idf:
|
if CORE.using_esp_idf:
|
||||||
add_idf_component(name="espressif/esp32-camera", ref="2.0.15")
|
add_idf_component(name="espressif/esp32-camera", ref="2.0.15")
|
||||||
|
@ -109,6 +109,7 @@ void ESP32TouchComponent::loop() {
|
|||||||
|
|
||||||
// Only publish if state changed - this filters out repeated events
|
// Only publish if state changed - this filters out repeated events
|
||||||
if (new_state != child->last_state_) {
|
if (new_state != child->last_state_) {
|
||||||
|
child->initial_state_published_ = true;
|
||||||
child->last_state_ = new_state;
|
child->last_state_ = new_state;
|
||||||
child->publish_state(new_state);
|
child->publish_state(new_state);
|
||||||
// Original ESP32: ISR only fires when touched, release is detected by timeout
|
// Original ESP32: ISR only fires when touched, release is detected by timeout
|
||||||
@ -175,6 +176,9 @@ void ESP32TouchComponent::on_shutdown() {
|
|||||||
void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) {
|
void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) {
|
||||||
ESP32TouchComponent *component = static_cast<ESP32TouchComponent *>(arg);
|
ESP32TouchComponent *component = static_cast<ESP32TouchComponent *>(arg);
|
||||||
|
|
||||||
|
uint32_t mask = 0;
|
||||||
|
touch_ll_read_trigger_status_mask(&mask);
|
||||||
|
touch_ll_clear_trigger_status_mask();
|
||||||
touch_pad_clear_status();
|
touch_pad_clear_status();
|
||||||
|
|
||||||
// INTERRUPT BEHAVIOR: On ESP32 v1 hardware, the interrupt fires when ANY configured
|
// INTERRUPT BEHAVIOR: On ESP32 v1 hardware, the interrupt fires when ANY configured
|
||||||
@ -184,6 +188,11 @@ void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) {
|
|||||||
// as any pad remains touched. This allows us to detect both new touches and
|
// as any pad remains touched. This allows us to detect both new touches and
|
||||||
// continued touches, but releases must be detected by timeout in the main loop.
|
// continued touches, but releases must be detected by timeout in the main loop.
|
||||||
|
|
||||||
|
// IMPORTANT: ESP32 v1 touch detection logic - INVERTED compared to v2!
|
||||||
|
// ESP32 v1: Touch is detected when capacitance INCREASES, causing the measured value to DECREASE
|
||||||
|
// Therefore: touched = (value < threshold)
|
||||||
|
// This is opposite to ESP32-S2/S3 v2 where touched = (value > threshold)
|
||||||
|
|
||||||
// Process all configured pads to check their current state
|
// Process all configured pads to check their current state
|
||||||
// Note: ESP32 v1 doesn't tell us which specific pad triggered the interrupt,
|
// Note: ESP32 v1 doesn't tell us which specific pad triggered the interrupt,
|
||||||
// so we must scan all configured pads to find which ones were touched
|
// so we must scan all configured pads to find which ones were touched
|
||||||
@ -201,19 +210,12 @@ void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) {
|
|||||||
value = touch_ll_read_raw_data(pad);
|
value = touch_ll_read_raw_data(pad);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip pads with 0 value - they haven't been measured in this cycle
|
// Skip pads that aren’t in the trigger mask
|
||||||
// This is important: not all pads are measured every interrupt cycle,
|
bool is_touched = (mask >> pad) & 1;
|
||||||
// only those that the hardware has updated
|
if (!is_touched) {
|
||||||
if (value == 0) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// IMPORTANT: ESP32 v1 touch detection logic - INVERTED compared to v2!
|
|
||||||
// ESP32 v1: Touch is detected when capacitance INCREASES, causing the measured value to DECREASE
|
|
||||||
// Therefore: touched = (value < threshold)
|
|
||||||
// This is opposite to ESP32-S2/S3 v2 where touched = (value > threshold)
|
|
||||||
bool is_touched = value < child->get_threshold();
|
|
||||||
|
|
||||||
// Always send the current state - the main loop will filter for changes
|
// Always send the current state - the main loop will filter for changes
|
||||||
// We send both touched and untouched states because the ISR doesn't
|
// We send both touched and untouched states because the ISR doesn't
|
||||||
// track previous state (to keep ISR fast and simple)
|
// track previous state (to keep ISR fast and simple)
|
||||||
|
@ -180,6 +180,7 @@ async def to_code(config):
|
|||||||
cg.add(esp8266_ns.setup_preferences())
|
cg.add(esp8266_ns.setup_preferences())
|
||||||
|
|
||||||
cg.add_platformio_option("lib_ldf_mode", "off")
|
cg.add_platformio_option("lib_ldf_mode", "off")
|
||||||
|
cg.add_platformio_option("lib_compat_mode", "strict")
|
||||||
|
|
||||||
cg.add_platformio_option("board", config[CONF_BOARD])
|
cg.add_platformio_option("board", config[CONF_BOARD])
|
||||||
cg.add_build_flag("-DUSE_ESP8266")
|
cg.add_build_flag("-DUSE_ESP8266")
|
||||||
|
0
esphome/components/gl_r01_i2c/__init__.py
Normal file
0
esphome/components/gl_r01_i2c/__init__.py
Normal file
68
esphome/components/gl_r01_i2c/gl_r01_i2c.cpp
Normal file
68
esphome/components/gl_r01_i2c/gl_r01_i2c.cpp
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
#include "esphome/core/log.h"
|
||||||
|
#include "esphome/core/hal.h"
|
||||||
|
#include "gl_r01_i2c.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace gl_r01_i2c {
|
||||||
|
|
||||||
|
static const char *const TAG = "gl_r01_i2c";
|
||||||
|
|
||||||
|
// Register definitions from datasheet
|
||||||
|
static const uint8_t REG_VERSION = 0x00;
|
||||||
|
static const uint8_t REG_DISTANCE = 0x02;
|
||||||
|
static const uint8_t REG_TRIGGER = 0x10;
|
||||||
|
static const uint8_t CMD_TRIGGER = 0xB0;
|
||||||
|
static const uint8_t RESTART_CMD1 = 0x5A;
|
||||||
|
static const uint8_t RESTART_CMD2 = 0xA5;
|
||||||
|
static const uint8_t READ_DELAY = 40; // minimum milliseconds from datasheet to safely read measurement result
|
||||||
|
|
||||||
|
void GLR01I2CComponent::setup() {
|
||||||
|
ESP_LOGCONFIG(TAG, "Setting up GL-R01 I2C...");
|
||||||
|
// Verify sensor presence
|
||||||
|
if (!this->read_byte_16(REG_VERSION, &this->version_)) {
|
||||||
|
ESP_LOGE(TAG, "Failed to communicate with GL-R01 I2C sensor!");
|
||||||
|
this->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ESP_LOGD(TAG, "Found GL-R01 I2C with version 0x%04X", this->version_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GLR01I2CComponent::dump_config() {
|
||||||
|
ESP_LOGCONFIG(TAG, "GL-R01 I2C:");
|
||||||
|
ESP_LOGCONFIG(TAG, " Firmware Version: 0x%04X", this->version_);
|
||||||
|
LOG_I2C_DEVICE(this);
|
||||||
|
LOG_SENSOR(" ", "Distance", this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GLR01I2CComponent::update() {
|
||||||
|
// Trigger a new measurement
|
||||||
|
if (!this->write_byte(REG_TRIGGER, CMD_TRIGGER)) {
|
||||||
|
ESP_LOGE(TAG, "Failed to trigger measurement!");
|
||||||
|
this->status_set_warning();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schedule reading the result after the read delay
|
||||||
|
this->set_timeout(READ_DELAY, [this]() { this->read_distance_(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
void GLR01I2CComponent::read_distance_() {
|
||||||
|
uint16_t distance = 0;
|
||||||
|
if (!this->read_byte_16(REG_DISTANCE, &distance)) {
|
||||||
|
ESP_LOGE(TAG, "Failed to read distance value!");
|
||||||
|
this->status_set_warning();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (distance == 0xFFFF) {
|
||||||
|
ESP_LOGW(TAG, "Invalid measurement received!");
|
||||||
|
this->status_set_warning();
|
||||||
|
} else {
|
||||||
|
ESP_LOGV(TAG, "Distance: %umm", distance);
|
||||||
|
this->publish_state(distance);
|
||||||
|
this->status_clear_warning();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace gl_r01_i2c
|
||||||
|
} // namespace esphome
|
22
esphome/components/gl_r01_i2c/gl_r01_i2c.h
Normal file
22
esphome/components/gl_r01_i2c/gl_r01_i2c.h
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/components/sensor/sensor.h"
|
||||||
|
#include "esphome/components/i2c/i2c.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace gl_r01_i2c {
|
||||||
|
|
||||||
|
class GLR01I2CComponent : public sensor::Sensor, public i2c::I2CDevice, public PollingComponent {
|
||||||
|
public:
|
||||||
|
void setup() override;
|
||||||
|
void dump_config() override;
|
||||||
|
void update() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void read_distance_();
|
||||||
|
uint16_t version_{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace gl_r01_i2c
|
||||||
|
} // namespace esphome
|
36
esphome/components/gl_r01_i2c/sensor.py
Normal file
36
esphome/components/gl_r01_i2c/sensor.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import esphome.codegen as cg
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.components import i2c, sensor
|
||||||
|
from esphome.const import (
|
||||||
|
CONF_ID,
|
||||||
|
DEVICE_CLASS_DISTANCE,
|
||||||
|
STATE_CLASS_MEASUREMENT,
|
||||||
|
UNIT_MILLIMETER,
|
||||||
|
)
|
||||||
|
|
||||||
|
CODEOWNERS = ["@pkejval"]
|
||||||
|
DEPENDENCIES = ["i2c"]
|
||||||
|
|
||||||
|
gl_r01_i2c_ns = cg.esphome_ns.namespace("gl_r01_i2c")
|
||||||
|
GLR01I2CComponent = gl_r01_i2c_ns.class_(
|
||||||
|
"GLR01I2CComponent", i2c.I2CDevice, cg.PollingComponent
|
||||||
|
)
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = (
|
||||||
|
sensor.sensor_schema(
|
||||||
|
GLR01I2CComponent,
|
||||||
|
unit_of_measurement=UNIT_MILLIMETER,
|
||||||
|
accuracy_decimals=0,
|
||||||
|
device_class=DEVICE_CLASS_DISTANCE,
|
||||||
|
state_class=STATE_CLASS_MEASUREMENT,
|
||||||
|
)
|
||||||
|
.extend(cv.polling_component_schema("60s"))
|
||||||
|
.extend(i2c.i2c_device_schema(0x74))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
|
await cg.register_component(var, config)
|
||||||
|
await sensor.register_sensor(var, config)
|
||||||
|
await i2c.register_i2c_device(var, config)
|
@ -45,3 +45,4 @@ async def to_code(config):
|
|||||||
cg.add_define("ESPHOME_BOARD", "host")
|
cg.add_define("ESPHOME_BOARD", "host")
|
||||||
cg.add_platformio_option("platform", "platformio/native")
|
cg.add_platformio_option("platform", "platformio/native")
|
||||||
cg.add_platformio_option("lib_ldf_mode", "off")
|
cg.add_platformio_option("lib_ldf_mode", "off")
|
||||||
|
cg.add_platformio_option("lib_compat_mode", "strict")
|
||||||
|
@ -111,8 +111,8 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
cv.Optional(CONF_MOISTURE): sensor.sensor_schema(
|
cv.Optional(CONF_MOISTURE): sensor.sensor_schema(
|
||||||
unit_of_measurement=UNIT_INTENSITY,
|
unit_of_measurement=UNIT_INTENSITY,
|
||||||
accuracy_decimals=0,
|
accuracy_decimals=0,
|
||||||
device_class=DEVICE_CLASS_PRECIPITATION_INTENSITY,
|
|
||||||
state_class=STATE_CLASS_MEASUREMENT,
|
state_class=STATE_CLASS_MEASUREMENT,
|
||||||
|
icon="mdi:weather-rainy",
|
||||||
),
|
),
|
||||||
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
||||||
unit_of_measurement=UNIT_CELSIUS,
|
unit_of_measurement=UNIT_CELSIUS,
|
||||||
|
@ -10,8 +10,10 @@ from PIL import Image, UnidentifiedImageError
|
|||||||
|
|
||||||
from esphome import core, external_files
|
from esphome import core, external_files
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
|
from esphome.components.const import CONF_BYTE_ORDER
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
|
CONF_DEFAULTS,
|
||||||
CONF_DITHER,
|
CONF_DITHER,
|
||||||
CONF_FILE,
|
CONF_FILE,
|
||||||
CONF_ICON,
|
CONF_ICON,
|
||||||
@ -38,6 +40,7 @@ CONF_OPAQUE = "opaque"
|
|||||||
CONF_CHROMA_KEY = "chroma_key"
|
CONF_CHROMA_KEY = "chroma_key"
|
||||||
CONF_ALPHA_CHANNEL = "alpha_channel"
|
CONF_ALPHA_CHANNEL = "alpha_channel"
|
||||||
CONF_INVERT_ALPHA = "invert_alpha"
|
CONF_INVERT_ALPHA = "invert_alpha"
|
||||||
|
CONF_IMAGES = "images"
|
||||||
|
|
||||||
TRANSPARENCY_TYPES = (
|
TRANSPARENCY_TYPES = (
|
||||||
CONF_OPAQUE,
|
CONF_OPAQUE,
|
||||||
@ -188,6 +191,10 @@ class ImageRGB565(ImageEncoder):
|
|||||||
dither,
|
dither,
|
||||||
invert_alpha,
|
invert_alpha,
|
||||||
)
|
)
|
||||||
|
self.big_endian = True
|
||||||
|
|
||||||
|
def set_big_endian(self, big_endian: bool) -> None:
|
||||||
|
self.big_endian = big_endian
|
||||||
|
|
||||||
def convert(self, image, path):
|
def convert(self, image, path):
|
||||||
return image.convert("RGBA")
|
return image.convert("RGBA")
|
||||||
@ -205,10 +212,16 @@ class ImageRGB565(ImageEncoder):
|
|||||||
g = 1
|
g = 1
|
||||||
b = 0
|
b = 0
|
||||||
rgb = (r << 11) | (g << 5) | b
|
rgb = (r << 11) | (g << 5) | b
|
||||||
self.data[self.index] = rgb >> 8
|
if self.big_endian:
|
||||||
self.index += 1
|
self.data[self.index] = rgb >> 8
|
||||||
self.data[self.index] = rgb & 0xFF
|
self.index += 1
|
||||||
self.index += 1
|
self.data[self.index] = rgb & 0xFF
|
||||||
|
self.index += 1
|
||||||
|
else:
|
||||||
|
self.data[self.index] = rgb & 0xFF
|
||||||
|
self.index += 1
|
||||||
|
self.data[self.index] = rgb >> 8
|
||||||
|
self.index += 1
|
||||||
if self.transparency == CONF_ALPHA_CHANNEL:
|
if self.transparency == CONF_ALPHA_CHANNEL:
|
||||||
if self.invert_alpha:
|
if self.invert_alpha:
|
||||||
a ^= 0xFF
|
a ^= 0xFF
|
||||||
@ -364,7 +377,7 @@ def validate_file_shorthand(value):
|
|||||||
value = cv.string_strict(value)
|
value = cv.string_strict(value)
|
||||||
parts = value.strip().split(":")
|
parts = value.strip().split(":")
|
||||||
if len(parts) == 2 and parts[0] in MDI_SOURCES:
|
if len(parts) == 2 and parts[0] in MDI_SOURCES:
|
||||||
match = re.match(r"[a-zA-Z0-9\-]+", parts[1])
|
match = re.match(r"^[a-zA-Z0-9\-]+$", parts[1])
|
||||||
if match is None:
|
if match is None:
|
||||||
raise cv.Invalid(f"Could not parse mdi icon name from '{value}'.")
|
raise cv.Invalid(f"Could not parse mdi icon name from '{value}'.")
|
||||||
return download_gh_svg(parts[1], parts[0])
|
return download_gh_svg(parts[1], parts[0])
|
||||||
@ -434,20 +447,29 @@ def validate_type(image_types):
|
|||||||
|
|
||||||
|
|
||||||
def validate_settings(value):
|
def validate_settings(value):
|
||||||
type = value[CONF_TYPE]
|
"""
|
||||||
|
Validate the settings for a single image configuration.
|
||||||
|
"""
|
||||||
|
conf_type = value[CONF_TYPE]
|
||||||
|
type_class = IMAGE_TYPE[conf_type]
|
||||||
transparency = value[CONF_TRANSPARENCY].lower()
|
transparency = value[CONF_TRANSPARENCY].lower()
|
||||||
allow_config = IMAGE_TYPE[type].allow_config
|
if transparency not in type_class.allow_config:
|
||||||
if transparency not in allow_config:
|
|
||||||
raise cv.Invalid(
|
raise cv.Invalid(
|
||||||
f"Image format '{type}' cannot have transparency: {transparency}"
|
f"Image format '{conf_type}' cannot have transparency: {transparency}"
|
||||||
)
|
)
|
||||||
invert_alpha = value.get(CONF_INVERT_ALPHA, False)
|
invert_alpha = value.get(CONF_INVERT_ALPHA, False)
|
||||||
if (
|
if (
|
||||||
invert_alpha
|
invert_alpha
|
||||||
and transparency != CONF_ALPHA_CHANNEL
|
and transparency != CONF_ALPHA_CHANNEL
|
||||||
and CONF_INVERT_ALPHA not in allow_config
|
and CONF_INVERT_ALPHA not in type_class.allow_config
|
||||||
):
|
):
|
||||||
raise cv.Invalid("No alpha channel to invert")
|
raise cv.Invalid("No alpha channel to invert")
|
||||||
|
if value.get(CONF_BYTE_ORDER) is not None and not callable(
|
||||||
|
getattr(type_class, "set_big_endian", None)
|
||||||
|
):
|
||||||
|
raise cv.Invalid(
|
||||||
|
f"Image format '{conf_type}' does not support byte order configuration"
|
||||||
|
)
|
||||||
if file := value.get(CONF_FILE):
|
if file := value.get(CONF_FILE):
|
||||||
file = Path(file)
|
file = Path(file)
|
||||||
if is_svg_file(file):
|
if is_svg_file(file):
|
||||||
@ -456,31 +478,82 @@ def validate_settings(value):
|
|||||||
try:
|
try:
|
||||||
Image.open(file)
|
Image.open(file)
|
||||||
except UnidentifiedImageError as exc:
|
except UnidentifiedImageError as exc:
|
||||||
raise cv.Invalid(f"File can't be opened as image: {file}") from exc
|
raise cv.Invalid(
|
||||||
|
f"File can't be opened as image: {file.absolute()}"
|
||||||
|
) from exc
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
IMAGE_ID_SCHEMA = {
|
||||||
|
cv.Required(CONF_ID): cv.declare_id(Image_),
|
||||||
|
cv.Required(CONF_FILE): cv.Any(validate_file_shorthand, TYPED_FILE_SCHEMA),
|
||||||
|
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
OPTIONS_SCHEMA = {
|
||||||
|
cv.Optional(CONF_RESIZE): cv.dimensions,
|
||||||
|
cv.Optional(CONF_DITHER, default="NONE"): cv.one_of(
|
||||||
|
"NONE", "FLOYDSTEINBERG", upper=True
|
||||||
|
),
|
||||||
|
cv.Optional(CONF_INVERT_ALPHA, default=False): cv.boolean,
|
||||||
|
cv.Optional(CONF_BYTE_ORDER): cv.one_of("BIG_ENDIAN", "LITTLE_ENDIAN", upper=True),
|
||||||
|
cv.Optional(CONF_TRANSPARENCY, default=CONF_OPAQUE): validate_transparency(),
|
||||||
|
cv.Optional(CONF_TYPE): validate_type(IMAGE_TYPE),
|
||||||
|
}
|
||||||
|
|
||||||
|
OPTIONS = [key.schema for key in OPTIONS_SCHEMA]
|
||||||
|
|
||||||
|
# image schema with no defaults, used with `CONF_IMAGES` in the config
|
||||||
|
IMAGE_SCHEMA_NO_DEFAULTS = {
|
||||||
|
**IMAGE_ID_SCHEMA,
|
||||||
|
**{cv.Optional(key): OPTIONS_SCHEMA[key] for key in OPTIONS},
|
||||||
|
}
|
||||||
|
|
||||||
BASE_SCHEMA = cv.Schema(
|
BASE_SCHEMA = cv.Schema(
|
||||||
{
|
{
|
||||||
cv.Required(CONF_ID): cv.declare_id(Image_),
|
**IMAGE_ID_SCHEMA,
|
||||||
cv.Required(CONF_FILE): cv.Any(validate_file_shorthand, TYPED_FILE_SCHEMA),
|
**OPTIONS_SCHEMA,
|
||||||
cv.Optional(CONF_RESIZE): cv.dimensions,
|
|
||||||
cv.Optional(CONF_DITHER, default="NONE"): cv.one_of(
|
|
||||||
"NONE", "FLOYDSTEINBERG", upper=True
|
|
||||||
),
|
|
||||||
cv.Optional(CONF_INVERT_ALPHA, default=False): cv.boolean,
|
|
||||||
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
|
|
||||||
}
|
}
|
||||||
).add_extra(validate_settings)
|
).add_extra(validate_settings)
|
||||||
|
|
||||||
IMAGE_SCHEMA = BASE_SCHEMA.extend(
|
IMAGE_SCHEMA = BASE_SCHEMA.extend(
|
||||||
{
|
{
|
||||||
cv.Required(CONF_TYPE): validate_type(IMAGE_TYPE),
|
cv.Required(CONF_TYPE): validate_type(IMAGE_TYPE),
|
||||||
cv.Optional(CONF_TRANSPARENCY, default=CONF_OPAQUE): validate_transparency(),
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_defaults(value):
|
||||||
|
"""
|
||||||
|
Validate the options for images with defaults
|
||||||
|
"""
|
||||||
|
defaults = value[CONF_DEFAULTS]
|
||||||
|
result = []
|
||||||
|
for index, image in enumerate(value[CONF_IMAGES]):
|
||||||
|
type = image.get(CONF_TYPE, defaults.get(CONF_TYPE))
|
||||||
|
if type is None:
|
||||||
|
raise cv.Invalid(
|
||||||
|
"Type is required either in the image config or in the defaults",
|
||||||
|
path=[CONF_IMAGES, index],
|
||||||
|
)
|
||||||
|
type_class = IMAGE_TYPE[type]
|
||||||
|
# A default byte order should be simply ignored if the type does not support it
|
||||||
|
available_options = [*OPTIONS]
|
||||||
|
if (
|
||||||
|
not callable(getattr(type_class, "set_big_endian", None))
|
||||||
|
and CONF_BYTE_ORDER not in image
|
||||||
|
):
|
||||||
|
available_options.remove(CONF_BYTE_ORDER)
|
||||||
|
config = {
|
||||||
|
**{key: image.get(key, defaults.get(key)) for key in available_options},
|
||||||
|
**{key.schema: image[key.schema] for key in IMAGE_ID_SCHEMA},
|
||||||
|
}
|
||||||
|
validate_settings(config)
|
||||||
|
result.append(config)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def typed_image_schema(image_type):
|
def typed_image_schema(image_type):
|
||||||
"""
|
"""
|
||||||
Construct a schema for a specific image type, allowing transparency options
|
Construct a schema for a specific image type, allowing transparency options
|
||||||
@ -523,10 +596,33 @@ def typed_image_schema(image_type):
|
|||||||
|
|
||||||
# The config schema can be a (possibly empty) single list of images,
|
# The config schema can be a (possibly empty) single list of images,
|
||||||
# or a dictionary of image types each with a list of images
|
# or a dictionary of image types each with a list of images
|
||||||
CONFIG_SCHEMA = cv.Any(
|
# or a dictionary with keys `defaults:` and `images:`
|
||||||
cv.Schema({cv.Optional(t.lower()): typed_image_schema(t) for t in IMAGE_TYPE}),
|
|
||||||
cv.ensure_list(IMAGE_SCHEMA),
|
|
||||||
)
|
def _config_schema(config):
|
||||||
|
if isinstance(config, list):
|
||||||
|
return cv.Schema([IMAGE_SCHEMA])(config)
|
||||||
|
if not isinstance(config, dict):
|
||||||
|
raise cv.Invalid(
|
||||||
|
"Badly formed image configuration, expected a list or a dictionary"
|
||||||
|
)
|
||||||
|
if CONF_DEFAULTS in config or CONF_IMAGES in config:
|
||||||
|
return validate_defaults(
|
||||||
|
cv.Schema(
|
||||||
|
{
|
||||||
|
cv.Required(CONF_DEFAULTS): OPTIONS_SCHEMA,
|
||||||
|
cv.Required(CONF_IMAGES): cv.ensure_list(IMAGE_SCHEMA_NO_DEFAULTS),
|
||||||
|
}
|
||||||
|
)(config)
|
||||||
|
)
|
||||||
|
if CONF_ID in config or CONF_FILE in config:
|
||||||
|
return cv.ensure_list(IMAGE_SCHEMA)([config])
|
||||||
|
return cv.Schema(
|
||||||
|
{cv.Optional(t.lower()): typed_image_schema(t) for t in IMAGE_TYPE}
|
||||||
|
)(config)
|
||||||
|
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = _config_schema
|
||||||
|
|
||||||
|
|
||||||
async def write_image(config, all_frames=False):
|
async def write_image(config, all_frames=False):
|
||||||
@ -585,6 +681,9 @@ async def write_image(config, all_frames=False):
|
|||||||
|
|
||||||
total_rows = height * frame_count
|
total_rows = height * frame_count
|
||||||
encoder = IMAGE_TYPE[type](width, total_rows, transparency, dither, invert_alpha)
|
encoder = IMAGE_TYPE[type](width, total_rows, transparency, dither, invert_alpha)
|
||||||
|
if byte_order := config.get(CONF_BYTE_ORDER):
|
||||||
|
# Check for valid type has already been done in validate_settings
|
||||||
|
encoder.set_big_endian(byte_order == "BIG_ENDIAN")
|
||||||
for frame_index in range(frame_count):
|
for frame_index in range(frame_count):
|
||||||
image.seek(frame_index)
|
image.seek(frame_index)
|
||||||
pixels = encoder.convert(image.resize((width, height)), path).getdata()
|
pixels = encoder.convert(image.resize((width, height)), path).getdata()
|
||||||
|
@ -268,6 +268,7 @@ async def component_to_code(config):
|
|||||||
|
|
||||||
# disable library compatibility checks
|
# disable library compatibility checks
|
||||||
cg.add_platformio_option("lib_ldf_mode", "off")
|
cg.add_platformio_option("lib_ldf_mode", "off")
|
||||||
|
cg.add_platformio_option("lib_compat_mode", "strict")
|
||||||
# include <Arduino.h> in every file
|
# include <Arduino.h> in every file
|
||||||
cg.add_platformio_option("build_src_flags", "-include Arduino.h")
|
cg.add_platformio_option("build_src_flags", "-include Arduino.h")
|
||||||
# dummy version code
|
# dummy version code
|
||||||
|
0
esphome/components/lps22/__init__.py
Normal file
0
esphome/components/lps22/__init__.py
Normal file
75
esphome/components/lps22/lps22.cpp
Normal file
75
esphome/components/lps22/lps22.cpp
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
#include "lps22.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace lps22 {
|
||||||
|
|
||||||
|
static constexpr const char *const TAG = "lps22";
|
||||||
|
|
||||||
|
static constexpr uint8_t WHO_AM_I = 0x0F;
|
||||||
|
static constexpr uint8_t LPS22HB_ID = 0xB1;
|
||||||
|
static constexpr uint8_t LPS22HH_ID = 0xB3;
|
||||||
|
static constexpr uint8_t CTRL_REG2 = 0x11;
|
||||||
|
static constexpr uint8_t CTRL_REG2_ONE_SHOT_MASK = 0b1;
|
||||||
|
static constexpr uint8_t STATUS = 0x27;
|
||||||
|
static constexpr uint8_t STATUS_T_DA_MASK = 0b10;
|
||||||
|
static constexpr uint8_t STATUS_P_DA_MASK = 0b01;
|
||||||
|
static constexpr uint8_t TEMP_L = 0x2b;
|
||||||
|
static constexpr uint8_t PRES_OUT_XL = 0x28;
|
||||||
|
static constexpr uint8_t REF_P_XL = 0x28;
|
||||||
|
static constexpr uint8_t READ_ATTEMPTS = 10;
|
||||||
|
static constexpr uint8_t READ_INTERVAL = 5;
|
||||||
|
static constexpr float PRESSURE_SCALE = 1.0f / 4096.0f;
|
||||||
|
static constexpr float TEMPERATURE_SCALE = 0.01f;
|
||||||
|
|
||||||
|
void LPS22Component::setup() {
|
||||||
|
uint8_t value = 0x00;
|
||||||
|
this->read_register(WHO_AM_I, &value, 1);
|
||||||
|
if (value != LPS22HB_ID && value != LPS22HH_ID) {
|
||||||
|
ESP_LOGW(TAG, "device IDs as %02x, which isn't a known LPS22HB or LPS22HH ID", value);
|
||||||
|
this->mark_failed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LPS22Component::dump_config() {
|
||||||
|
ESP_LOGCONFIG(TAG, "LPS22:");
|
||||||
|
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||||
|
LOG_SENSOR(" ", "Pressure", this->pressure_sensor_);
|
||||||
|
LOG_I2C_DEVICE(this);
|
||||||
|
LOG_UPDATE_INTERVAL(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LPS22Component::update() {
|
||||||
|
uint8_t value = 0x00;
|
||||||
|
this->read_register(CTRL_REG2, &value, 1);
|
||||||
|
value |= CTRL_REG2_ONE_SHOT_MASK;
|
||||||
|
this->write_register(CTRL_REG2, &value, 1);
|
||||||
|
this->set_retry(READ_INTERVAL, READ_ATTEMPTS, [this](uint8_t _) { return this->try_read_(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
RetryResult LPS22Component::try_read_() {
|
||||||
|
uint8_t value = 0x00;
|
||||||
|
this->read_register(STATUS, &value, 1);
|
||||||
|
const uint8_t expected_status_mask = STATUS_T_DA_MASK | STATUS_P_DA_MASK;
|
||||||
|
if ((value & expected_status_mask) != expected_status_mask) {
|
||||||
|
ESP_LOGD(TAG, "STATUS not ready: %x", value);
|
||||||
|
return RetryResult::RETRY;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this->temperature_sensor_ != nullptr) {
|
||||||
|
uint8_t t_buf[2]{0};
|
||||||
|
this->read_register(TEMP_L, t_buf, 2);
|
||||||
|
int16_t encoded = static_cast<int16_t>(encode_uint16(t_buf[1], t_buf[0]));
|
||||||
|
float temp = TEMPERATURE_SCALE * static_cast<float>(encoded);
|
||||||
|
this->temperature_sensor_->publish_state(temp);
|
||||||
|
}
|
||||||
|
if (this->pressure_sensor_ != nullptr) {
|
||||||
|
uint8_t p_buf[3]{0};
|
||||||
|
this->read_register(PRES_OUT_XL, p_buf, 3);
|
||||||
|
uint32_t p_lsb = encode_uint24(p_buf[2], p_buf[1], p_buf[0]);
|
||||||
|
this->pressure_sensor_->publish_state(PRESSURE_SCALE * static_cast<float>(p_lsb));
|
||||||
|
}
|
||||||
|
return RetryResult::DONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace lps22
|
||||||
|
} // namespace esphome
|
27
esphome/components/lps22/lps22.h
Normal file
27
esphome/components/lps22/lps22.h
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/components/sensor/sensor.h"
|
||||||
|
#include "esphome/components/i2c/i2c.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace lps22 {
|
||||||
|
|
||||||
|
class LPS22Component : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice {
|
||||||
|
public:
|
||||||
|
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; }
|
||||||
|
void set_pressure_sensor(sensor::Sensor *pressure_sensor) { this->pressure_sensor_ = pressure_sensor; }
|
||||||
|
|
||||||
|
void setup() override;
|
||||||
|
void update() override;
|
||||||
|
void dump_config() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
sensor::Sensor *temperature_sensor_{nullptr};
|
||||||
|
sensor::Sensor *pressure_sensor_{nullptr};
|
||||||
|
|
||||||
|
RetryResult try_read_();
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace lps22
|
||||||
|
} // namespace esphome
|
58
esphome/components/lps22/sensor.py
Normal file
58
esphome/components/lps22/sensor.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import esphome.codegen as cg
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.components import i2c, sensor
|
||||||
|
from esphome.const import (
|
||||||
|
CONF_ID,
|
||||||
|
CONF_TEMPERATURE,
|
||||||
|
CONF_PRESSURE,
|
||||||
|
STATE_CLASS_MEASUREMENT,
|
||||||
|
UNIT_CELSIUS,
|
||||||
|
UNIT_HECTOPASCAL,
|
||||||
|
ICON_THERMOMETER,
|
||||||
|
DEVICE_CLASS_TEMPERATURE,
|
||||||
|
DEVICE_CLASS_PRESSURE,
|
||||||
|
)
|
||||||
|
|
||||||
|
CODEOWNERS = ["@nagisa"]
|
||||||
|
DEPENDENCIES = ["i2c"]
|
||||||
|
|
||||||
|
lps22 = cg.esphome_ns.namespace("lps22")
|
||||||
|
|
||||||
|
LPS22Component = lps22.class_("LPS22Component", cg.PollingComponent, i2c.I2CDevice)
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = (
|
||||||
|
cv.Schema(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(LPS22Component),
|
||||||
|
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
||||||
|
unit_of_measurement=UNIT_CELSIUS,
|
||||||
|
icon=ICON_THERMOMETER,
|
||||||
|
accuracy_decimals=2,
|
||||||
|
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||||
|
state_class=STATE_CLASS_MEASUREMENT,
|
||||||
|
),
|
||||||
|
cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
|
||||||
|
unit_of_measurement=UNIT_HECTOPASCAL,
|
||||||
|
accuracy_decimals=2,
|
||||||
|
device_class=DEVICE_CLASS_PRESSURE,
|
||||||
|
state_class=STATE_CLASS_MEASUREMENT,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.extend(cv.polling_component_schema("60s"))
|
||||||
|
.extend(i2c.i2c_device_schema(0x5D)) # can also be 0x5C
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
|
await cg.register_component(var, config)
|
||||||
|
await i2c.register_i2c_device(var, config)
|
||||||
|
|
||||||
|
if temperature_config := config.get(CONF_TEMPERATURE):
|
||||||
|
sens = await sensor.new_sensor(temperature_config)
|
||||||
|
cg.add(var.set_temperature_sensor(sens))
|
||||||
|
|
||||||
|
if pressure_config := config.get(CONF_PRESSURE):
|
||||||
|
sens = await sensor.new_sensor(pressure_config)
|
||||||
|
cg.add(var.set_pressure_sensor(sens))
|
@ -153,11 +153,15 @@ void MQTTBackendESP32::mqtt_event_handler_(const Event &event) {
|
|||||||
case MQTT_EVENT_DATA: {
|
case MQTT_EVENT_DATA: {
|
||||||
static std::string topic;
|
static std::string topic;
|
||||||
if (!event.topic.empty()) {
|
if (!event.topic.empty()) {
|
||||||
|
// When a single message arrives as multiple chunks, the topic will be empty
|
||||||
|
// on any but the first message, leading to event.topic being an empty string.
|
||||||
|
// To ensure handlers get the correct topic, cache the last seen topic to
|
||||||
|
// simulate always receiving the topic from underlying library
|
||||||
topic = event.topic;
|
topic = event.topic;
|
||||||
}
|
}
|
||||||
ESP_LOGV(TAG, "MQTT_EVENT_DATA %s", topic.c_str());
|
ESP_LOGV(TAG, "MQTT_EVENT_DATA %s", topic.c_str());
|
||||||
this->on_message_.call(!event.topic.empty() ? topic.c_str() : nullptr, event.data.data(), event.data.size(),
|
this->on_message_.call(topic.c_str(), event.data.data(), event.data.size(), event.current_data_offset,
|
||||||
event.current_data_offset, event.total_data_len);
|
event.total_data_len);
|
||||||
} break;
|
} break;
|
||||||
case MQTT_EVENT_ERROR:
|
case MQTT_EVENT_ERROR:
|
||||||
ESP_LOGE(TAG, "MQTT_EVENT_ERROR");
|
ESP_LOGE(TAG, "MQTT_EVENT_ERROR");
|
||||||
|
@ -44,7 +44,7 @@ void NextionBinarySensor::set_state(bool state, bool publish, bool send_to_nexti
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
if (send_to_nextion) {
|
if (send_to_nextion) {
|
||||||
if (this->nextion_->is_sleeping() || !this->visible_) {
|
if (this->nextion_->is_sleeping() || !this->component_flags_.visible) {
|
||||||
this->needs_to_send_update_ = true;
|
this->needs_to_send_update_ = true;
|
||||||
} else {
|
} else {
|
||||||
this->needs_to_send_update_ = false;
|
this->needs_to_send_update_ = false;
|
||||||
|
@ -8,8 +8,8 @@ void NextionComponent::set_background_color(Color bco) {
|
|||||||
return; // This is a variable. no need to set color
|
return; // This is a variable. no need to set color
|
||||||
}
|
}
|
||||||
this->bco_ = bco;
|
this->bco_ = bco;
|
||||||
this->bco_needs_update_ = true;
|
this->component_flags_.bco_needs_update = true;
|
||||||
this->bco_is_set_ = true;
|
this->component_flags_.bco_is_set = true;
|
||||||
this->update_component_settings();
|
this->update_component_settings();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -19,8 +19,8 @@ void NextionComponent::set_background_pressed_color(Color bco2) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this->bco2_ = bco2;
|
this->bco2_ = bco2;
|
||||||
this->bco2_needs_update_ = true;
|
this->component_flags_.bco2_needs_update = true;
|
||||||
this->bco2_is_set_ = true;
|
this->component_flags_.bco2_is_set = true;
|
||||||
this->update_component_settings();
|
this->update_component_settings();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -29,8 +29,8 @@ void NextionComponent::set_foreground_color(Color pco) {
|
|||||||
return; // This is a variable. no need to set color
|
return; // This is a variable. no need to set color
|
||||||
}
|
}
|
||||||
this->pco_ = pco;
|
this->pco_ = pco;
|
||||||
this->pco_needs_update_ = true;
|
this->component_flags_.pco_needs_update = true;
|
||||||
this->pco_is_set_ = true;
|
this->component_flags_.pco_is_set = true;
|
||||||
this->update_component_settings();
|
this->update_component_settings();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,8 +39,8 @@ void NextionComponent::set_foreground_pressed_color(Color pco2) {
|
|||||||
return; // This is a variable. no need to set color
|
return; // This is a variable. no need to set color
|
||||||
}
|
}
|
||||||
this->pco2_ = pco2;
|
this->pco2_ = pco2;
|
||||||
this->pco2_needs_update_ = true;
|
this->component_flags_.pco2_needs_update = true;
|
||||||
this->pco2_is_set_ = true;
|
this->component_flags_.pco2_is_set = true;
|
||||||
this->update_component_settings();
|
this->update_component_settings();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,8 +49,8 @@ void NextionComponent::set_font_id(uint8_t font_id) {
|
|||||||
return; // This is a variable. no need to set color
|
return; // This is a variable. no need to set color
|
||||||
}
|
}
|
||||||
this->font_id_ = font_id;
|
this->font_id_ = font_id;
|
||||||
this->font_id_needs_update_ = true;
|
this->component_flags_.font_id_needs_update = true;
|
||||||
this->font_id_is_set_ = true;
|
this->component_flags_.font_id_is_set = true;
|
||||||
this->update_component_settings();
|
this->update_component_settings();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,20 +58,20 @@ void NextionComponent::set_visible(bool visible) {
|
|||||||
if (this->variable_name_ == this->variable_name_to_send_) {
|
if (this->variable_name_ == this->variable_name_to_send_) {
|
||||||
return; // This is a variable. no need to set color
|
return; // This is a variable. no need to set color
|
||||||
}
|
}
|
||||||
this->visible_ = visible;
|
this->component_flags_.visible = visible;
|
||||||
this->visible_needs_update_ = true;
|
this->component_flags_.visible_needs_update = true;
|
||||||
this->visible_is_set_ = true;
|
this->component_flags_.visible_is_set = true;
|
||||||
this->update_component_settings();
|
this->update_component_settings();
|
||||||
}
|
}
|
||||||
|
|
||||||
void NextionComponent::update_component_settings(bool force_update) {
|
void NextionComponent::update_component_settings(bool force_update) {
|
||||||
if (this->nextion_->is_sleeping() || !this->nextion_->is_setup() || !this->visible_is_set_ ||
|
if (this->nextion_->is_sleeping() || !this->nextion_->is_setup() || !this->component_flags_.visible_is_set ||
|
||||||
(!this->visible_needs_update_ && !this->visible_)) {
|
(!this->component_flags_.visible_needs_update && !this->component_flags_.visible)) {
|
||||||
this->needs_to_send_update_ = true;
|
this->needs_to_send_update_ = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this->visible_needs_update_ || (force_update && this->visible_is_set_)) {
|
if (this->component_flags_.visible_needs_update || (force_update && this->component_flags_.visible_is_set)) {
|
||||||
std::string name_to_send = this->variable_name_;
|
std::string name_to_send = this->variable_name_;
|
||||||
|
|
||||||
size_t pos = name_to_send.find_last_of('.');
|
size_t pos = name_to_send.find_last_of('.');
|
||||||
@ -79,9 +79,9 @@ void NextionComponent::update_component_settings(bool force_update) {
|
|||||||
name_to_send = name_to_send.substr(pos + 1);
|
name_to_send = name_to_send.substr(pos + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
this->visible_needs_update_ = false;
|
this->component_flags_.visible_needs_update = false;
|
||||||
|
|
||||||
if (this->visible_) {
|
if (this->component_flags_.visible) {
|
||||||
this->nextion_->show_component(name_to_send.c_str());
|
this->nextion_->show_component(name_to_send.c_str());
|
||||||
this->send_state_to_nextion();
|
this->send_state_to_nextion();
|
||||||
} else {
|
} else {
|
||||||
@ -90,26 +90,26 @@ void NextionComponent::update_component_settings(bool force_update) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this->bco_needs_update_ || (force_update && this->bco2_is_set_)) {
|
if (this->component_flags_.bco_needs_update || (force_update && this->component_flags_.bco2_is_set)) {
|
||||||
this->nextion_->set_component_background_color(this->variable_name_.c_str(), this->bco_);
|
this->nextion_->set_component_background_color(this->variable_name_.c_str(), this->bco_);
|
||||||
this->bco_needs_update_ = false;
|
this->component_flags_.bco_needs_update = false;
|
||||||
}
|
}
|
||||||
if (this->bco2_needs_update_ || (force_update && this->bco2_is_set_)) {
|
if (this->component_flags_.bco2_needs_update || (force_update && this->component_flags_.bco2_is_set)) {
|
||||||
this->nextion_->set_component_pressed_background_color(this->variable_name_.c_str(), this->bco2_);
|
this->nextion_->set_component_pressed_background_color(this->variable_name_.c_str(), this->bco2_);
|
||||||
this->bco2_needs_update_ = false;
|
this->component_flags_.bco2_needs_update = false;
|
||||||
}
|
}
|
||||||
if (this->pco_needs_update_ || (force_update && this->pco_is_set_)) {
|
if (this->component_flags_.pco_needs_update || (force_update && this->component_flags_.pco_is_set)) {
|
||||||
this->nextion_->set_component_foreground_color(this->variable_name_.c_str(), this->pco_);
|
this->nextion_->set_component_foreground_color(this->variable_name_.c_str(), this->pco_);
|
||||||
this->pco_needs_update_ = false;
|
this->component_flags_.pco_needs_update = false;
|
||||||
}
|
}
|
||||||
if (this->pco2_needs_update_ || (force_update && this->pco2_is_set_)) {
|
if (this->component_flags_.pco2_needs_update || (force_update && this->component_flags_.pco2_is_set)) {
|
||||||
this->nextion_->set_component_pressed_foreground_color(this->variable_name_.c_str(), this->pco2_);
|
this->nextion_->set_component_pressed_foreground_color(this->variable_name_.c_str(), this->pco2_);
|
||||||
this->pco2_needs_update_ = false;
|
this->component_flags_.pco2_needs_update = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this->font_id_needs_update_ || (force_update && this->font_id_is_set_)) {
|
if (this->component_flags_.font_id_needs_update || (force_update && this->component_flags_.font_id_is_set)) {
|
||||||
this->nextion_->set_component_font(this->variable_name_.c_str(), this->font_id_);
|
this->nextion_->set_component_font(this->variable_name_.c_str(), this->font_id_);
|
||||||
this->font_id_needs_update_ = false;
|
this->component_flags_.font_id_needs_update = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} // namespace nextion
|
} // namespace nextion
|
||||||
|
@ -21,29 +21,64 @@ class NextionComponent : public NextionComponentBase {
|
|||||||
void set_visible(bool visible);
|
void set_visible(bool visible);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
/**
|
||||||
|
* @brief Constructor initializes component state with visible=true (default state)
|
||||||
|
*/
|
||||||
|
NextionComponent() {
|
||||||
|
component_flags_ = {}; // Zero-initialize all state
|
||||||
|
component_flags_.visible = 1; // Set default visibility to true
|
||||||
|
}
|
||||||
|
|
||||||
NextionBase *nextion_;
|
NextionBase *nextion_;
|
||||||
|
|
||||||
bool bco_needs_update_ = false;
|
// Color and styling properties
|
||||||
bool bco_is_set_ = false;
|
Color bco_; // Background color
|
||||||
Color bco_;
|
Color bco2_; // Pressed background color
|
||||||
bool bco2_needs_update_ = false;
|
Color pco_; // Foreground color
|
||||||
bool bco2_is_set_ = false;
|
Color pco2_; // Pressed foreground color
|
||||||
Color bco2_;
|
|
||||||
bool pco_needs_update_ = false;
|
|
||||||
bool pco_is_set_ = false;
|
|
||||||
Color pco_;
|
|
||||||
bool pco2_needs_update_ = false;
|
|
||||||
bool pco2_is_set_ = false;
|
|
||||||
Color pco2_;
|
|
||||||
uint8_t font_id_ = 0;
|
uint8_t font_id_ = 0;
|
||||||
bool font_id_needs_update_ = false;
|
|
||||||
bool font_id_is_set_ = false;
|
|
||||||
|
|
||||||
bool visible_ = true;
|
/**
|
||||||
bool visible_needs_update_ = false;
|
* @brief Component state management using compact bitfield structure
|
||||||
bool visible_is_set_ = false;
|
*
|
||||||
|
* Stores all component state flags and properties in a single 16-bit bitfield
|
||||||
|
* for efficient memory usage and improved cache locality.
|
||||||
|
*
|
||||||
|
* Each component property maintains two state flags:
|
||||||
|
* - needs_update: Indicates the property requires synchronization with the display
|
||||||
|
* - is_set: Tracks whether the property has been explicitly configured
|
||||||
|
*
|
||||||
|
* The visible field stores both the update flags and the actual visibility state.
|
||||||
|
*/
|
||||||
|
struct ComponentState {
|
||||||
|
// Background color flags
|
||||||
|
uint16_t bco_needs_update : 1;
|
||||||
|
uint16_t bco_is_set : 1;
|
||||||
|
|
||||||
// void send_state_to_nextion() = 0;
|
// Pressed background color flags
|
||||||
|
uint16_t bco2_needs_update : 1;
|
||||||
|
uint16_t bco2_is_set : 1;
|
||||||
|
|
||||||
|
// Foreground color flags
|
||||||
|
uint16_t pco_needs_update : 1;
|
||||||
|
uint16_t pco_is_set : 1;
|
||||||
|
|
||||||
|
// Pressed foreground color flags
|
||||||
|
uint16_t pco2_needs_update : 1;
|
||||||
|
uint16_t pco2_is_set : 1;
|
||||||
|
|
||||||
|
// Font ID flags
|
||||||
|
uint16_t font_id_needs_update : 1;
|
||||||
|
uint16_t font_id_is_set : 1;
|
||||||
|
|
||||||
|
// Visibility flags
|
||||||
|
uint16_t visible_needs_update : 1;
|
||||||
|
uint16_t visible_is_set : 1;
|
||||||
|
uint16_t visible : 1; // Actual visibility state
|
||||||
|
|
||||||
|
// Reserved bits for future expansion
|
||||||
|
uint16_t reserved : 3;
|
||||||
|
} component_flags_;
|
||||||
};
|
};
|
||||||
} // namespace nextion
|
} // namespace nextion
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
@ -53,7 +53,7 @@ void NextionSensor::set_state(float state, bool publish, bool send_to_nextion) {
|
|||||||
|
|
||||||
if (this->wave_chan_id_ == UINT8_MAX) {
|
if (this->wave_chan_id_ == UINT8_MAX) {
|
||||||
if (send_to_nextion) {
|
if (send_to_nextion) {
|
||||||
if (this->nextion_->is_sleeping() || !this->visible_) {
|
if (this->nextion_->is_sleeping() || !this->component_flags_.visible) {
|
||||||
this->needs_to_send_update_ = true;
|
this->needs_to_send_update_ = true;
|
||||||
} else {
|
} else {
|
||||||
this->needs_to_send_update_ = false;
|
this->needs_to_send_update_ = false;
|
||||||
|
@ -28,7 +28,7 @@ void NextionSwitch::set_state(bool state, bool publish, bool send_to_nextion) {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
if (send_to_nextion) {
|
if (send_to_nextion) {
|
||||||
if (this->nextion_->is_sleeping() || !this->visible_) {
|
if (this->nextion_->is_sleeping() || !this->component_flags_.visible) {
|
||||||
this->needs_to_send_update_ = true;
|
this->needs_to_send_update_ = true;
|
||||||
} else {
|
} else {
|
||||||
this->needs_to_send_update_ = false;
|
this->needs_to_send_update_ = false;
|
||||||
|
@ -26,7 +26,7 @@ void NextionTextSensor::set_state(const std::string &state, bool publish, bool s
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
if (send_to_nextion) {
|
if (send_to_nextion) {
|
||||||
if (this->nextion_->is_sleeping() || !this->visible_) {
|
if (this->nextion_->is_sleeping() || !this->component_flags_.visible) {
|
||||||
this->needs_to_send_update_ = true;
|
this->needs_to_send_update_ = true;
|
||||||
} else {
|
} else {
|
||||||
this->nextion_->add_no_result_to_queue_with_set(this, state);
|
this->nextion_->add_no_result_to_queue_with_set(this, state);
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
#include "nfc.h"
|
#include "nfc.h"
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
@ -7,29 +8,9 @@ namespace nfc {
|
|||||||
|
|
||||||
static const char *const TAG = "nfc";
|
static const char *const TAG = "nfc";
|
||||||
|
|
||||||
std::string format_uid(std::vector<uint8_t> &uid) {
|
std::string format_uid(const std::vector<uint8_t> &uid) { return format_hex_pretty(uid, '-', false); }
|
||||||
char buf[(uid.size() * 2) + uid.size() - 1];
|
|
||||||
int offset = 0;
|
|
||||||
for (size_t i = 0; i < uid.size(); i++) {
|
|
||||||
const char *format = "%02X";
|
|
||||||
if (i + 1 < uid.size())
|
|
||||||
format = "%02X-";
|
|
||||||
offset += sprintf(buf + offset, format, uid[i]);
|
|
||||||
}
|
|
||||||
return std::string(buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string format_bytes(std::vector<uint8_t> &bytes) {
|
std::string format_bytes(const std::vector<uint8_t> &bytes) { return format_hex_pretty(bytes, ' ', false); }
|
||||||
char buf[(bytes.size() * 2) + bytes.size() - 1];
|
|
||||||
int offset = 0;
|
|
||||||
for (size_t i = 0; i < bytes.size(); i++) {
|
|
||||||
const char *format = "%02X";
|
|
||||||
if (i + 1 < bytes.size())
|
|
||||||
format = "%02X ";
|
|
||||||
offset += sprintf(buf + offset, format, bytes[i]);
|
|
||||||
}
|
|
||||||
return std::string(buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t guess_tag_type(uint8_t uid_length) {
|
uint8_t guess_tag_type(uint8_t uid_length) {
|
||||||
if (uid_length == 4) {
|
if (uid_length == 4) {
|
||||||
|
@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
#include "ndef_record.h"
|
|
||||||
#include "ndef_message.h"
|
#include "ndef_message.h"
|
||||||
|
#include "ndef_record.h"
|
||||||
#include "nfc_tag.h"
|
#include "nfc_tag.h"
|
||||||
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@ -53,8 +53,8 @@ static const uint8_t DEFAULT_KEY[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
|||||||
static const uint8_t NDEF_KEY[6] = {0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7};
|
static const uint8_t NDEF_KEY[6] = {0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7};
|
||||||
static const uint8_t MAD_KEY[6] = {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5};
|
static const uint8_t MAD_KEY[6] = {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5};
|
||||||
|
|
||||||
std::string format_uid(std::vector<uint8_t> &uid);
|
std::string format_uid(const std::vector<uint8_t> &uid);
|
||||||
std::string format_bytes(std::vector<uint8_t> &bytes);
|
std::string format_bytes(const std::vector<uint8_t> &bytes);
|
||||||
|
|
||||||
uint8_t guess_tag_type(uint8_t uid_length);
|
uint8_t guess_tag_type(uint8_t uid_length);
|
||||||
uint8_t get_mifare_classic_ndef_start_index(std::vector<uint8_t> &data);
|
uint8_t get_mifare_classic_ndef_start_index(std::vector<uint8_t> &data);
|
||||||
|
@ -165,6 +165,7 @@ async def to_code(config):
|
|||||||
# Allow LDF to properly discover dependency including those in preprocessor
|
# Allow LDF to properly discover dependency including those in preprocessor
|
||||||
# conditionals
|
# conditionals
|
||||||
cg.add_platformio_option("lib_ldf_mode", "chain+")
|
cg.add_platformio_option("lib_ldf_mode", "chain+")
|
||||||
|
cg.add_platformio_option("lib_compat_mode", "strict")
|
||||||
cg.add_platformio_option("board", config[CONF_BOARD])
|
cg.add_platformio_option("board", config[CONF_BOARD])
|
||||||
cg.add_build_flag("-DUSE_RP2040")
|
cg.add_build_flag("-DUSE_RP2040")
|
||||||
cg.set_cpp_standard("gnu++20")
|
cg.set_cpp_standard("gnu++20")
|
||||||
|
@ -224,7 +224,7 @@ bool SSD1306::is_sh1106_() const {
|
|||||||
}
|
}
|
||||||
bool SSD1306::is_sh1107_() const { return this->model_ == SH1107_MODEL_128_64 || this->model_ == SH1107_MODEL_128_128; }
|
bool SSD1306::is_sh1107_() const { return this->model_ == SH1107_MODEL_128_64 || this->model_ == SH1107_MODEL_128_128; }
|
||||||
bool SSD1306::is_ssd1305_() const {
|
bool SSD1306::is_ssd1305_() const {
|
||||||
return this->model_ == SSD1305_MODEL_128_64 || this->model_ == SSD1305_MODEL_128_64;
|
return this->model_ == SSD1305_MODEL_128_64 || this->model_ == SSD1305_MODEL_128_32;
|
||||||
}
|
}
|
||||||
void SSD1306::update() {
|
void SSD1306::update() {
|
||||||
this->do_update_();
|
this->do_update_();
|
||||||
|
@ -3132,7 +3132,7 @@ void HOT GDEY0583T81::display() {
|
|||||||
} else {
|
} else {
|
||||||
// Partial out (PTOUT), makes the display exit partial mode
|
// Partial out (PTOUT), makes the display exit partial mode
|
||||||
this->command(0x92);
|
this->command(0x92);
|
||||||
ESP_LOGD(TAG, "Partial update done, next full update after %d cycles",
|
ESP_LOGD(TAG, "Partial update done, next full update after %" PRIu32 " cycles",
|
||||||
this->full_update_every_ - this->at_update_ - 1);
|
this->full_update_every_ - this->at_update_ - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,7 +78,7 @@ enum JsonDetail { DETAIL_ALL, DETAIL_STATE };
|
|||||||
This is because only minimal changes were made to the ESPAsyncWebServer lib_dep, it was undesirable to put deferred
|
This is because only minimal changes were made to the ESPAsyncWebServer lib_dep, it was undesirable to put deferred
|
||||||
update logic into that library. We need one deferred queue per connection so instead of one AsyncEventSource with
|
update logic into that library. We need one deferred queue per connection so instead of one AsyncEventSource with
|
||||||
multiple clients, we have multiple event sources with one client each. This is slightly awkward which is why it's
|
multiple clients, we have multiple event sources with one client each. This is slightly awkward which is why it's
|
||||||
implemented in a more straightforward way for ESP-IDF. Arudino platform will eventually go away and this workaround
|
implemented in a more straightforward way for ESP-IDF. Arduino platform will eventually go away and this workaround
|
||||||
can be forgotten.
|
can be forgotten.
|
||||||
*/
|
*/
|
||||||
#ifdef USE_ARDUINO
|
#ifdef USE_ARDUINO
|
||||||
|
@ -1055,6 +1055,7 @@ def float_with_unit(quantity, regex_suffix, optional_unit=False):
|
|||||||
return validator
|
return validator
|
||||||
|
|
||||||
|
|
||||||
|
bps = float_with_unit("bits per second", "(bps|bits/s|bit/s)?")
|
||||||
frequency = float_with_unit("frequency", "(Hz|HZ|hz)?")
|
frequency = float_with_unit("frequency", "(Hz|HZ|hz)?")
|
||||||
resistance = float_with_unit("resistance", "(Ω|Ω|ohm|Ohm|OHM)?")
|
resistance = float_with_unit("resistance", "(Ω|Ω|ohm|Ohm|OHM)?")
|
||||||
current = float_with_unit("current", "(a|A|amp|Amp|amps|Amps|ampere|Ampere)?")
|
current = float_with_unit("current", "(a|A|amp|Amp|amps|Amps|ampere|Ampere)?")
|
||||||
|
@ -4,7 +4,7 @@ from enum import Enum
|
|||||||
|
|
||||||
from esphome.enum import StrEnum
|
from esphome.enum import StrEnum
|
||||||
|
|
||||||
__version__ = "2025.7.0-dev"
|
__version__ = "2025.8.0-dev"
|
||||||
|
|
||||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||||
VALID_SUBSTITUTIONS_CHARACTERS = (
|
VALID_SUBSTITUTIONS_CHARACTERS = (
|
||||||
|
@ -187,6 +187,12 @@ def entity_duplicate_validator(platform: str) -> Callable[[ConfigType], ConfigTy
|
|||||||
# No name to validate
|
# No name to validate
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
# Skip validation for internal entities
|
||||||
|
# Internal entities are not exposed to Home Assistant and don't use the hash-based
|
||||||
|
# entity state tracking system, so name collisions don't matter for them
|
||||||
|
if config.get(CONF_INTERNAL, False):
|
||||||
|
return config
|
||||||
|
|
||||||
# Get the entity name
|
# Get the entity name
|
||||||
entity_name = config[CONF_NAME]
|
entity_name = config[CONF_NAME]
|
||||||
|
|
||||||
|
@ -258,53 +258,60 @@ std::string format_hex(const uint8_t *data, size_t length) {
|
|||||||
std::string format_hex(const std::vector<uint8_t> &data) { return format_hex(data.data(), data.size()); }
|
std::string format_hex(const std::vector<uint8_t> &data) { return format_hex(data.data(), data.size()); }
|
||||||
|
|
||||||
static char format_hex_pretty_char(uint8_t v) { return v >= 10 ? 'A' + (v - 10) : '0' + v; }
|
static char format_hex_pretty_char(uint8_t v) { return v >= 10 ? 'A' + (v - 10) : '0' + v; }
|
||||||
std::string format_hex_pretty(const uint8_t *data, size_t length) {
|
std::string format_hex_pretty(const uint8_t *data, size_t length, char separator, bool show_length) {
|
||||||
if (length == 0)
|
if (data == nullptr || length == 0)
|
||||||
return "";
|
return "";
|
||||||
std::string ret;
|
std::string ret;
|
||||||
ret.resize(3 * length - 1);
|
uint8_t multiple = separator ? 3 : 2; // 3 if separator is not \0, 2 otherwise
|
||||||
|
ret.resize(multiple * length - (separator ? 1 : 0));
|
||||||
for (size_t i = 0; i < length; i++) {
|
for (size_t i = 0; i < length; i++) {
|
||||||
ret[3 * i] = format_hex_pretty_char((data[i] & 0xF0) >> 4);
|
ret[multiple * i] = format_hex_pretty_char((data[i] & 0xF0) >> 4);
|
||||||
ret[3 * i + 1] = format_hex_pretty_char(data[i] & 0x0F);
|
ret[multiple * i + 1] = format_hex_pretty_char(data[i] & 0x0F);
|
||||||
if (i != length - 1)
|
if (separator && i != length - 1)
|
||||||
ret[3 * i + 2] = '.';
|
ret[multiple * i + 2] = separator;
|
||||||
}
|
}
|
||||||
if (length > 4)
|
if (show_length && length > 4)
|
||||||
return ret + " (" + to_string(length) + ")";
|
return ret + " (" + std::to_string(length) + ")";
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
std::string format_hex_pretty(const std::vector<uint8_t> &data) { return format_hex_pretty(data.data(), data.size()); }
|
std::string format_hex_pretty(const std::vector<uint8_t> &data, char separator, bool show_length) {
|
||||||
|
return format_hex_pretty(data.data(), data.size(), separator, show_length);
|
||||||
|
}
|
||||||
|
|
||||||
std::string format_hex_pretty(const uint16_t *data, size_t length) {
|
std::string format_hex_pretty(const uint16_t *data, size_t length, char separator, bool show_length) {
|
||||||
if (length == 0)
|
if (data == nullptr || length == 0)
|
||||||
return "";
|
return "";
|
||||||
std::string ret;
|
std::string ret;
|
||||||
ret.resize(5 * length - 1);
|
uint8_t multiple = separator ? 5 : 4; // 5 if separator is not \0, 4 otherwise
|
||||||
|
ret.resize(multiple * length - (separator ? 1 : 0));
|
||||||
for (size_t i = 0; i < length; i++) {
|
for (size_t i = 0; i < length; i++) {
|
||||||
ret[5 * i] = format_hex_pretty_char((data[i] & 0xF000) >> 12);
|
ret[multiple * i] = format_hex_pretty_char((data[i] & 0xF000) >> 12);
|
||||||
ret[5 * i + 1] = format_hex_pretty_char((data[i] & 0x0F00) >> 8);
|
ret[multiple * i + 1] = format_hex_pretty_char((data[i] & 0x0F00) >> 8);
|
||||||
ret[5 * i + 2] = format_hex_pretty_char((data[i] & 0x00F0) >> 4);
|
ret[multiple * i + 2] = format_hex_pretty_char((data[i] & 0x00F0) >> 4);
|
||||||
ret[5 * i + 3] = format_hex_pretty_char(data[i] & 0x000F);
|
ret[multiple * i + 3] = format_hex_pretty_char(data[i] & 0x000F);
|
||||||
if (i != length - 1)
|
if (separator && i != length - 1)
|
||||||
ret[5 * i + 2] = '.';
|
ret[multiple * i + 4] = separator;
|
||||||
}
|
}
|
||||||
if (length > 4)
|
if (show_length && length > 4)
|
||||||
return ret + " (" + to_string(length) + ")";
|
return ret + " (" + std::to_string(length) + ")";
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
std::string format_hex_pretty(const std::vector<uint16_t> &data) { return format_hex_pretty(data.data(), data.size()); }
|
std::string format_hex_pretty(const std::vector<uint16_t> &data, char separator, bool show_length) {
|
||||||
std::string format_hex_pretty(const std::string &data) {
|
return format_hex_pretty(data.data(), data.size(), separator, show_length);
|
||||||
|
}
|
||||||
|
std::string format_hex_pretty(const std::string &data, char separator, bool show_length) {
|
||||||
if (data.empty())
|
if (data.empty())
|
||||||
return "";
|
return "";
|
||||||
std::string ret;
|
std::string ret;
|
||||||
ret.resize(3 * data.length() - 1);
|
uint8_t multiple = separator ? 3 : 2; // 3 if separator is not \0, 2 otherwise
|
||||||
|
ret.resize(multiple * data.length() - (separator ? 1 : 0));
|
||||||
for (size_t i = 0; i < data.length(); i++) {
|
for (size_t i = 0; i < data.length(); i++) {
|
||||||
ret[3 * i] = format_hex_pretty_char((data[i] & 0xF0) >> 4);
|
ret[multiple * i] = format_hex_pretty_char((data[i] & 0xF0) >> 4);
|
||||||
ret[3 * i + 1] = format_hex_pretty_char(data[i] & 0x0F);
|
ret[multiple * i + 1] = format_hex_pretty_char(data[i] & 0x0F);
|
||||||
if (i != data.length() - 1)
|
if (separator && i != data.length() - 1)
|
||||||
ret[3 * i + 2] = '.';
|
ret[multiple * i + 2] = separator;
|
||||||
}
|
}
|
||||||
if (data.length() > 4)
|
if (show_length && data.length() > 4)
|
||||||
return ret + " (" + std::to_string(data.length()) + ")";
|
return ret + " (" + std::to_string(data.length()) + ")";
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
@ -344,20 +344,149 @@ template<std::size_t N> std::string format_hex(const std::array<uint8_t, N> &dat
|
|||||||
return format_hex(data.data(), data.size());
|
return format_hex(data.data(), data.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Format the byte array \p data of length \p len in pretty-printed, human-readable hex.
|
/** Format a byte array in pretty-printed, human-readable hex format.
|
||||||
std::string format_hex_pretty(const uint8_t *data, size_t length);
|
*
|
||||||
/// Format the word array \p data of length \p len in pretty-printed, human-readable hex.
|
* Converts binary data to a hexadecimal string representation with customizable formatting.
|
||||||
std::string format_hex_pretty(const uint16_t *data, size_t length);
|
* Each byte is displayed as a two-digit uppercase hex value, separated by the specified separator.
|
||||||
/// Format the vector \p data in pretty-printed, human-readable hex.
|
* Optionally includes the total byte count in parentheses at the end.
|
||||||
std::string format_hex_pretty(const std::vector<uint8_t> &data);
|
*
|
||||||
/// Format the vector \p data in pretty-printed, human-readable hex.
|
* @param data Pointer to the byte array to format.
|
||||||
std::string format_hex_pretty(const std::vector<uint16_t> &data);
|
* @param length Number of bytes in the array.
|
||||||
/// Format the string \p data in pretty-printed, human-readable hex.
|
* @param separator Character to use between hex bytes (default: '.').
|
||||||
std::string format_hex_pretty(const std::string &data);
|
* @param show_length Whether to append the byte count in parentheses (default: true).
|
||||||
/// Format an unsigned integer in pretty-printed, human-readable hex, starting with the most significant byte.
|
* @return Formatted hex string, e.g., "A1.B2.C3.D4.E5 (5)" or "A1:B2:C3" depending on parameters.
|
||||||
template<typename T, enable_if_t<std::is_unsigned<T>::value, int> = 0> std::string format_hex_pretty(T val) {
|
*
|
||||||
|
* @note Returns empty string if data is nullptr or length is 0.
|
||||||
|
* @note The length will only be appended if show_length is true AND the length is greater than 4.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* @code
|
||||||
|
* uint8_t data[] = {0xA1, 0xB2, 0xC3};
|
||||||
|
* format_hex_pretty(data, 3); // Returns "A1.B2.C3" (no length shown for <= 4 parts)
|
||||||
|
* uint8_t data2[] = {0xA1, 0xB2, 0xC3, 0xD4, 0xE5};
|
||||||
|
* format_hex_pretty(data2, 5); // Returns "A1.B2.C3.D4.E5 (5)"
|
||||||
|
* format_hex_pretty(data2, 5, ':'); // Returns "A1:B2:C3:D4:E5 (5)"
|
||||||
|
* format_hex_pretty(data2, 5, '.', false); // Returns "A1.B2.C3.D4.E5"
|
||||||
|
* @endcode
|
||||||
|
*/
|
||||||
|
std::string format_hex_pretty(const uint8_t *data, size_t length, char separator = '.', bool show_length = true);
|
||||||
|
|
||||||
|
/** Format a 16-bit word array in pretty-printed, human-readable hex format.
|
||||||
|
*
|
||||||
|
* Similar to the byte array version, but formats 16-bit words as 4-digit hex values.
|
||||||
|
*
|
||||||
|
* @param data Pointer to the 16-bit word array to format.
|
||||||
|
* @param length Number of 16-bit words in the array.
|
||||||
|
* @param separator Character to use between hex words (default: '.').
|
||||||
|
* @param show_length Whether to append the word count in parentheses (default: true).
|
||||||
|
* @return Formatted hex string with 4-digit hex values per word.
|
||||||
|
*
|
||||||
|
* @note The length will only be appended if show_length is true AND the length is greater than 4.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* @code
|
||||||
|
* uint16_t data[] = {0xA1B2, 0xC3D4};
|
||||||
|
* format_hex_pretty(data, 2); // Returns "A1B2.C3D4" (no length shown for <= 4 parts)
|
||||||
|
* uint16_t data2[] = {0xA1B2, 0xC3D4, 0xE5F6};
|
||||||
|
* format_hex_pretty(data2, 3); // Returns "A1B2.C3D4.E5F6 (3)"
|
||||||
|
* @endcode
|
||||||
|
*/
|
||||||
|
std::string format_hex_pretty(const uint16_t *data, size_t length, char separator = '.', bool show_length = true);
|
||||||
|
|
||||||
|
/** Format a byte vector in pretty-printed, human-readable hex format.
|
||||||
|
*
|
||||||
|
* Convenience overload for std::vector<uint8_t>. Formats each byte as a two-digit
|
||||||
|
* uppercase hex value with customizable separator.
|
||||||
|
*
|
||||||
|
* @param data Vector of bytes to format.
|
||||||
|
* @param separator Character to use between hex bytes (default: '.').
|
||||||
|
* @param show_length Whether to append the byte count in parentheses (default: true).
|
||||||
|
* @return Formatted hex string representation of the vector contents.
|
||||||
|
*
|
||||||
|
* @note The length will only be appended if show_length is true AND the vector size is greater than 4.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* @code
|
||||||
|
* std::vector<uint8_t> data = {0xDE, 0xAD, 0xBE, 0xEF};
|
||||||
|
* format_hex_pretty(data); // Returns "DE.AD.BE.EF" (no length shown for <= 4 parts)
|
||||||
|
* std::vector<uint8_t> data2 = {0xDE, 0xAD, 0xBE, 0xEF, 0xCA};
|
||||||
|
* format_hex_pretty(data2); // Returns "DE.AD.BE.EF.CA (5)"
|
||||||
|
* format_hex_pretty(data2, '-'); // Returns "DE-AD-BE-EF-CA (5)"
|
||||||
|
* @endcode
|
||||||
|
*/
|
||||||
|
std::string format_hex_pretty(const std::vector<uint8_t> &data, char separator = '.', bool show_length = true);
|
||||||
|
|
||||||
|
/** Format a 16-bit word vector in pretty-printed, human-readable hex format.
|
||||||
|
*
|
||||||
|
* Convenience overload for std::vector<uint16_t>. Each 16-bit word is formatted
|
||||||
|
* as a 4-digit uppercase hex value in big-endian order.
|
||||||
|
*
|
||||||
|
* @param data Vector of 16-bit words to format.
|
||||||
|
* @param separator Character to use between hex words (default: '.').
|
||||||
|
* @param show_length Whether to append the word count in parentheses (default: true).
|
||||||
|
* @return Formatted hex string representation of the vector contents.
|
||||||
|
*
|
||||||
|
* @note The length will only be appended if show_length is true AND the vector size is greater than 4.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* @code
|
||||||
|
* std::vector<uint16_t> data = {0x1234, 0x5678};
|
||||||
|
* format_hex_pretty(data); // Returns "1234.5678" (no length shown for <= 4 parts)
|
||||||
|
* std::vector<uint16_t> data2 = {0x1234, 0x5678, 0x9ABC};
|
||||||
|
* format_hex_pretty(data2); // Returns "1234.5678.9ABC (3)"
|
||||||
|
* @endcode
|
||||||
|
*/
|
||||||
|
std::string format_hex_pretty(const std::vector<uint16_t> &data, char separator = '.', bool show_length = true);
|
||||||
|
|
||||||
|
/** Format a string's bytes in pretty-printed, human-readable hex format.
|
||||||
|
*
|
||||||
|
* Treats each character in the string as a byte and formats it in hex.
|
||||||
|
* Useful for debugging binary data stored in std::string containers.
|
||||||
|
*
|
||||||
|
* @param data String whose bytes should be formatted as hex.
|
||||||
|
* @param separator Character to use between hex bytes (default: '.').
|
||||||
|
* @param show_length Whether to append the byte count in parentheses (default: true).
|
||||||
|
* @return Formatted hex string representation of the string's byte contents.
|
||||||
|
*
|
||||||
|
* @note The length will only be appended if show_length is true AND the string length is greater than 4.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* @code
|
||||||
|
* std::string data = "ABC"; // ASCII: 0x41, 0x42, 0x43
|
||||||
|
* format_hex_pretty(data); // Returns "41.42.43" (no length shown for <= 4 parts)
|
||||||
|
* std::string data2 = "ABCDE";
|
||||||
|
* format_hex_pretty(data2); // Returns "41.42.43.44.45 (5)"
|
||||||
|
* @endcode
|
||||||
|
*/
|
||||||
|
std::string format_hex_pretty(const std::string &data, char separator = '.', bool show_length = true);
|
||||||
|
|
||||||
|
/** Format an unsigned integer in pretty-printed, human-readable hex format.
|
||||||
|
*
|
||||||
|
* Converts the integer to big-endian byte order and formats each byte as hex.
|
||||||
|
* The most significant byte appears first in the output string.
|
||||||
|
*
|
||||||
|
* @tparam T Unsigned integer type (uint8_t, uint16_t, uint32_t, uint64_t, etc.).
|
||||||
|
* @param val The unsigned integer value to format.
|
||||||
|
* @param separator Character to use between hex bytes (default: '.').
|
||||||
|
* @param show_length Whether to append the byte count in parentheses (default: true).
|
||||||
|
* @return Formatted hex string with most significant byte first.
|
||||||
|
*
|
||||||
|
* @note The length will only be appended if show_length is true AND sizeof(T) is greater than 4.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* @code
|
||||||
|
* uint32_t value = 0x12345678;
|
||||||
|
* format_hex_pretty(value); // Returns "12.34.56.78" (no length shown for <= 4 parts)
|
||||||
|
* uint64_t value2 = 0x123456789ABCDEF0;
|
||||||
|
* format_hex_pretty(value2); // Returns "12.34.56.78.9A.BC.DE.F0 (8)"
|
||||||
|
* format_hex_pretty(value2, ':'); // Returns "12:34:56:78:9A:BC:DE:F0 (8)"
|
||||||
|
* format_hex_pretty<uint16_t>(0x1234); // Returns "12.34"
|
||||||
|
* @endcode
|
||||||
|
*/
|
||||||
|
template<typename T, enable_if_t<std::is_unsigned<T>::value, int> = 0>
|
||||||
|
std::string format_hex_pretty(T val, char separator = '.', bool show_length = true) {
|
||||||
val = convert_big_endian(val);
|
val = convert_big_endian(val);
|
||||||
return format_hex_pretty(reinterpret_cast<uint8_t *>(&val), sizeof(T));
|
return format_hex_pretty(reinterpret_cast<uint8_t *>(&val), sizeof(T), separator, show_length);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Format the byte array \p data of length \p len in binary.
|
/// Format the byte array \p data of length \p len in binary.
|
||||||
|
@ -66,10 +66,8 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
|
|||||||
|
|
||||||
if (delay == SCHEDULER_DONT_RUN) {
|
if (delay == SCHEDULER_DONT_RUN) {
|
||||||
// Still need to cancel existing timer if name is not empty
|
// Still need to cancel existing timer if name is not empty
|
||||||
if (this->is_name_valid_(name_cstr)) {
|
LockGuard guard{this->lock_};
|
||||||
LockGuard guard{this->lock_};
|
this->cancel_item_locked_(component, name_cstr, type);
|
||||||
this->cancel_item_locked_(component, name_cstr, type);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,10 +123,8 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
|
|||||||
|
|
||||||
LockGuard guard{this->lock_};
|
LockGuard guard{this->lock_};
|
||||||
// If name is provided, do atomic cancel-and-add
|
// If name is provided, do atomic cancel-and-add
|
||||||
if (this->is_name_valid_(name_cstr)) {
|
// Cancel existing items
|
||||||
// Cancel existing items
|
this->cancel_item_locked_(component, name_cstr, type);
|
||||||
this->cancel_item_locked_(component, name_cstr, type);
|
|
||||||
}
|
|
||||||
// Add new item directly to to_add_
|
// Add new item directly to to_add_
|
||||||
// since we have the lock held
|
// since we have the lock held
|
||||||
this->to_add_.push_back(std::move(item));
|
this->to_add_.push_back(std::move(item));
|
||||||
@ -442,10 +438,6 @@ bool HOT Scheduler::cancel_item_(Component *component, bool is_static_string, co
|
|||||||
// Get the name as const char*
|
// Get the name as const char*
|
||||||
const char *name_cstr = this->get_name_cstr_(is_static_string, name_ptr);
|
const char *name_cstr = this->get_name_cstr_(is_static_string, name_ptr);
|
||||||
|
|
||||||
// Handle null or empty names
|
|
||||||
if (!this->is_name_valid_(name_cstr))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// obtain lock because this function iterates and can be called from non-loop task context
|
// obtain lock because this function iterates and can be called from non-loop task context
|
||||||
LockGuard guard{this->lock_};
|
LockGuard guard{this->lock_};
|
||||||
return this->cancel_item_locked_(component, name_cstr, type);
|
return this->cancel_item_locked_(component, name_cstr, type);
|
||||||
@ -453,6 +445,11 @@ bool HOT Scheduler::cancel_item_(Component *component, bool is_static_string, co
|
|||||||
|
|
||||||
// Helper to cancel items by name - must be called with lock held
|
// Helper to cancel items by name - must be called with lock held
|
||||||
bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_cstr, SchedulerItem::Type type) {
|
bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_cstr, SchedulerItem::Type type) {
|
||||||
|
// Early return if name is invalid - no items to cancel
|
||||||
|
if (name_cstr == nullptr || name_cstr[0] == '\0') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
size_t total_cancelled = 0;
|
size_t total_cancelled = 0;
|
||||||
|
|
||||||
// Check all containers for matching items
|
// Check all containers for matching items
|
||||||
|
@ -150,9 +150,6 @@ class Scheduler {
|
|||||||
return is_static_string ? static_cast<const char *>(name_ptr) : static_cast<const std::string *>(name_ptr)->c_str();
|
return is_static_string ? static_cast<const char *>(name_ptr) : static_cast<const std::string *>(name_ptr)->c_str();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to check if a name is valid (not null and not empty)
|
|
||||||
inline bool is_name_valid_(const char *name) { return name != nullptr && name[0] != '\0'; }
|
|
||||||
|
|
||||||
// Common implementation for cancel operations
|
// Common implementation for cancel operations
|
||||||
bool cancel_item_(Component *component, bool is_static_string, const void *name_ptr, SchedulerItem::Type type);
|
bool cancel_item_(Component *component, bool is_static_string, const void *name_ptr, SchedulerItem::Type type);
|
||||||
|
|
||||||
|
@ -61,6 +61,7 @@ src_filter =
|
|||||||
+<../tests/dummy_main.cpp>
|
+<../tests/dummy_main.cpp>
|
||||||
+<../.temp/all-include.cpp>
|
+<../.temp/all-include.cpp>
|
||||||
lib_ldf_mode = off
|
lib_ldf_mode = off
|
||||||
|
lib_compat_mode = strict
|
||||||
|
|
||||||
; This are common settings for all Arduino-framework based environments.
|
; This are common settings for all Arduino-framework based environments.
|
||||||
[common:arduino]
|
[common:arduino]
|
||||||
|
@ -13,7 +13,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
|
|||||||
esptool==4.9.0
|
esptool==4.9.0
|
||||||
click==8.1.7
|
click==8.1.7
|
||||||
esphome-dashboard==20250514.0
|
esphome-dashboard==20250514.0
|
||||||
aioesphomeapi==34.1.0
|
aioesphomeapi==34.2.0
|
||||||
zeroconf==0.147.0
|
zeroconf==0.147.0
|
||||||
puremagic==1.30
|
puremagic==1.30
|
||||||
ruamel.yaml==0.18.14 # dashboard_import
|
ruamel.yaml==0.18.14 # dashboard_import
|
||||||
|
@ -987,13 +987,24 @@ def build_message_type(
|
|||||||
|
|
||||||
# Add MESSAGE_TYPE method if this is a service message
|
# Add MESSAGE_TYPE method if this is a service message
|
||||||
if message_id is not None:
|
if message_id is not None:
|
||||||
|
# Validate that message_id fits in uint8_t
|
||||||
|
if message_id > 255:
|
||||||
|
raise ValueError(
|
||||||
|
f"Message ID {message_id} for {desc.name} exceeds uint8_t maximum (255)"
|
||||||
|
)
|
||||||
|
|
||||||
# Add static constexpr for message type
|
# Add static constexpr for message type
|
||||||
public_content.append(f"static constexpr uint16_t MESSAGE_TYPE = {message_id};")
|
public_content.append(f"static constexpr uint8_t MESSAGE_TYPE = {message_id};")
|
||||||
|
|
||||||
# Add estimated size constant
|
# Add estimated size constant
|
||||||
estimated_size = calculate_message_estimated_size(desc)
|
estimated_size = calculate_message_estimated_size(desc)
|
||||||
|
# Validate that estimated_size fits in uint8_t
|
||||||
|
if estimated_size > 255:
|
||||||
|
raise ValueError(
|
||||||
|
f"Estimated size {estimated_size} for {desc.name} exceeds uint8_t maximum (255)"
|
||||||
|
)
|
||||||
public_content.append(
|
public_content.append(
|
||||||
f"static constexpr uint16_t ESTIMATED_SIZE = {estimated_size};"
|
f"static constexpr uint8_t ESTIMATED_SIZE = {estimated_size};"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add message_name method inline in header
|
# Add message_name method inline in header
|
||||||
|
@ -270,7 +270,7 @@ def lint_newline(fname):
|
|||||||
return "File contains Windows newline. Please set your editor to Unix newline mode."
|
return "File contains Windows newline. Please set your editor to Unix newline mode."
|
||||||
|
|
||||||
|
|
||||||
@lint_content_check(exclude=["*.svg"])
|
@lint_content_check(exclude=["*.svg", ".clang-tidy.hash"])
|
||||||
def lint_end_newline(fname, content):
|
def lint_end_newline(fname, content):
|
||||||
if content and not content.endswith("\n"):
|
if content and not content.endswith("\n"):
|
||||||
return "File does not end with a newline, please add an empty line at the end of the file."
|
return "File does not end with a newline, please add an empty line at the end of the file."
|
||||||
|
@ -22,6 +22,7 @@ from helpers import (
|
|||||||
git_ls_files,
|
git_ls_files,
|
||||||
load_idedata,
|
load_idedata,
|
||||||
print_error_for_file,
|
print_error_for_file,
|
||||||
|
print_file_list,
|
||||||
root_path,
|
root_path,
|
||||||
temp_header_file,
|
temp_header_file,
|
||||||
)
|
)
|
||||||
@ -218,13 +219,14 @@ def main():
|
|||||||
)
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
idedata = load_idedata(args.environment)
|
|
||||||
options = clang_options(idedata)
|
|
||||||
|
|
||||||
files = []
|
files = []
|
||||||
for path in git_ls_files(["*.cpp"]):
|
for path in git_ls_files(["*.cpp"]):
|
||||||
files.append(os.path.relpath(path, os.getcwd()))
|
files.append(os.path.relpath(path, os.getcwd()))
|
||||||
|
|
||||||
|
# Print initial file count if it's large
|
||||||
|
if len(files) > 50:
|
||||||
|
print(f"Found {len(files)} total files to process")
|
||||||
|
|
||||||
if args.files:
|
if args.files:
|
||||||
# Match against files specified on command-line
|
# Match against files specified on command-line
|
||||||
file_name_re = re.compile("|".join(args.files))
|
file_name_re = re.compile("|".join(args.files))
|
||||||
@ -240,10 +242,28 @@ def main():
|
|||||||
|
|
||||||
if args.split_num:
|
if args.split_num:
|
||||||
files = split_list(files, args.split_num)[args.split_at - 1]
|
files = split_list(files, args.split_num)[args.split_at - 1]
|
||||||
|
print(f"Split {args.split_at}/{args.split_num}: checking {len(files)} files")
|
||||||
|
|
||||||
|
# Print file count before adding header file
|
||||||
|
print(f"\nTotal files to check: {len(files)}")
|
||||||
|
|
||||||
|
# Early exit if no files to check
|
||||||
|
if not files:
|
||||||
|
print("No files to check - exiting early")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# Only build header file if we have actual files to check
|
||||||
if args.all_headers and args.split_at in (None, 1):
|
if args.all_headers and args.split_at in (None, 1):
|
||||||
build_all_include()
|
build_all_include()
|
||||||
files.insert(0, temp_header_file)
|
files.insert(0, temp_header_file)
|
||||||
|
print(f"Added all-include header file, new total: {len(files)}")
|
||||||
|
|
||||||
|
# Print final file list before loading idedata
|
||||||
|
print_file_list(files, "Final files to process:")
|
||||||
|
|
||||||
|
# Load idedata and options only if we have files to check
|
||||||
|
idedata = load_idedata(args.environment)
|
||||||
|
options = clang_options(idedata)
|
||||||
|
|
||||||
tmpdir = None
|
tmpdir = None
|
||||||
if args.fix:
|
if args.fix:
|
||||||
|
188
script/clang_tidy_hash.py
Executable file
188
script/clang_tidy_hash.py
Executable file
@ -0,0 +1,188 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Calculate and manage hash for clang-tidy configuration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Add the script directory to path to import helpers
|
||||||
|
script_dir = Path(__file__).parent
|
||||||
|
sys.path.insert(0, str(script_dir))
|
||||||
|
|
||||||
|
|
||||||
|
def read_file_lines(path: Path) -> list[str]:
|
||||||
|
"""Read lines from a file."""
|
||||||
|
with open(path) as f:
|
||||||
|
return f.readlines()
|
||||||
|
|
||||||
|
|
||||||
|
def parse_requirement_line(line: str) -> tuple[str, str] | None:
|
||||||
|
"""Parse a requirement line and return (package, original_line) or None.
|
||||||
|
|
||||||
|
Handles formats like:
|
||||||
|
- package==1.2.3
|
||||||
|
- package==1.2.3 # comment
|
||||||
|
- package>=1.2.3,<2.0.0
|
||||||
|
"""
|
||||||
|
original_line = line.strip()
|
||||||
|
|
||||||
|
# Extract the part before any comment for parsing
|
||||||
|
parse_line = line
|
||||||
|
if "#" in parse_line:
|
||||||
|
parse_line = parse_line[: parse_line.index("#")]
|
||||||
|
|
||||||
|
parse_line = parse_line.strip()
|
||||||
|
if not parse_line:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Use regex to extract package name
|
||||||
|
# This matches package names followed by version operators
|
||||||
|
match = re.match(r"^([a-zA-Z0-9_-]+)(==|>=|<=|>|<|!=|~=)(.+)$", parse_line)
|
||||||
|
if match:
|
||||||
|
return (match.group(1), original_line) # Return package name and original line
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_clang_tidy_version_from_requirements() -> str:
|
||||||
|
"""Get clang-tidy version from requirements_dev.txt"""
|
||||||
|
requirements_path = Path(__file__).parent.parent / "requirements_dev.txt"
|
||||||
|
lines = read_file_lines(requirements_path)
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
parsed = parse_requirement_line(line)
|
||||||
|
if parsed and parsed[0] == "clang-tidy":
|
||||||
|
# Return the original line (preserves comments)
|
||||||
|
return parsed[1]
|
||||||
|
|
||||||
|
return "clang-tidy version not found"
|
||||||
|
|
||||||
|
|
||||||
|
def extract_platformio_flags() -> str:
|
||||||
|
"""Extract clang-tidy related flags from platformio.ini"""
|
||||||
|
flags: list[str] = []
|
||||||
|
in_clangtidy_section = False
|
||||||
|
|
||||||
|
platformio_path = Path(__file__).parent.parent / "platformio.ini"
|
||||||
|
lines = read_file_lines(platformio_path)
|
||||||
|
for line in lines:
|
||||||
|
line = line.strip()
|
||||||
|
if line.startswith("[flags:clangtidy]"):
|
||||||
|
in_clangtidy_section = True
|
||||||
|
continue
|
||||||
|
elif line.startswith("[") and in_clangtidy_section:
|
||||||
|
break
|
||||||
|
elif in_clangtidy_section and line and not line.startswith("#"):
|
||||||
|
flags.append(line)
|
||||||
|
|
||||||
|
return "\n".join(sorted(flags))
|
||||||
|
|
||||||
|
|
||||||
|
def read_file_bytes(path: Path) -> bytes:
|
||||||
|
"""Read bytes from a file."""
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_clang_tidy_hash() -> str:
|
||||||
|
"""Calculate hash of clang-tidy configuration and version"""
|
||||||
|
hasher = hashlib.sha256()
|
||||||
|
|
||||||
|
# Hash .clang-tidy file
|
||||||
|
clang_tidy_path = Path(__file__).parent.parent / ".clang-tidy"
|
||||||
|
content = read_file_bytes(clang_tidy_path)
|
||||||
|
hasher.update(content)
|
||||||
|
|
||||||
|
# Hash clang-tidy version from requirements_dev.txt
|
||||||
|
version = get_clang_tidy_version_from_requirements()
|
||||||
|
hasher.update(version.encode())
|
||||||
|
|
||||||
|
# Hash relevant platformio.ini sections
|
||||||
|
pio_flags = extract_platformio_flags()
|
||||||
|
hasher.update(pio_flags.encode())
|
||||||
|
|
||||||
|
return hasher.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def read_stored_hash() -> str | None:
|
||||||
|
"""Read the stored hash from file"""
|
||||||
|
hash_file = Path(__file__).parent.parent / ".clang-tidy.hash"
|
||||||
|
if hash_file.exists():
|
||||||
|
lines = read_file_lines(hash_file)
|
||||||
|
return lines[0].strip() if lines else None
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def write_file_content(path: Path, content: str) -> None:
|
||||||
|
"""Write content to a file."""
|
||||||
|
with open(path, "w") as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
|
||||||
|
def write_hash(hash_value: str) -> None:
|
||||||
|
"""Write hash to file"""
|
||||||
|
hash_file = Path(__file__).parent.parent / ".clang-tidy.hash"
|
||||||
|
write_file_content(hash_file, hash_value)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description="Manage clang-tidy configuration hash")
|
||||||
|
parser.add_argument(
|
||||||
|
"--check",
|
||||||
|
action="store_true",
|
||||||
|
help="Check if full scan needed (exit 0 if needed)",
|
||||||
|
)
|
||||||
|
parser.add_argument("--update", action="store_true", help="Update the hash file")
|
||||||
|
parser.add_argument(
|
||||||
|
"--update-if-changed",
|
||||||
|
action="store_true",
|
||||||
|
help="Update hash only if configuration changed (for pre-commit)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--verify", action="store_true", help="Verify hash matches (for CI)"
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
current_hash = calculate_clang_tidy_hash()
|
||||||
|
stored_hash = read_stored_hash()
|
||||||
|
|
||||||
|
if args.check:
|
||||||
|
# Exit 0 if full scan needed (hash changed or no hash file)
|
||||||
|
sys.exit(0 if current_hash != stored_hash else 1)
|
||||||
|
|
||||||
|
elif args.update:
|
||||||
|
write_hash(current_hash)
|
||||||
|
print(f"Hash updated: {current_hash}")
|
||||||
|
|
||||||
|
elif args.update_if_changed:
|
||||||
|
if current_hash != stored_hash:
|
||||||
|
write_hash(current_hash)
|
||||||
|
print(f"Clang-tidy hash updated: {current_hash}")
|
||||||
|
# Exit 0 so pre-commit can stage the file
|
||||||
|
sys.exit(0)
|
||||||
|
else:
|
||||||
|
print("Clang-tidy hash unchanged")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
elif args.verify:
|
||||||
|
if current_hash != stored_hash:
|
||||||
|
print("ERROR: Clang-tidy configuration has changed but hash not updated!")
|
||||||
|
print(f"Expected: {current_hash}")
|
||||||
|
print(f"Found: {stored_hash}")
|
||||||
|
print("\nPlease run: script/clang_tidy_hash.py --update")
|
||||||
|
sys.exit(1)
|
||||||
|
print("Hash verification passed")
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(f"Current hash: {current_hash}")
|
||||||
|
print(f"Stored hash: {stored_hash}")
|
||||||
|
print(f"Match: {current_hash == stored_hash}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
245
script/determine-jobs.py
Executable file
245
script/determine-jobs.py
Executable file
@ -0,0 +1,245 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Determine which CI jobs should run based on changed files.
|
||||||
|
|
||||||
|
This script is a centralized way to determine which CI jobs need to run based on
|
||||||
|
what files have changed. It outputs JSON with the following structure:
|
||||||
|
|
||||||
|
{
|
||||||
|
"integration_tests": true/false,
|
||||||
|
"clang_tidy": true/false,
|
||||||
|
"clang_format": true/false,
|
||||||
|
"python_linters": true/false,
|
||||||
|
"changed_components": ["component1", "component2", ...],
|
||||||
|
"component_test_count": 5
|
||||||
|
}
|
||||||
|
|
||||||
|
The CI workflow uses this information to:
|
||||||
|
- Skip or run integration tests
|
||||||
|
- Skip or run clang-tidy (and whether to do a full scan)
|
||||||
|
- Skip or run clang-format
|
||||||
|
- Skip or run Python linters (ruff, flake8, pylint, pyupgrade)
|
||||||
|
- Determine which components to test individually
|
||||||
|
- Decide how to split component tests (if there are many)
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python script/determine-jobs.py [-b BRANCH]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-b, --branch BRANCH Branch to compare against (default: dev)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from helpers import (
|
||||||
|
CPP_FILE_EXTENSIONS,
|
||||||
|
ESPHOME_COMPONENTS_PATH,
|
||||||
|
PYTHON_FILE_EXTENSIONS,
|
||||||
|
changed_files,
|
||||||
|
get_all_dependencies,
|
||||||
|
get_components_from_integration_fixtures,
|
||||||
|
parse_list_components_output,
|
||||||
|
root_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def should_run_integration_tests(branch: str | None = None) -> bool:
|
||||||
|
"""Determine if integration tests should run based on changed files.
|
||||||
|
|
||||||
|
This function is used by the CI workflow to intelligently skip integration tests when they're
|
||||||
|
not needed, saving significant CI time and resources.
|
||||||
|
|
||||||
|
Integration tests will run when ANY of the following conditions are met:
|
||||||
|
|
||||||
|
1. Core C++ files changed (esphome/core/*)
|
||||||
|
- Any .cpp, .h, .tcc files in the core directory
|
||||||
|
- These files contain fundamental functionality used throughout ESPHome
|
||||||
|
- Examples: esphome/core/component.cpp, esphome/core/application.h
|
||||||
|
|
||||||
|
2. Core Python files changed (esphome/core/*.py)
|
||||||
|
- Only .py files in the esphome/core/ directory
|
||||||
|
- These are core Python files that affect the entire system
|
||||||
|
- Examples: esphome/core/config.py, esphome/core/__init__.py
|
||||||
|
- NOT included: esphome/*.py, esphome/dashboard/*.py, esphome/components/*/*.py
|
||||||
|
|
||||||
|
3. Integration test files changed
|
||||||
|
- Any file in tests/integration/ directory
|
||||||
|
- This includes test files themselves and fixture YAML files
|
||||||
|
- Examples: tests/integration/test_api.py, tests/integration/fixtures/api.yaml
|
||||||
|
|
||||||
|
4. Components used by integration tests (or their dependencies) changed
|
||||||
|
- The function parses all YAML files in tests/integration/fixtures/
|
||||||
|
- Extracts which components are used in integration tests
|
||||||
|
- Recursively finds all dependencies of those components
|
||||||
|
- If any of these components have changes, tests must run
|
||||||
|
- Example: If api.yaml uses 'sensor' and 'api' components, and 'api' depends on 'socket',
|
||||||
|
then changes to sensor/, api/, or socket/ components trigger tests
|
||||||
|
|
||||||
|
Args:
|
||||||
|
branch: Branch to compare against. If None, uses default.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if integration tests should run, False otherwise.
|
||||||
|
"""
|
||||||
|
files = changed_files(branch)
|
||||||
|
|
||||||
|
# Check if any core files changed (esphome/core/*)
|
||||||
|
for file in files:
|
||||||
|
if file.startswith("esphome/core/"):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Check if any integration test files changed
|
||||||
|
if any("tests/integration" in file for file in files):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Get all components used in integration tests and their dependencies
|
||||||
|
fixture_components = get_components_from_integration_fixtures()
|
||||||
|
all_required_components = get_all_dependencies(fixture_components)
|
||||||
|
|
||||||
|
# Check if any required components changed
|
||||||
|
for file in files:
|
||||||
|
if file.startswith(ESPHOME_COMPONENTS_PATH):
|
||||||
|
parts = file.split("/")
|
||||||
|
if len(parts) >= 3:
|
||||||
|
component = parts[2]
|
||||||
|
if component in all_required_components:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def should_run_clang_tidy(branch: str | None = None) -> bool:
|
||||||
|
"""Determine if clang-tidy should run based on changed files.
|
||||||
|
|
||||||
|
This function is used by the CI workflow to intelligently skip clang-tidy checks when they're
|
||||||
|
not needed, saving significant CI time and resources.
|
||||||
|
|
||||||
|
Clang-tidy will run when ANY of the following conditions are met:
|
||||||
|
|
||||||
|
1. Clang-tidy configuration changed
|
||||||
|
- The hash of .clang-tidy configuration file has changed
|
||||||
|
- The hash includes the .clang-tidy file, clang-tidy version from requirements_dev.txt,
|
||||||
|
and relevant platformio.ini sections
|
||||||
|
- When configuration changes, a full scan is needed to ensure all code complies
|
||||||
|
with the new rules
|
||||||
|
- Detected by script/clang_tidy_hash.py --check returning exit code 0
|
||||||
|
|
||||||
|
2. Any C++ source files changed
|
||||||
|
- Any file with C++ extensions: .cpp, .h, .hpp, .cc, .cxx, .c, .tcc
|
||||||
|
- Includes files anywhere in the repository, not just in esphome/
|
||||||
|
- This ensures all C++ code is checked, including tests, examples, etc.
|
||||||
|
- Examples: esphome/core/component.cpp, tests/custom/my_component.h
|
||||||
|
|
||||||
|
If the hash check fails for any reason, clang-tidy runs as a safety measure to ensure
|
||||||
|
code quality is maintained.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
branch: Branch to compare against. If None, uses default.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if clang-tidy should run, False otherwise.
|
||||||
|
"""
|
||||||
|
# First check if clang-tidy configuration changed (full scan needed)
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
[os.path.join(root_path, "script", "clang_tidy_hash.py"), "--check"],
|
||||||
|
capture_output=True,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
# Exit 0 means hash changed (full scan needed)
|
||||||
|
if result.returncode == 0:
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
# If hash check fails, run clang-tidy to be safe
|
||||||
|
return True
|
||||||
|
|
||||||
|
return _any_changed_file_endswith(branch, CPP_FILE_EXTENSIONS)
|
||||||
|
|
||||||
|
|
||||||
|
def should_run_clang_format(branch: str | None = None) -> bool:
|
||||||
|
"""Determine if clang-format should run based on changed files.
|
||||||
|
|
||||||
|
This function is used by the CI workflow to skip clang-format checks when no C++ files
|
||||||
|
have changed, saving CI time and resources.
|
||||||
|
|
||||||
|
Clang-format will run when any C++ source files have changed.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
branch: Branch to compare against. If None, uses default.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if clang-format should run, False otherwise.
|
||||||
|
"""
|
||||||
|
return _any_changed_file_endswith(branch, CPP_FILE_EXTENSIONS)
|
||||||
|
|
||||||
|
|
||||||
|
def should_run_python_linters(branch: str | None = None) -> bool:
|
||||||
|
"""Determine if Python linters (ruff, flake8, pylint, pyupgrade) should run based on changed files.
|
||||||
|
|
||||||
|
This function is used by the CI workflow to skip Python linting checks when no Python files
|
||||||
|
have changed, saving CI time and resources.
|
||||||
|
|
||||||
|
Python linters will run when any Python source files have changed.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
branch: Branch to compare against. If None, uses default.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if Python linters should run, False otherwise.
|
||||||
|
"""
|
||||||
|
return _any_changed_file_endswith(branch, PYTHON_FILE_EXTENSIONS)
|
||||||
|
|
||||||
|
|
||||||
|
def _any_changed_file_endswith(branch: str | None, extensions: tuple[str, ...]) -> bool:
|
||||||
|
"""Check if a changed file ends with any of the specified extensions."""
|
||||||
|
return any(file.endswith(extensions) for file in changed_files(branch))
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""Main function that determines which CI jobs to run."""
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Determine which CI jobs should run based on changed files"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-b", "--branch", help="Branch to compare changed files against"
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Determine what should run
|
||||||
|
run_integration = should_run_integration_tests(args.branch)
|
||||||
|
run_clang_tidy = should_run_clang_tidy(args.branch)
|
||||||
|
run_clang_format = should_run_clang_format(args.branch)
|
||||||
|
run_python_linters = should_run_python_linters(args.branch)
|
||||||
|
|
||||||
|
# Get changed components using list-components.py for exact compatibility
|
||||||
|
script_path = Path(__file__).parent / "list-components.py"
|
||||||
|
cmd = [sys.executable, str(script_path), "--changed"]
|
||||||
|
if args.branch:
|
||||||
|
cmd.extend(["-b", args.branch])
|
||||||
|
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
||||||
|
changed_components = parse_list_components_output(result.stdout)
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
output: dict[str, Any] = {
|
||||||
|
"integration_tests": run_integration,
|
||||||
|
"clang_tidy": run_clang_tidy,
|
||||||
|
"clang_format": run_clang_format,
|
||||||
|
"python_linters": run_python_linters,
|
||||||
|
"changed_components": changed_components,
|
||||||
|
"component_test_count": len(changed_components),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Output as JSON
|
||||||
|
print(json.dumps(output))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
@ -1,8 +1,14 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from functools import cache
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import os.path
|
import os.path
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import time
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import colorama
|
import colorama
|
||||||
|
|
||||||
@ -11,14 +17,42 @@ basepath = os.path.join(root_path, "esphome")
|
|||||||
temp_folder = os.path.join(root_path, ".temp")
|
temp_folder = os.path.join(root_path, ".temp")
|
||||||
temp_header_file = os.path.join(temp_folder, "all-include.cpp")
|
temp_header_file = os.path.join(temp_folder, "all-include.cpp")
|
||||||
|
|
||||||
|
# C++ file extensions used for clang-tidy and clang-format checks
|
||||||
|
CPP_FILE_EXTENSIONS = (".cpp", ".h", ".hpp", ".cc", ".cxx", ".c", ".tcc")
|
||||||
|
|
||||||
def styled(color, msg, reset=True):
|
# Python file extensions
|
||||||
|
PYTHON_FILE_EXTENSIONS = (".py", ".pyi")
|
||||||
|
|
||||||
|
# YAML file extensions
|
||||||
|
YAML_FILE_EXTENSIONS = (".yaml", ".yml")
|
||||||
|
|
||||||
|
# Component path prefix
|
||||||
|
ESPHOME_COMPONENTS_PATH = "esphome/components/"
|
||||||
|
|
||||||
|
|
||||||
|
def parse_list_components_output(output: str) -> list[str]:
|
||||||
|
"""Parse the output from list-components.py script.
|
||||||
|
|
||||||
|
The script outputs one component name per line.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
output: The stdout from list-components.py
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of component names, or empty list if no output
|
||||||
|
"""
|
||||||
|
if not output or not output.strip():
|
||||||
|
return []
|
||||||
|
return [c.strip() for c in output.strip().split("\n") if c.strip()]
|
||||||
|
|
||||||
|
|
||||||
|
def styled(color: str | tuple[str, ...], msg: str, reset: bool = True) -> str:
|
||||||
prefix = "".join(color) if isinstance(color, tuple) else color
|
prefix = "".join(color) if isinstance(color, tuple) else color
|
||||||
suffix = colorama.Style.RESET_ALL if reset else ""
|
suffix = colorama.Style.RESET_ALL if reset else ""
|
||||||
return prefix + msg + suffix
|
return prefix + msg + suffix
|
||||||
|
|
||||||
|
|
||||||
def print_error_for_file(file, body):
|
def print_error_for_file(file: str, body: str | None) -> None:
|
||||||
print(
|
print(
|
||||||
styled(colorama.Fore.GREEN, "### File ")
|
styled(colorama.Fore.GREEN, "### File ")
|
||||||
+ styled((colorama.Fore.GREEN, colorama.Style.BRIGHT), file)
|
+ styled((colorama.Fore.GREEN, colorama.Style.BRIGHT), file)
|
||||||
@ -29,17 +63,22 @@ def print_error_for_file(file, body):
|
|||||||
print()
|
print()
|
||||||
|
|
||||||
|
|
||||||
def build_all_include():
|
def build_all_include() -> None:
|
||||||
# Build a cpp file that includes all header files in this repo.
|
# Build a cpp file that includes all header files in this repo.
|
||||||
# Otherwise header-only integrations would not be tested by clang-tidy
|
# Otherwise header-only integrations would not be tested by clang-tidy
|
||||||
headers = []
|
|
||||||
for path in walk_files(basepath):
|
# Use git ls-files to find all .h files in the esphome directory
|
||||||
filetypes = (".h",)
|
# This is much faster than walking the filesystem
|
||||||
ext = os.path.splitext(path)[1]
|
cmd = ["git", "ls-files", "esphome/**/*.h"]
|
||||||
if ext in filetypes:
|
proc = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
||||||
path = os.path.relpath(path, root_path)
|
|
||||||
include_p = path.replace(os.path.sep, "/")
|
# Process git output - git already returns paths relative to repo root
|
||||||
headers.append(f'#include "{include_p}"')
|
headers = [
|
||||||
|
f'#include "{include_p}"'
|
||||||
|
for line in proc.stdout.strip().split("\n")
|
||||||
|
if (include_p := line.replace(os.path.sep, "/"))
|
||||||
|
]
|
||||||
|
|
||||||
headers.sort()
|
headers.sort()
|
||||||
headers.append("")
|
headers.append("")
|
||||||
content = "\n".join(headers)
|
content = "\n".join(headers)
|
||||||
@ -48,29 +87,87 @@ def build_all_include():
|
|||||||
p.write_text(content, encoding="utf-8")
|
p.write_text(content, encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
def walk_files(path):
|
def get_output(*args: str) -> str:
|
||||||
for root, _, files in os.walk(path):
|
|
||||||
for name in files:
|
|
||||||
yield os.path.join(root, name)
|
|
||||||
|
|
||||||
|
|
||||||
def get_output(*args):
|
|
||||||
with subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc:
|
with subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc:
|
||||||
output, _ = proc.communicate()
|
output, _ = proc.communicate()
|
||||||
return output.decode("utf-8")
|
return output.decode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
def get_err(*args):
|
def get_err(*args: str) -> str:
|
||||||
with subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc:
|
with subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc:
|
||||||
_, err = proc.communicate()
|
_, err = proc.communicate()
|
||||||
return err.decode("utf-8")
|
return err.decode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
def splitlines_no_ends(string):
|
def splitlines_no_ends(string: str) -> list[str]:
|
||||||
return [s.strip() for s in string.splitlines()]
|
return [s.strip() for s in string.splitlines()]
|
||||||
|
|
||||||
|
|
||||||
def changed_files(branch="dev"):
|
def _get_pr_number_from_github_env() -> str | None:
|
||||||
|
"""Extract PR number from GitHub environment variables.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PR number as string, or None if not found
|
||||||
|
"""
|
||||||
|
# First try parsing GITHUB_REF (fastest)
|
||||||
|
github_ref = os.environ.get("GITHUB_REF", "")
|
||||||
|
if "/pull/" in github_ref:
|
||||||
|
return github_ref.split("/pull/")[1].split("/")[0]
|
||||||
|
|
||||||
|
# Fallback to GitHub event file
|
||||||
|
github_event_path = os.environ.get("GITHUB_EVENT_PATH")
|
||||||
|
if github_event_path and os.path.exists(github_event_path):
|
||||||
|
with open(github_event_path) as f:
|
||||||
|
event_data = json.load(f)
|
||||||
|
pr_data = event_data.get("pull_request", {})
|
||||||
|
if pr_number := pr_data.get("number"):
|
||||||
|
return str(pr_number)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@cache
|
||||||
|
def _get_changed_files_github_actions() -> list[str] | None:
|
||||||
|
"""Get changed files in GitHub Actions environment.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of changed files, or None if should fall back to git method
|
||||||
|
"""
|
||||||
|
event_name = os.environ.get("GITHUB_EVENT_NAME")
|
||||||
|
|
||||||
|
# For pull requests
|
||||||
|
if event_name == "pull_request":
|
||||||
|
pr_number = _get_pr_number_from_github_env()
|
||||||
|
if pr_number:
|
||||||
|
# Use GitHub CLI to get changed files directly
|
||||||
|
cmd = ["gh", "pr", "diff", pr_number, "--name-only"]
|
||||||
|
return _get_changed_files_from_command(cmd)
|
||||||
|
|
||||||
|
# For pushes (including squash-and-merge)
|
||||||
|
elif event_name == "push":
|
||||||
|
# For push events, we want to check what changed in this commit
|
||||||
|
try:
|
||||||
|
# Get the changed files in the last commit
|
||||||
|
return _get_changed_files_from_command(
|
||||||
|
["git", "diff", "HEAD~1..HEAD", "--name-only"]
|
||||||
|
)
|
||||||
|
except: # noqa: E722
|
||||||
|
# Fall back to the original method if this fails
|
||||||
|
pass
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def changed_files(branch: str | None = None) -> list[str]:
|
||||||
|
# In GitHub Actions, we can use the API to get changed files more efficiently
|
||||||
|
if os.environ.get("GITHUB_ACTIONS") == "true":
|
||||||
|
github_files = _get_changed_files_github_actions()
|
||||||
|
if github_files is not None:
|
||||||
|
return github_files
|
||||||
|
|
||||||
|
# Original implementation for local development
|
||||||
|
if not branch: # Treat None and empty string the same
|
||||||
|
branch = "dev"
|
||||||
check_remotes = ["upstream", "origin"]
|
check_remotes = ["upstream", "origin"]
|
||||||
check_remotes.extend(splitlines_no_ends(get_output("git", "remote")))
|
check_remotes.extend(splitlines_no_ends(get_output("git", "remote")))
|
||||||
for remote in check_remotes:
|
for remote in check_remotes:
|
||||||
@ -83,25 +180,165 @@ def changed_files(branch="dev"):
|
|||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
raise ValueError("Git not configured")
|
raise ValueError("Git not configured")
|
||||||
command = ["git", "diff", merge_base, "--name-only"]
|
return _get_changed_files_from_command(["git", "diff", merge_base, "--name-only"])
|
||||||
changed = splitlines_no_ends(get_output(*command))
|
|
||||||
changed = [os.path.relpath(f, os.getcwd()) for f in changed]
|
|
||||||
changed.sort()
|
|
||||||
return changed
|
|
||||||
|
|
||||||
|
|
||||||
def filter_changed(files):
|
def _get_changed_files_from_command(command: list[str]) -> list[str]:
|
||||||
|
"""Run a git command to get changed files and return them as a list."""
|
||||||
|
proc = subprocess.run(command, capture_output=True, text=True, check=False)
|
||||||
|
if proc.returncode != 0:
|
||||||
|
raise Exception(f"Command failed: {' '.join(command)}\nstderr: {proc.stderr}")
|
||||||
|
|
||||||
|
changed_files = splitlines_no_ends(proc.stdout)
|
||||||
|
changed_files = [os.path.relpath(f, os.getcwd()) for f in changed_files if f]
|
||||||
|
changed_files.sort()
|
||||||
|
return changed_files
|
||||||
|
|
||||||
|
|
||||||
|
def get_changed_components() -> list[str] | None:
|
||||||
|
"""Get list of changed components using list-components.py script.
|
||||||
|
|
||||||
|
This function:
|
||||||
|
1. First checks if any core C++/header files (esphome/core/*.{cpp,h,hpp,cc,cxx,c}) changed - if so, returns None
|
||||||
|
2. Otherwise delegates to ./script/list-components.py --changed which:
|
||||||
|
- Analyzes all changed files
|
||||||
|
- Determines which components are affected (including dependencies)
|
||||||
|
- Returns a list of component names that need to be checked
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- None: Core C++/header files changed, need full scan
|
||||||
|
- Empty list: No components changed (only non-component files changed)
|
||||||
|
- List of strings: Names of components that need checking (e.g., ["wifi", "mqtt"])
|
||||||
|
"""
|
||||||
|
# Check if any core C++ or header files changed first
|
||||||
changed = changed_files()
|
changed = changed_files()
|
||||||
files = [f for f in files if f in changed]
|
core_cpp_changed = any(
|
||||||
print("Changed files:")
|
f.startswith("esphome/core/")
|
||||||
if not files:
|
and f.endswith(CPP_FILE_EXTENSIONS[:-1]) # Exclude .tcc for core files
|
||||||
print(" No changed files!")
|
for f in changed
|
||||||
for c in files:
|
)
|
||||||
print(f" {c}")
|
if core_cpp_changed:
|
||||||
|
print("Core C++/header files changed - will run full clang-tidy scan")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Use list-components.py to get changed components
|
||||||
|
script_path = os.path.join(root_path, "script", "list-components.py")
|
||||||
|
cmd = [script_path, "--changed"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd, capture_output=True, text=True, check=True, close_fds=False
|
||||||
|
)
|
||||||
|
return parse_list_components_output(result.stdout)
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
# If the script fails, fall back to full scan
|
||||||
|
print("Could not determine changed components - will run full clang-tidy scan")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _filter_changed_ci(files: list[str]) -> list[str]:
|
||||||
|
"""Filter files based on changed components in CI environment.
|
||||||
|
|
||||||
|
This function implements intelligent filtering to reduce CI runtime by only
|
||||||
|
checking files that could be affected by the changes. It handles three scenarios:
|
||||||
|
|
||||||
|
1. Core C++/header files changed (returns None from get_changed_components):
|
||||||
|
- Triggered when any C++/header file in esphome/core/ is modified
|
||||||
|
- Action: Check ALL files (full scan)
|
||||||
|
- Reason: Core C++/header files are used throughout the codebase
|
||||||
|
|
||||||
|
2. No components changed (returns empty list from get_changed_components):
|
||||||
|
- Triggered when only non-component files changed (e.g., scripts, configs)
|
||||||
|
- Action: Check only the specific non-component files that changed
|
||||||
|
- Example: If only script/clang-tidy changed, only check that file
|
||||||
|
|
||||||
|
3. Specific components changed (returns list of component names):
|
||||||
|
- Component detection done by: ./script/list-components.py --changed
|
||||||
|
- That script analyzes which components are affected by the changed files
|
||||||
|
INCLUDING their dependencies
|
||||||
|
- Action: Check ALL files in each component that list-components.py identifies
|
||||||
|
- Example: If wifi.cpp changed, list-components.py might return ["wifi", "network"]
|
||||||
|
if network depends on wifi. We then check ALL files in both
|
||||||
|
esphome/components/wifi/ and esphome/components/network/
|
||||||
|
- Reason: Component files often have interdependencies (headers, base classes)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
files: List of all files that clang-tidy would normally check
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Filtered list of files to check
|
||||||
|
"""
|
||||||
|
components = get_changed_components()
|
||||||
|
if components is None:
|
||||||
|
# Scenario 1: Core files changed or couldn't determine components
|
||||||
|
# Action: Return all files for full scan
|
||||||
|
return files
|
||||||
|
|
||||||
|
if not components:
|
||||||
|
# Scenario 2: No components changed - only non-component files changed
|
||||||
|
# Action: Check only the specific non-component files that changed
|
||||||
|
changed = changed_files()
|
||||||
|
files = [
|
||||||
|
f
|
||||||
|
for f in files
|
||||||
|
if f in changed and not f.startswith(ESPHOME_COMPONENTS_PATH)
|
||||||
|
]
|
||||||
|
if not files:
|
||||||
|
print("No files changed")
|
||||||
|
return files
|
||||||
|
|
||||||
|
# Scenario 3: Specific components changed
|
||||||
|
# Action: Check ALL files in each changed component
|
||||||
|
# Convert component list to set for O(1) lookups
|
||||||
|
component_set = set(components)
|
||||||
|
print(f"Changed components: {', '.join(sorted(components))}")
|
||||||
|
|
||||||
|
# The 'files' parameter contains ALL files in the codebase that clang-tidy would check.
|
||||||
|
# We filter this down to only files in the changed components.
|
||||||
|
# We check ALL files in each changed component (not just the changed files)
|
||||||
|
# because changes in one file can affect other files in the same component.
|
||||||
|
filtered_files = []
|
||||||
|
for f in files:
|
||||||
|
if f.startswith(ESPHOME_COMPONENTS_PATH):
|
||||||
|
# Check if file belongs to any of the changed components
|
||||||
|
parts = f.split("/")
|
||||||
|
if len(parts) >= 3 and parts[2] in component_set:
|
||||||
|
filtered_files.append(f)
|
||||||
|
|
||||||
|
return filtered_files
|
||||||
|
|
||||||
|
|
||||||
|
def _filter_changed_local(files: list[str]) -> list[str]:
|
||||||
|
"""Filter files based on git changes for local development.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
files: List of all files to filter
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Filtered list of files to check
|
||||||
|
"""
|
||||||
|
# For local development, just check changed files directly
|
||||||
|
changed = changed_files()
|
||||||
|
return [f for f in files if f in changed]
|
||||||
|
|
||||||
|
|
||||||
|
def filter_changed(files: list[str]) -> list[str]:
|
||||||
|
"""Filter files to only those that changed or are in changed components.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
files: List of files to filter
|
||||||
|
"""
|
||||||
|
# When running from CI, use component-based filtering
|
||||||
|
if os.environ.get("GITHUB_ACTIONS") == "true":
|
||||||
|
files = _filter_changed_ci(files)
|
||||||
|
else:
|
||||||
|
files = _filter_changed_local(files)
|
||||||
|
|
||||||
|
print_file_list(files, "Files to check after filtering:")
|
||||||
return files
|
return files
|
||||||
|
|
||||||
|
|
||||||
def filter_grep(files, value):
|
def filter_grep(files: list[str], value: str) -> list[str]:
|
||||||
matched = []
|
matched = []
|
||||||
for file in files:
|
for file in files:
|
||||||
with open(file, encoding="utf-8") as handle:
|
with open(file, encoding="utf-8") as handle:
|
||||||
@ -111,7 +348,7 @@ def filter_grep(files, value):
|
|||||||
return matched
|
return matched
|
||||||
|
|
||||||
|
|
||||||
def git_ls_files(patterns=None):
|
def git_ls_files(patterns: list[str] | None = None) -> dict[str, int]:
|
||||||
command = ["git", "ls-files", "-s"]
|
command = ["git", "ls-files", "-s"]
|
||||||
if patterns is not None:
|
if patterns is not None:
|
||||||
command.extend(patterns)
|
command.extend(patterns)
|
||||||
@ -121,7 +358,10 @@ def git_ls_files(patterns=None):
|
|||||||
return {s[3].strip(): int(s[0]) for s in lines}
|
return {s[3].strip(): int(s[0]) for s in lines}
|
||||||
|
|
||||||
|
|
||||||
def load_idedata(environment):
|
def load_idedata(environment: str) -> dict[str, Any]:
|
||||||
|
start_time = time.time()
|
||||||
|
print(f"Loading IDE data for environment '{environment}'...")
|
||||||
|
|
||||||
platformio_ini = Path(root_path) / "platformio.ini"
|
platformio_ini = Path(root_path) / "platformio.ini"
|
||||||
temp_idedata = Path(temp_folder) / f"idedata-{environment}.json"
|
temp_idedata = Path(temp_folder) / f"idedata-{environment}.json"
|
||||||
changed = False
|
changed = False
|
||||||
@ -142,7 +382,10 @@ def load_idedata(environment):
|
|||||||
changed = True
|
changed = True
|
||||||
|
|
||||||
if not changed:
|
if not changed:
|
||||||
return json.loads(temp_idedata.read_text())
|
data = json.loads(temp_idedata.read_text())
|
||||||
|
elapsed = time.time() - start_time
|
||||||
|
print(f"IDE data loaded from cache in {elapsed:.2f} seconds")
|
||||||
|
return data
|
||||||
|
|
||||||
# ensure temp directory exists before running pio, as it writes sdkconfig to it
|
# ensure temp directory exists before running pio, as it writes sdkconfig to it
|
||||||
Path(temp_folder).mkdir(exist_ok=True)
|
Path(temp_folder).mkdir(exist_ok=True)
|
||||||
@ -158,6 +401,9 @@ def load_idedata(environment):
|
|||||||
match = re.search(r'{\s*".*}', stdout.decode("utf-8"))
|
match = re.search(r'{\s*".*}', stdout.decode("utf-8"))
|
||||||
data = json.loads(match.group())
|
data = json.loads(match.group())
|
||||||
temp_idedata.write_text(json.dumps(data, indent=2) + "\n")
|
temp_idedata.write_text(json.dumps(data, indent=2) + "\n")
|
||||||
|
|
||||||
|
elapsed = time.time() - start_time
|
||||||
|
print(f"IDE data generated and cached in {elapsed:.2f} seconds")
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
@ -196,6 +442,29 @@ def get_binary(name: str, version: str) -> str:
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def print_file_list(
|
||||||
|
files: list[str], title: str = "Files:", max_files: int = 20
|
||||||
|
) -> None:
|
||||||
|
"""Print a list of files with optional truncation for large lists.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
files: List of file paths to print
|
||||||
|
title: Title to print before the list
|
||||||
|
max_files: Maximum number of files to show before truncating (default: 20)
|
||||||
|
"""
|
||||||
|
print(title)
|
||||||
|
if not files:
|
||||||
|
print(" No files to check!")
|
||||||
|
elif len(files) <= max_files:
|
||||||
|
for f in sorted(files):
|
||||||
|
print(f" {f}")
|
||||||
|
else:
|
||||||
|
sorted_files = sorted(files)
|
||||||
|
for f in sorted_files[:10]:
|
||||||
|
print(f" {f}")
|
||||||
|
print(f" ... and {len(files) - 10} more files")
|
||||||
|
|
||||||
|
|
||||||
def get_usable_cpu_count() -> int:
|
def get_usable_cpu_count() -> int:
|
||||||
"""Return the number of CPUs that can be used for processes.
|
"""Return the number of CPUs that can be used for processes.
|
||||||
|
|
||||||
@ -205,3 +474,83 @@ def get_usable_cpu_count() -> int:
|
|||||||
return (
|
return (
|
||||||
os.process_cpu_count() if hasattr(os, "process_cpu_count") else os.cpu_count()
|
os.process_cpu_count() if hasattr(os, "process_cpu_count") else os.cpu_count()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_dependencies(component_names: set[str]) -> set[str]:
|
||||||
|
"""Get all dependencies for a set of components.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
component_names: Set of component names to get dependencies for
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Set of all components including dependencies and auto-loaded components
|
||||||
|
"""
|
||||||
|
from esphome.const import KEY_CORE
|
||||||
|
from esphome.core import CORE
|
||||||
|
from esphome.loader import get_component
|
||||||
|
|
||||||
|
all_components: set[str] = set(component_names)
|
||||||
|
|
||||||
|
# Reset CORE to ensure clean state
|
||||||
|
CORE.reset()
|
||||||
|
|
||||||
|
# Set up fake config path for component loading
|
||||||
|
root = Path(__file__).parent.parent
|
||||||
|
CORE.config_path = str(root)
|
||||||
|
CORE.data[KEY_CORE] = {}
|
||||||
|
|
||||||
|
# Keep finding dependencies until no new ones are found
|
||||||
|
while True:
|
||||||
|
new_components: set[str] = set()
|
||||||
|
|
||||||
|
for comp_name in all_components:
|
||||||
|
comp = get_component(comp_name)
|
||||||
|
if not comp:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Add dependencies (extract component name before '.')
|
||||||
|
new_components.update(dep.split(".")[0] for dep in comp.dependencies)
|
||||||
|
|
||||||
|
# Add auto_load components
|
||||||
|
new_components.update(comp.auto_load)
|
||||||
|
|
||||||
|
# Check if we found any new components
|
||||||
|
new_components -= all_components
|
||||||
|
if not new_components:
|
||||||
|
break
|
||||||
|
|
||||||
|
all_components.update(new_components)
|
||||||
|
|
||||||
|
return all_components
|
||||||
|
|
||||||
|
|
||||||
|
def get_components_from_integration_fixtures() -> set[str]:
|
||||||
|
"""Extract all components used in integration test fixtures.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Set of component names used in integration test fixtures
|
||||||
|
"""
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
components: set[str] = set()
|
||||||
|
fixtures_dir = Path(__file__).parent.parent / "tests" / "integration" / "fixtures"
|
||||||
|
|
||||||
|
for yaml_file in fixtures_dir.glob("*.yaml"):
|
||||||
|
with open(yaml_file) as f:
|
||||||
|
config: dict[str, any] | None = yaml.safe_load(f)
|
||||||
|
if not config:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Add all top-level component keys
|
||||||
|
components.update(config.keys())
|
||||||
|
|
||||||
|
# Add platform components (e.g., output.template)
|
||||||
|
for value in config.values():
|
||||||
|
if not isinstance(value, list):
|
||||||
|
continue
|
||||||
|
|
||||||
|
for item in value:
|
||||||
|
if isinstance(item, dict) and "platform" in item:
|
||||||
|
components.add(item["platform"])
|
||||||
|
|
||||||
|
return components
|
||||||
|
@ -20,6 +20,12 @@ def filter_component_files(str):
|
|||||||
return str.startswith("esphome/components/") | str.startswith("tests/components/")
|
return str.startswith("esphome/components/") | str.startswith("tests/components/")
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_component_files() -> list[str]:
|
||||||
|
"""Get all component files from git."""
|
||||||
|
files = git_ls_files()
|
||||||
|
return list(filter(filter_component_files, files))
|
||||||
|
|
||||||
|
|
||||||
def extract_component_names_array_from_files_array(files):
|
def extract_component_names_array_from_files_array(files):
|
||||||
components = []
|
components = []
|
||||||
for file in files:
|
for file in files:
|
||||||
@ -165,17 +171,20 @@ def main():
|
|||||||
if args.branch and not args.changed:
|
if args.branch and not args.changed:
|
||||||
parser.error("--branch requires --changed")
|
parser.error("--branch requires --changed")
|
||||||
|
|
||||||
files = git_ls_files()
|
|
||||||
files = filter(filter_component_files, files)
|
|
||||||
|
|
||||||
if args.changed:
|
if args.changed:
|
||||||
if args.branch:
|
# When --changed is passed, only get the changed files
|
||||||
changed = changed_files(args.branch)
|
changed = changed_files(args.branch)
|
||||||
else:
|
|
||||||
changed = changed_files()
|
|
||||||
# If any base test file(s) changed, there's no need to filter out components
|
# If any base test file(s) changed, there's no need to filter out components
|
||||||
if not any("tests/test_build_components" in file for file in changed):
|
if any("tests/test_build_components" in file for file in changed):
|
||||||
files = [f for f in files if f in changed]
|
# Need to get all component files
|
||||||
|
files = get_all_component_files()
|
||||||
|
else:
|
||||||
|
# Only look at changed component files
|
||||||
|
files = [f for f in changed if filter_component_files(f)]
|
||||||
|
else:
|
||||||
|
# Get all component files
|
||||||
|
files = get_all_component_files()
|
||||||
|
|
||||||
for c in get_components(files, args.changed):
|
for c in get_components(files, args.changed):
|
||||||
print(c)
|
print(c)
|
||||||
|
@ -1,29 +1,71 @@
|
|||||||
"""Fixtures for component tests."""
|
"""Fixtures for component tests."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable, Generator
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
# Add package root to python path
|
# Add package root to python path
|
||||||
here = Path(__file__).parent
|
here = Path(__file__).parent
|
||||||
package_root = here.parent.parent
|
package_root = here.parent.parent
|
||||||
sys.path.insert(0, package_root.as_posix())
|
sys.path.insert(0, package_root.as_posix())
|
||||||
|
|
||||||
import pytest # noqa: E402
|
|
||||||
|
|
||||||
from esphome.__main__ import generate_cpp_contents # noqa: E402
|
from esphome.__main__ import generate_cpp_contents # noqa: E402
|
||||||
from esphome.config import read_config # noqa: E402
|
from esphome.config import read_config # noqa: E402
|
||||||
from esphome.core import CORE # noqa: E402
|
from esphome.core import CORE # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def config_path(request: pytest.FixtureRequest) -> Generator[None]:
|
||||||
|
"""Set CORE.config_path to the component's config directory and reset it after the test."""
|
||||||
|
original_path = CORE.config_path
|
||||||
|
config_dir = Path(request.fspath).parent / "config"
|
||||||
|
|
||||||
|
# Check if config directory exists, if not use parent directory
|
||||||
|
if config_dir.exists():
|
||||||
|
# Set config_path to a dummy yaml file in the config directory
|
||||||
|
# This ensures CORE.config_dir points to the config directory
|
||||||
|
CORE.config_path = str(config_dir / "dummy.yaml")
|
||||||
|
else:
|
||||||
|
CORE.config_path = str(Path(request.fspath).parent / "dummy.yaml")
|
||||||
|
|
||||||
|
yield
|
||||||
|
CORE.config_path = original_path
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def generate_main():
|
def component_fixture_path(request: pytest.FixtureRequest) -> Callable[[str], Path]:
|
||||||
|
"""Return a function to get absolute paths relative to the component's fixtures directory."""
|
||||||
|
|
||||||
|
def _get_path(file_name: str) -> Path:
|
||||||
|
"""Get the absolute path of a file relative to the component's fixtures directory."""
|
||||||
|
return (Path(request.fspath).parent / "fixtures" / file_name).absolute()
|
||||||
|
|
||||||
|
return _get_path
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def component_config_path(request: pytest.FixtureRequest) -> Callable[[str], Path]:
|
||||||
|
"""Return a function to get absolute paths relative to the component's config directory."""
|
||||||
|
|
||||||
|
def _get_path(file_name: str) -> Path:
|
||||||
|
"""Get the absolute path of a file relative to the component's config directory."""
|
||||||
|
return (Path(request.fspath).parent / "config" / file_name).absolute()
|
||||||
|
|
||||||
|
return _get_path
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def generate_main() -> Generator[Callable[[str | Path], str]]:
|
||||||
"""Generates the C++ main.cpp file and returns it in string form."""
|
"""Generates the C++ main.cpp file and returns it in string form."""
|
||||||
|
|
||||||
def generator(path: str) -> str:
|
def generator(path: str | Path) -> str:
|
||||||
CORE.config_path = path
|
CORE.config_path = str(path)
|
||||||
CORE.config = read_config({})
|
CORE.config = read_config({})
|
||||||
generate_cpp_contents(CORE.config)
|
generate_cpp_contents(CORE.config)
|
||||||
print(CORE.cpp_main_section)
|
|
||||||
return CORE.cpp_main_section
|
return CORE.cpp_main_section
|
||||||
|
|
||||||
yield generator
|
yield generator
|
||||||
|
0
tests/component_tests/image/config/bad.png
Normal file
0
tests/component_tests/image/config/bad.png
Normal file
BIN
tests/component_tests/image/config/image.png
Normal file
BIN
tests/component_tests/image/config/image.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 685 B |
20
tests/component_tests/image/config/image_test.yaml
Normal file
20
tests/component_tests/image/config/image_test.yaml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
esphome:
|
||||||
|
name: test
|
||||||
|
|
||||||
|
esp32:
|
||||||
|
board: esp32s3box
|
||||||
|
|
||||||
|
image:
|
||||||
|
- file: image.png
|
||||||
|
byte_order: little_endian
|
||||||
|
id: cat_img
|
||||||
|
type: rgb565
|
||||||
|
|
||||||
|
spi:
|
||||||
|
mosi_pin: 6
|
||||||
|
clk_pin: 7
|
||||||
|
|
||||||
|
display:
|
||||||
|
- platform: mipi_spi
|
||||||
|
id: lcd_display
|
||||||
|
model: s3box
|
183
tests/component_tests/image/test_init.py
Normal file
183
tests/component_tests/image/test_init.py
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
"""Tests for image configuration validation."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from esphome import config_validation as cv
|
||||||
|
from esphome.components.image import CONFIG_SCHEMA
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("config", "error_match"),
|
||||||
|
[
|
||||||
|
pytest.param(
|
||||||
|
"a string",
|
||||||
|
"Badly formed image configuration, expected a list or a dictionary",
|
||||||
|
id="invalid_string_config",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{"id": "image_id", "type": "rgb565"},
|
||||||
|
r"required key not provided @ data\[0\]\['file'\]",
|
||||||
|
id="missing_file",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{"file": "image.png", "type": "rgb565"},
|
||||||
|
r"required key not provided @ data\[0\]\['id'\]",
|
||||||
|
id="missing_id",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{"id": "mdi_id", "file": "mdi:weather-##", "type": "rgb565"},
|
||||||
|
"Could not parse mdi icon name",
|
||||||
|
id="invalid_mdi_icon",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{
|
||||||
|
"id": "image_id",
|
||||||
|
"file": "image.png",
|
||||||
|
"type": "binary",
|
||||||
|
"transparency": "alpha_channel",
|
||||||
|
},
|
||||||
|
"Image format 'BINARY' cannot have transparency",
|
||||||
|
id="binary_with_transparency",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{
|
||||||
|
"id": "image_id",
|
||||||
|
"file": "image.png",
|
||||||
|
"type": "rgb565",
|
||||||
|
"transparency": "chroma_key",
|
||||||
|
"invert_alpha": True,
|
||||||
|
},
|
||||||
|
"No alpha channel to invert",
|
||||||
|
id="invert_alpha_without_alpha_channel",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{
|
||||||
|
"id": "image_id",
|
||||||
|
"file": "image.png",
|
||||||
|
"type": "binary",
|
||||||
|
"byte_order": "big_endian",
|
||||||
|
},
|
||||||
|
"Image format 'BINARY' does not support byte order configuration",
|
||||||
|
id="binary_with_byte_order",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{"id": "image_id", "file": "bad.png", "type": "binary"},
|
||||||
|
"File can't be opened as image",
|
||||||
|
id="invalid_image_file",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{"defaults": {}, "images": [{"id": "image_id", "file": "image.png"}]},
|
||||||
|
"Type is required either in the image config or in the defaults",
|
||||||
|
id="missing_type_in_defaults",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_image_configuration_errors(
|
||||||
|
config: Any,
|
||||||
|
error_match: str,
|
||||||
|
) -> None:
|
||||||
|
"""Test detection of invalid configuration."""
|
||||||
|
with pytest.raises(cv.Invalid, match=error_match):
|
||||||
|
CONFIG_SCHEMA(config)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"config",
|
||||||
|
[
|
||||||
|
pytest.param(
|
||||||
|
{
|
||||||
|
"id": "image_id",
|
||||||
|
"file": "image.png",
|
||||||
|
"type": "rgb565",
|
||||||
|
"transparency": "chroma_key",
|
||||||
|
"byte_order": "little_endian",
|
||||||
|
"dither": "FloydSteinberg",
|
||||||
|
"resize": "100x100",
|
||||||
|
"invert_alpha": False,
|
||||||
|
},
|
||||||
|
id="single_image_all_options",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "image_id",
|
||||||
|
"file": "image.png",
|
||||||
|
"type": "binary",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
id="list_of_images",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{
|
||||||
|
"defaults": {
|
||||||
|
"type": "rgb565",
|
||||||
|
"transparency": "chroma_key",
|
||||||
|
"byte_order": "little_endian",
|
||||||
|
"dither": "FloydSteinberg",
|
||||||
|
"resize": "100x100",
|
||||||
|
"invert_alpha": False,
|
||||||
|
},
|
||||||
|
"images": [
|
||||||
|
{
|
||||||
|
"id": "image_id",
|
||||||
|
"file": "image.png",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
id="images_with_defaults",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{
|
||||||
|
"rgb565": {
|
||||||
|
"alpha_channel": [
|
||||||
|
{
|
||||||
|
"id": "image_id",
|
||||||
|
"file": "image.png",
|
||||||
|
"transparency": "alpha_channel",
|
||||||
|
"byte_order": "little_endian",
|
||||||
|
"dither": "FloydSteinberg",
|
||||||
|
"resize": "100x100",
|
||||||
|
"invert_alpha": False,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"binary": [
|
||||||
|
{
|
||||||
|
"id": "image_id",
|
||||||
|
"file": "image.png",
|
||||||
|
"transparency": "opaque",
|
||||||
|
"dither": "FloydSteinberg",
|
||||||
|
"resize": "100x100",
|
||||||
|
"invert_alpha": False,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
id="type_based_organization",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_image_configuration_success(
|
||||||
|
config: dict[str, Any] | list[dict[str, Any]],
|
||||||
|
) -> None:
|
||||||
|
"""Test successful configuration validation."""
|
||||||
|
CONFIG_SCHEMA(config)
|
||||||
|
|
||||||
|
|
||||||
|
def test_image_generation(
|
||||||
|
generate_main: Callable[[str | Path], str],
|
||||||
|
component_config_path: Callable[[str], Path],
|
||||||
|
) -> None:
|
||||||
|
"""Test image generation configuration."""
|
||||||
|
|
||||||
|
main_cpp = generate_main(component_config_path("image_test.yaml"))
|
||||||
|
assert "uint8_t_id[] PROGMEM = {0x24, 0x21, 0x24, 0x21" in main_cpp
|
||||||
|
assert (
|
||||||
|
"cat_img = new image::Image(uint8_t_id, 32, 24, image::IMAGE_TYPE_RGB565, image::TRANSPARENCY_OPAQUE);"
|
||||||
|
in main_cpp
|
||||||
|
)
|
12
tests/components/gl_r01_i2c/common.yaml
Normal file
12
tests/components/gl_r01_i2c/common.yaml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
i2c:
|
||||||
|
- id: i2c_gl_r01_i2c
|
||||||
|
scl: ${scl_pin}
|
||||||
|
sda: ${sda_pin}
|
||||||
|
|
||||||
|
sensor:
|
||||||
|
- platform: gl_r01_i2c
|
||||||
|
id: tof
|
||||||
|
name: "ToF sensor"
|
||||||
|
i2c_id: i2c_gl_r01_i2c
|
||||||
|
address: 0x74
|
||||||
|
update_interval: 15s
|
5
tests/components/gl_r01_i2c/test.esp32-ard.yaml
Normal file
5
tests/components/gl_r01_i2c/test.esp32-ard.yaml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
substitutions:
|
||||||
|
scl_pin: GPIO16
|
||||||
|
sda_pin: GPIO17
|
||||||
|
|
||||||
|
<<: !include common.yaml
|
5
tests/components/gl_r01_i2c/test.esp32-c3-ard.yaml
Normal file
5
tests/components/gl_r01_i2c/test.esp32-c3-ard.yaml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
substitutions:
|
||||||
|
scl_pin: GPIO5
|
||||||
|
sda_pin: GPIO4
|
||||||
|
|
||||||
|
<<: !include common.yaml
|
5
tests/components/gl_r01_i2c/test.esp32-c3-idf.yaml
Normal file
5
tests/components/gl_r01_i2c/test.esp32-c3-idf.yaml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
substitutions:
|
||||||
|
scl_pin: GPIO5
|
||||||
|
sda_pin: GPIO4
|
||||||
|
|
||||||
|
<<: !include common.yaml
|
5
tests/components/gl_r01_i2c/test.esp32-idf.yaml
Normal file
5
tests/components/gl_r01_i2c/test.esp32-idf.yaml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
substitutions:
|
||||||
|
scl_pin: GPIO16
|
||||||
|
sda_pin: GPIO17
|
||||||
|
|
||||||
|
<<: !include common.yaml
|
5
tests/components/gl_r01_i2c/test.esp8266-ard.yaml
Normal file
5
tests/components/gl_r01_i2c/test.esp8266-ard.yaml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
substitutions:
|
||||||
|
scl_pin: GPIO5
|
||||||
|
sda_pin: GPIO4
|
||||||
|
|
||||||
|
<<: !include common.yaml
|
5
tests/components/gl_r01_i2c/test.rp2040-ard.yaml
Normal file
5
tests/components/gl_r01_i2c/test.rp2040-ard.yaml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
substitutions:
|
||||||
|
scl_pin: GPIO5
|
||||||
|
sda_pin: GPIO4
|
||||||
|
|
||||||
|
<<: !include common.yaml
|
@ -1,17 +0,0 @@
|
|||||||
spi:
|
|
||||||
- id: spi_main_lcd
|
|
||||||
clk_pin: 16
|
|
||||||
mosi_pin: 17
|
|
||||||
miso_pin: 32
|
|
||||||
|
|
||||||
display:
|
|
||||||
- platform: ili9xxx
|
|
||||||
id: main_lcd
|
|
||||||
model: ili9342
|
|
||||||
cs_pin: 14
|
|
||||||
dc_pin: 13
|
|
||||||
reset_pin: 21
|
|
||||||
invert_colors: true
|
|
||||||
|
|
||||||
<<: !include common.yaml
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
|||||||
spi:
|
|
||||||
- id: spi_main_lcd
|
|
||||||
clk_pin: 6
|
|
||||||
mosi_pin: 7
|
|
||||||
miso_pin: 5
|
|
||||||
|
|
||||||
display:
|
|
||||||
- platform: ili9xxx
|
|
||||||
id: main_lcd
|
|
||||||
model: ili9342
|
|
||||||
cs_pin: 3
|
|
||||||
dc_pin: 11
|
|
||||||
reset_pin: 10
|
|
||||||
invert_colors: true
|
|
||||||
|
|
||||||
<<: !include common.yaml
|
|
@ -1,16 +0,0 @@
|
|||||||
spi:
|
|
||||||
- id: spi_main_lcd
|
|
||||||
clk_pin: 6
|
|
||||||
mosi_pin: 7
|
|
||||||
miso_pin: 5
|
|
||||||
|
|
||||||
display:
|
|
||||||
- platform: ili9xxx
|
|
||||||
id: main_lcd
|
|
||||||
model: ili9342
|
|
||||||
cs_pin: 3
|
|
||||||
dc_pin: 11
|
|
||||||
reset_pin: 10
|
|
||||||
invert_colors: true
|
|
||||||
|
|
||||||
<<: !include common.yaml
|
|
@ -13,4 +13,13 @@ display:
|
|||||||
reset_pin: 16
|
reset_pin: 16
|
||||||
invert_colors: true
|
invert_colors: true
|
||||||
|
|
||||||
<<: !include common.yaml
|
image:
|
||||||
|
defaults:
|
||||||
|
type: rgb565
|
||||||
|
transparency: opaque
|
||||||
|
byte_order: little_endian
|
||||||
|
resize: 50x50
|
||||||
|
dither: FloydSteinberg
|
||||||
|
images:
|
||||||
|
- id: test_image
|
||||||
|
file: ../../pnglogo.png
|
||||||
|
8
tests/components/lps22/common.yaml
Normal file
8
tests/components/lps22/common.yaml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
sensor:
|
||||||
|
- platform: lps22
|
||||||
|
address: 0x5d
|
||||||
|
update_interval: 10s
|
||||||
|
temperature:
|
||||||
|
name: "LPS22 Temperature"
|
||||||
|
pressure:
|
||||||
|
name: "LPS22 Pressure"
|
6
tests/components/lps22/test.esp32-ard.yaml
Normal file
6
tests/components/lps22/test.esp32-ard.yaml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
i2c:
|
||||||
|
- id: i2c_lps22
|
||||||
|
scl: 16
|
||||||
|
sda: 17
|
||||||
|
|
||||||
|
<<: !include common.yaml
|
6
tests/components/lps22/test.esp32-c3-ard.yaml
Normal file
6
tests/components/lps22/test.esp32-c3-ard.yaml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
i2c:
|
||||||
|
- id: i2c_lps22
|
||||||
|
scl: 5
|
||||||
|
sda: 4
|
||||||
|
|
||||||
|
<<: !include common.yaml
|
6
tests/components/lps22/test.esp32-c3-idf.yaml
Normal file
6
tests/components/lps22/test.esp32-c3-idf.yaml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
i2c:
|
||||||
|
- id: i2c_lps22
|
||||||
|
scl: 5
|
||||||
|
sda: 4
|
||||||
|
|
||||||
|
<<: !include common.yaml
|
6
tests/components/lps22/test.esp32-idf.yaml
Normal file
6
tests/components/lps22/test.esp32-idf.yaml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
i2c:
|
||||||
|
- id: i2c_lps22
|
||||||
|
scl: 16
|
||||||
|
sda: 17
|
||||||
|
|
||||||
|
<<: !include common.yaml
|
6
tests/components/lps22/test.esp8266-ard.yaml
Normal file
6
tests/components/lps22/test.esp8266-ard.yaml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
i2c:
|
||||||
|
- id: i2c_lps22
|
||||||
|
scl: 5
|
||||||
|
sda: 4
|
||||||
|
|
||||||
|
<<: !include common.yaml
|
6
tests/components/lps22/test.rp2040-ard.yaml
Normal file
6
tests/components/lps22/test.rp2040-ard.yaml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
i2c:
|
||||||
|
- id: i2c_lps22
|
||||||
|
scl: 5
|
||||||
|
sda: 4
|
||||||
|
|
||||||
|
<<: !include common.yaml
|
@ -78,3 +78,268 @@ pytest -s tests/integration/test_host_mode_basic.py
|
|||||||
- Each test gets its own temporary directory and unique port
|
- Each test gets its own temporary directory and unique port
|
||||||
- Port allocation minimizes race conditions by holding the socket until just before ESPHome starts
|
- Port allocation minimizes race conditions by holding the socket until just before ESPHome starts
|
||||||
- Output from ESPHome processes is displayed for debugging
|
- Output from ESPHome processes is displayed for debugging
|
||||||
|
|
||||||
|
## Integration Test Writing Guide
|
||||||
|
|
||||||
|
### Test Patterns and Best Practices
|
||||||
|
|
||||||
|
#### 1. Test File Naming Convention
|
||||||
|
- Use descriptive names: `test_{category}_{feature}.py`
|
||||||
|
- Common categories: `host_mode`, `api`, `scheduler`, `light`, `areas_and_devices`
|
||||||
|
- Examples:
|
||||||
|
- `test_host_mode_basic.py` - Basic host mode functionality
|
||||||
|
- `test_api_message_batching.py` - API message batching
|
||||||
|
- `test_scheduler_stress.py` - Scheduler stress testing
|
||||||
|
|
||||||
|
#### 2. Essential Imports
|
||||||
|
```python
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from aioesphomeapi import EntityState, SensorState
|
||||||
|
|
||||||
|
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Common Test Patterns
|
||||||
|
|
||||||
|
##### Basic Entity Test
|
||||||
|
```python
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_my_sensor(
|
||||||
|
yaml_config: str,
|
||||||
|
run_compiled: RunCompiledFunction,
|
||||||
|
api_client_connected: APIClientConnectedFactory,
|
||||||
|
) -> None:
|
||||||
|
"""Test sensor functionality."""
|
||||||
|
async with run_compiled(yaml_config), api_client_connected() as client:
|
||||||
|
# Get entity list
|
||||||
|
entities, services = await client.list_entities_services()
|
||||||
|
|
||||||
|
# Find specific entity
|
||||||
|
sensor = next((e for e in entities if e.object_id == "my_sensor"), None)
|
||||||
|
assert sensor is not None
|
||||||
|
```
|
||||||
|
|
||||||
|
##### State Subscription Pattern
|
||||||
|
```python
|
||||||
|
# Track state changes with futures
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
states: dict[int, EntityState] = {}
|
||||||
|
state_future: asyncio.Future[EntityState] = loop.create_future()
|
||||||
|
|
||||||
|
def on_state(state: EntityState) -> None:
|
||||||
|
states[state.key] = state
|
||||||
|
# Check for specific condition using isinstance
|
||||||
|
if isinstance(state, SensorState) and state.state == expected_value:
|
||||||
|
if not state_future.done():
|
||||||
|
state_future.set_result(state)
|
||||||
|
|
||||||
|
client.subscribe_states(on_state)
|
||||||
|
|
||||||
|
# Wait for state with timeout
|
||||||
|
try:
|
||||||
|
result = await asyncio.wait_for(state_future, timeout=5.0)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
pytest.fail(f"Expected state not received. Got: {list(states.values())}")
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Service Execution Pattern
|
||||||
|
```python
|
||||||
|
# Find and execute service
|
||||||
|
entities, services = await client.list_entities_services()
|
||||||
|
my_service = next((s for s in services if s.name == "my_service"), None)
|
||||||
|
assert my_service is not None
|
||||||
|
|
||||||
|
# Execute with parameters
|
||||||
|
client.execute_service(my_service, {"param1": "value1", "param2": 42})
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Multiple Entity Tracking
|
||||||
|
```python
|
||||||
|
# For tests with many entities
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
entity_count = 50
|
||||||
|
received_states: set[int] = set()
|
||||||
|
all_states_future: asyncio.Future[bool] = loop.create_future()
|
||||||
|
|
||||||
|
def on_state(state: EntityState) -> None:
|
||||||
|
received_states.add(state.key)
|
||||||
|
if len(received_states) >= entity_count and not all_states_future.done():
|
||||||
|
all_states_future.set_result(True)
|
||||||
|
|
||||||
|
client.subscribe_states(on_state)
|
||||||
|
await asyncio.wait_for(all_states_future, timeout=10.0)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. YAML Fixture Guidelines
|
||||||
|
|
||||||
|
##### Naming Convention
|
||||||
|
- Match test function name: `test_my_feature` → `fixtures/my_feature.yaml`
|
||||||
|
- Note: Remove `test_` prefix for fixture filename
|
||||||
|
|
||||||
|
##### Basic Structure
|
||||||
|
```yaml
|
||||||
|
esphome:
|
||||||
|
name: test-name # Use kebab-case
|
||||||
|
# Optional: areas, devices, platformio_options
|
||||||
|
|
||||||
|
host: # Always use host platform for integration tests
|
||||||
|
api: # Port injected automatically
|
||||||
|
logger:
|
||||||
|
level: DEBUG # Optional: Set log level
|
||||||
|
|
||||||
|
# Component configurations
|
||||||
|
sensor:
|
||||||
|
- platform: template
|
||||||
|
name: "My Sensor"
|
||||||
|
id: my_sensor
|
||||||
|
lambda: return 42.0;
|
||||||
|
update_interval: 0.1s # Fast updates for testing
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Advanced Features
|
||||||
|
```yaml
|
||||||
|
# External components for custom test code
|
||||||
|
external_components:
|
||||||
|
- source:
|
||||||
|
type: local
|
||||||
|
path: EXTERNAL_COMPONENT_PATH # Replaced by test framework
|
||||||
|
components: [my_test_component]
|
||||||
|
|
||||||
|
# Areas and devices
|
||||||
|
esphome:
|
||||||
|
name: test-device
|
||||||
|
areas:
|
||||||
|
- id: living_room
|
||||||
|
name: "Living Room"
|
||||||
|
- id: kitchen
|
||||||
|
name: "Kitchen"
|
||||||
|
parent_id: living_room
|
||||||
|
devices:
|
||||||
|
- id: my_device
|
||||||
|
name: "Test Device"
|
||||||
|
area_id: living_room
|
||||||
|
|
||||||
|
# API services
|
||||||
|
api:
|
||||||
|
services:
|
||||||
|
- service: test_service
|
||||||
|
variables:
|
||||||
|
my_param: string
|
||||||
|
then:
|
||||||
|
- logger.log:
|
||||||
|
format: "Service called with: %s"
|
||||||
|
args: [my_param.c_str()]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5. Testing Complex Scenarios
|
||||||
|
|
||||||
|
##### External Components
|
||||||
|
Create C++ components in `fixtures/external_components/` for:
|
||||||
|
- Stress testing
|
||||||
|
- Custom entity behaviors
|
||||||
|
- Scheduler testing
|
||||||
|
- Memory management tests
|
||||||
|
|
||||||
|
##### Log Line Monitoring
|
||||||
|
```python
|
||||||
|
log_lines: list[str] = []
|
||||||
|
|
||||||
|
def on_log_line(line: str) -> None:
|
||||||
|
log_lines.append(line)
|
||||||
|
if "expected message" in line:
|
||||||
|
# Handle specific log messages
|
||||||
|
|
||||||
|
async with run_compiled(yaml_config, line_callback=on_log_line):
|
||||||
|
# Test implementation
|
||||||
|
```
|
||||||
|
|
||||||
|
Example using futures for specific log patterns:
|
||||||
|
```python
|
||||||
|
import re
|
||||||
|
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
connected_future = loop.create_future()
|
||||||
|
service_future = loop.create_future()
|
||||||
|
|
||||||
|
# Patterns to match
|
||||||
|
connected_pattern = re.compile(r"Client .* connected from")
|
||||||
|
service_pattern = re.compile(r"Service called")
|
||||||
|
|
||||||
|
def check_output(line: str) -> None:
|
||||||
|
"""Check log output for expected messages."""
|
||||||
|
if not connected_future.done() and connected_pattern.search(line):
|
||||||
|
connected_future.set_result(True)
|
||||||
|
elif not service_future.done() and service_pattern.search(line):
|
||||||
|
service_future.set_result(True)
|
||||||
|
|
||||||
|
async with run_compiled(yaml_config, line_callback=check_output):
|
||||||
|
async with api_client_connected() as client:
|
||||||
|
# Wait for specific log message
|
||||||
|
await asyncio.wait_for(connected_future, timeout=5.0)
|
||||||
|
|
||||||
|
# Do test actions...
|
||||||
|
|
||||||
|
# Wait for service log
|
||||||
|
await asyncio.wait_for(service_future, timeout=5.0)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: Tests that monitor log messages typically have fewer race conditions compared to state-based testing, making them more reliable. However, be aware that the host platform currently does not have a thread-safe logger, so logging from threads will not work correctly.
|
||||||
|
|
||||||
|
##### Timeout Handling
|
||||||
|
```python
|
||||||
|
# Always use timeouts for async operations
|
||||||
|
try:
|
||||||
|
result = await asyncio.wait_for(some_future, timeout=5.0)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
pytest.fail("Operation timed out - check test expectations")
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6. Common Assertions
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Device info
|
||||||
|
assert device_info.name == "expected-name"
|
||||||
|
assert device_info.compilation_time is not None
|
||||||
|
|
||||||
|
# Entity properties
|
||||||
|
assert sensor.accuracy_decimals == 2
|
||||||
|
assert sensor.state_class == 1 # measurement
|
||||||
|
assert sensor.force_update is True
|
||||||
|
|
||||||
|
# Service availability
|
||||||
|
assert len(services) > 0
|
||||||
|
assert any(s.name == "expected_service" for s in services)
|
||||||
|
|
||||||
|
# State values
|
||||||
|
assert state.state == expected_value
|
||||||
|
assert state.missing_state is False
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 7. Debugging Tips
|
||||||
|
|
||||||
|
- Use `pytest -s` to see ESPHome output during tests
|
||||||
|
- Add descriptive failure messages to assertions
|
||||||
|
- Use `pytest.fail()` with detailed error info for timeouts
|
||||||
|
- Check `log_lines` for compilation or runtime errors
|
||||||
|
- Enable debug logging in YAML fixtures when needed
|
||||||
|
|
||||||
|
#### 8. Performance Considerations
|
||||||
|
|
||||||
|
- Use short update intervals (0.1s) for faster tests
|
||||||
|
- Set reasonable timeouts (5-10s for most operations)
|
||||||
|
- Batch multiple assertions when possible
|
||||||
|
- Clean up resources properly using context managers
|
||||||
|
|
||||||
|
#### 9. Test Categories
|
||||||
|
|
||||||
|
- **Basic Tests**: Minimal functionality verification
|
||||||
|
- **Entity Tests**: Sensor, switch, light behavior
|
||||||
|
- **API Tests**: Message batching, services, events
|
||||||
|
- **Scheduler Tests**: Timing, defer operations, stress
|
||||||
|
- **Memory Tests**: Conditional compilation, optimization
|
||||||
|
- **Integration Tests**: Areas, devices, complex interactions
|
||||||
|
@ -5,12 +5,14 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import AsyncGenerator, Callable, Generator
|
from collections.abc import AsyncGenerator, Callable, Generator
|
||||||
from contextlib import AbstractAsyncContextManager, asynccontextmanager
|
from contextlib import AbstractAsyncContextManager, asynccontextmanager
|
||||||
|
import fcntl
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import platform
|
import platform
|
||||||
import signal
|
import signal
|
||||||
import socket
|
import socket
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
from typing import TextIO
|
from typing import TextIO
|
||||||
@ -50,6 +52,66 @@ if platform.system() == "Windows":
|
|||||||
import pty # not available on Windows
|
import pty # not available on Windows
|
||||||
|
|
||||||
|
|
||||||
|
def _get_platformio_env(cache_dir: Path) -> dict[str, str]:
|
||||||
|
"""Get environment variables for PlatformIO with shared cache."""
|
||||||
|
env = os.environ.copy()
|
||||||
|
env["PLATFORMIO_CORE_DIR"] = str(cache_dir)
|
||||||
|
env["PLATFORMIO_CACHE_DIR"] = str(cache_dir / ".cache")
|
||||||
|
env["PLATFORMIO_LIBDEPS_DIR"] = str(cache_dir / "libdeps")
|
||||||
|
return env
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def shared_platformio_cache() -> Generator[Path]:
|
||||||
|
"""Initialize a shared PlatformIO cache for all integration tests."""
|
||||||
|
# Use a dedicated directory for integration tests to avoid conflicts
|
||||||
|
test_cache_dir = Path.home() / ".esphome-integration-tests"
|
||||||
|
cache_dir = test_cache_dir / "platformio"
|
||||||
|
|
||||||
|
# Use a lock file in the home directory to ensure only one process initializes the cache
|
||||||
|
# This is needed when running with pytest-xdist
|
||||||
|
# The lock file must be in a directory that already exists to avoid race conditions
|
||||||
|
lock_file = Path.home() / ".esphome-integration-tests-init.lock"
|
||||||
|
|
||||||
|
# Always acquire the lock to ensure cache is ready before proceeding
|
||||||
|
with open(lock_file, "w") as lock_fd:
|
||||||
|
fcntl.flock(lock_fd.fileno(), fcntl.LOCK_EX)
|
||||||
|
|
||||||
|
# Check if cache needs initialization while holding the lock
|
||||||
|
if not cache_dir.exists() or not any(cache_dir.iterdir()):
|
||||||
|
# Create the test cache directory if it doesn't exist
|
||||||
|
test_cache_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
# Create a basic host config
|
||||||
|
init_dir = Path(tmpdir)
|
||||||
|
config_path = init_dir / "cache_init.yaml"
|
||||||
|
config_path.write_text("""esphome:
|
||||||
|
name: cache-init
|
||||||
|
host:
|
||||||
|
api:
|
||||||
|
encryption:
|
||||||
|
key: "IIevImVI42I0FGos5nLqFK91jrJehrgidI0ArwMLr8w="
|
||||||
|
logger:
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Run compilation to populate the cache
|
||||||
|
# We must succeed here to avoid race conditions where multiple
|
||||||
|
# tests try to populate the same cache directory simultaneously
|
||||||
|
env = _get_platformio_env(cache_dir)
|
||||||
|
|
||||||
|
subprocess.run(
|
||||||
|
["esphome", "compile", str(config_path)],
|
||||||
|
check=True,
|
||||||
|
cwd=init_dir,
|
||||||
|
env=env,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Lock is held until here, ensuring cache is fully populated before any test proceeds
|
||||||
|
|
||||||
|
yield cache_dir
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module", autouse=True)
|
@pytest.fixture(scope="module", autouse=True)
|
||||||
def enable_aioesphomeapi_debug_logging():
|
def enable_aioesphomeapi_debug_logging():
|
||||||
"""Enable debug logging for aioesphomeapi to help diagnose connection issues."""
|
"""Enable debug logging for aioesphomeapi to help diagnose connection issues."""
|
||||||
@ -161,10 +223,15 @@ async def write_yaml_config(
|
|||||||
@pytest_asyncio.fixture
|
@pytest_asyncio.fixture
|
||||||
async def compile_esphome(
|
async def compile_esphome(
|
||||||
integration_test_dir: Path,
|
integration_test_dir: Path,
|
||||||
|
shared_platformio_cache: Path,
|
||||||
) -> AsyncGenerator[CompileFunction]:
|
) -> AsyncGenerator[CompileFunction]:
|
||||||
"""Compile an ESPHome configuration and return the binary path."""
|
"""Compile an ESPHome configuration and return the binary path."""
|
||||||
|
|
||||||
async def _compile(config_path: Path) -> Path:
|
async def _compile(config_path: Path) -> Path:
|
||||||
|
# Use the shared PlatformIO cache for faster compilation
|
||||||
|
# This avoids re-downloading dependencies for each test
|
||||||
|
env = _get_platformio_env(shared_platformio_cache)
|
||||||
|
|
||||||
# Retry compilation up to 3 times if we get a segfault
|
# Retry compilation up to 3 times if we get a segfault
|
||||||
max_retries = 3
|
max_retries = 3
|
||||||
for attempt in range(max_retries):
|
for attempt in range(max_retries):
|
||||||
@ -179,6 +246,7 @@ async def compile_esphome(
|
|||||||
stdin=asyncio.subprocess.DEVNULL,
|
stdin=asyncio.subprocess.DEVNULL,
|
||||||
# Start in a new process group to isolate signal handling
|
# Start in a new process group to isolate signal handling
|
||||||
start_new_session=True,
|
start_new_session=True,
|
||||||
|
env=env,
|
||||||
)
|
)
|
||||||
await proc.wait()
|
await proc.wait()
|
||||||
|
|
||||||
|
@ -2,14 +2,10 @@ esphome:
|
|||||||
name: api-conditional-memory-test
|
name: api-conditional-memory-test
|
||||||
host:
|
host:
|
||||||
api:
|
api:
|
||||||
batch_delay: 0ms
|
|
||||||
actions:
|
actions:
|
||||||
- action: test_simple_service
|
- action: test_simple_service
|
||||||
then:
|
then:
|
||||||
- logger.log: "Simple service called"
|
- logger.log: "Simple service called"
|
||||||
- binary_sensor.template.publish:
|
|
||||||
id: service_called_sensor
|
|
||||||
state: ON
|
|
||||||
- action: test_service_with_args
|
- action: test_service_with_args
|
||||||
variables:
|
variables:
|
||||||
arg_string: string
|
arg_string: string
|
||||||
@ -20,53 +16,14 @@ api:
|
|||||||
- logger.log:
|
- logger.log:
|
||||||
format: "Service called with: %s, %d, %d, %.2f"
|
format: "Service called with: %s, %d, %d, %.2f"
|
||||||
args: [arg_string.c_str(), arg_int, arg_bool, arg_float]
|
args: [arg_string.c_str(), arg_int, arg_bool, arg_float]
|
||||||
- sensor.template.publish:
|
|
||||||
id: service_arg_sensor
|
|
||||||
state: !lambda 'return arg_float;'
|
|
||||||
on_client_connected:
|
on_client_connected:
|
||||||
- logger.log:
|
- logger.log:
|
||||||
format: "Client %s connected from %s"
|
format: "Client %s connected from %s"
|
||||||
args: [client_info.c_str(), client_address.c_str()]
|
args: [client_info.c_str(), client_address.c_str()]
|
||||||
- binary_sensor.template.publish:
|
|
||||||
id: client_connected
|
|
||||||
state: ON
|
|
||||||
- text_sensor.template.publish:
|
|
||||||
id: last_client_info
|
|
||||||
state: !lambda 'return client_info;'
|
|
||||||
on_client_disconnected:
|
on_client_disconnected:
|
||||||
- logger.log:
|
- logger.log:
|
||||||
format: "Client %s disconnected from %s"
|
format: "Client %s disconnected from %s"
|
||||||
args: [client_info.c_str(), client_address.c_str()]
|
args: [client_info.c_str(), client_address.c_str()]
|
||||||
- binary_sensor.template.publish:
|
|
||||||
id: client_connected
|
|
||||||
state: OFF
|
|
||||||
- binary_sensor.template.publish:
|
|
||||||
id: client_disconnected_event
|
|
||||||
state: ON
|
|
||||||
|
|
||||||
logger:
|
logger:
|
||||||
level: DEBUG
|
level: DEBUG
|
||||||
|
|
||||||
binary_sensor:
|
|
||||||
- platform: template
|
|
||||||
name: "Client Connected"
|
|
||||||
id: client_connected
|
|
||||||
device_class: connectivity
|
|
||||||
- platform: template
|
|
||||||
name: "Client Disconnected Event"
|
|
||||||
id: client_disconnected_event
|
|
||||||
- platform: template
|
|
||||||
name: "Service Called"
|
|
||||||
id: service_called_sensor
|
|
||||||
|
|
||||||
sensor:
|
|
||||||
- platform: template
|
|
||||||
name: "Service Argument Value"
|
|
||||||
id: service_arg_sensor
|
|
||||||
unit_of_measurement: ""
|
|
||||||
accuracy_decimals: 2
|
|
||||||
|
|
||||||
text_sensor:
|
|
||||||
- platform: template
|
|
||||||
name: "Last Client Info"
|
|
||||||
id: last_client_info
|
|
||||||
|
@ -23,19 +23,6 @@ void SchedulerStringLifetimeComponent::run_string_lifetime_test() {
|
|||||||
test_vector_reallocation();
|
test_vector_reallocation();
|
||||||
test_string_move_semantics();
|
test_string_move_semantics();
|
||||||
test_lambda_capture_lifetime();
|
test_lambda_capture_lifetime();
|
||||||
|
|
||||||
// Schedule final check
|
|
||||||
this->set_timeout("final_check", 200, [this]() {
|
|
||||||
ESP_LOGI(TAG, "String lifetime tests complete");
|
|
||||||
ESP_LOGI(TAG, "Tests passed: %d", this->tests_passed_);
|
|
||||||
ESP_LOGI(TAG, "Tests failed: %d", this->tests_failed_);
|
|
||||||
|
|
||||||
if (this->tests_failed_ == 0) {
|
|
||||||
ESP_LOGI(TAG, "SUCCESS: All string lifetime tests passed!");
|
|
||||||
} else {
|
|
||||||
ESP_LOGE(TAG, "FAILURE: %d string lifetime tests failed!", this->tests_failed_);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void SchedulerStringLifetimeComponent::run_test1() {
|
void SchedulerStringLifetimeComponent::run_test1() {
|
||||||
@ -69,7 +56,6 @@ void SchedulerStringLifetimeComponent::run_test5() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void SchedulerStringLifetimeComponent::run_final_check() {
|
void SchedulerStringLifetimeComponent::run_final_check() {
|
||||||
ESP_LOGI(TAG, "String lifetime tests complete");
|
|
||||||
ESP_LOGI(TAG, "Tests passed: %d", this->tests_passed_);
|
ESP_LOGI(TAG, "Tests passed: %d", this->tests_passed_);
|
||||||
ESP_LOGI(TAG, "Tests failed: %d", this->tests_failed_);
|
ESP_LOGI(TAG, "Tests failed: %d", this->tests_failed_);
|
||||||
|
|
||||||
@ -78,6 +64,7 @@ void SchedulerStringLifetimeComponent::run_final_check() {
|
|||||||
} else {
|
} else {
|
||||||
ESP_LOGE(TAG, "FAILURE: %d string lifetime tests failed!", this->tests_failed_);
|
ESP_LOGE(TAG, "FAILURE: %d string lifetime tests failed!", this->tests_failed_);
|
||||||
}
|
}
|
||||||
|
ESP_LOGI(TAG, "String lifetime tests complete");
|
||||||
}
|
}
|
||||||
|
|
||||||
void SchedulerStringLifetimeComponent::test_temporary_string_lifetime() {
|
void SchedulerStringLifetimeComponent::test_temporary_string_lifetime() {
|
||||||
|
43
tests/integration/fixtures/scheduler_null_name.yaml
Normal file
43
tests/integration/fixtures/scheduler_null_name.yaml
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
esphome:
|
||||||
|
name: scheduler-null-name
|
||||||
|
|
||||||
|
host:
|
||||||
|
|
||||||
|
logger:
|
||||||
|
level: DEBUG
|
||||||
|
|
||||||
|
api:
|
||||||
|
services:
|
||||||
|
- service: test_null_name
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
// First, create a scenario that would trigger the crash
|
||||||
|
// The crash happens when defer() is called with a name that would be cancelled
|
||||||
|
|
||||||
|
// Test 1: Create a defer with a valid name
|
||||||
|
App.scheduler.set_timeout(nullptr, "test_defer", 0, []() {
|
||||||
|
ESP_LOGI("TEST", "First defer should be cancelled");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 2: Create another defer with the same name - this triggers cancel_item_locked_
|
||||||
|
// In the unfixed code, this would crash if the name was NULL
|
||||||
|
App.scheduler.set_timeout(nullptr, "test_defer", 0, []() {
|
||||||
|
ESP_LOGI("TEST", "Second defer executed");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 3: Now test with nullptr - this is the actual crash scenario
|
||||||
|
// Create a defer item without a name (like voice assistant does)
|
||||||
|
const char* null_name = nullptr;
|
||||||
|
App.scheduler.set_timeout(nullptr, null_name, 0, []() {
|
||||||
|
ESP_LOGI("TEST", "Defer with null name executed");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 4: Create another defer with null name - this would trigger the crash
|
||||||
|
App.scheduler.set_timeout(nullptr, null_name, 0, []() {
|
||||||
|
ESP_LOGI("TEST", "Second null defer executed");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 5: Verify scheduler still works
|
||||||
|
App.scheduler.set_timeout(nullptr, "valid_timeout", 50, []() {
|
||||||
|
ESP_LOGI("TEST", "Test completed successfully");
|
||||||
|
});
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user