mirror of
https://github.com/esphome/esphome.git
synced 2026-06-25 20:34:48 +00:00
Compare commits
152 Commits
api-unroll
...
core-chunk
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ea417966d | ||
|
|
6446f309c1 | ||
|
|
093c34d4a4 | ||
|
|
3ab935bebb | ||
|
|
bb0067f517 | ||
|
|
550f6e7c72 | ||
|
|
5f2582efcd | ||
|
|
e26ce59797 | ||
|
|
9fa6d224c2 | ||
|
|
91b238aa97 | ||
|
|
00f08ba6ed | ||
|
|
f82401a504 | ||
|
|
178f23a7aa | ||
|
|
864d31aa65 | ||
|
|
936694af2c | ||
|
|
6a7c9af870 | ||
|
|
29dcf9fc51 | ||
|
|
6b67224286 | ||
|
|
70ea527161 | ||
|
|
34c35c84d5 | ||
|
|
bcbfc843ae | ||
|
|
d4fe46bb24 | ||
|
|
523c6f2376 | ||
|
|
b018ac67bc | ||
|
|
1a529a62aa | ||
|
|
6a46437a5f | ||
|
|
cfe8c0eeee | ||
|
|
b232fc91ab | ||
|
|
ac50f33388 | ||
|
|
ff52bb3029 | ||
|
|
627e440bd6 | ||
|
|
6bb90a1268 | ||
|
|
7d8add70a7 | ||
|
|
9094392870 | ||
|
|
c6ad23fbc0 | ||
|
|
6af7a9ed8f | ||
|
|
0b051289f5 | ||
|
|
d8329dba22 | ||
|
|
ee70a4aa72 | ||
|
|
04a58159d0 | ||
|
|
4c758fa1da | ||
|
|
c8e21802db | ||
|
|
b40ffacb8d | ||
|
|
e0118dd8eb | ||
|
|
e7194dce75 | ||
|
|
01b5bef37f | ||
|
|
403a9f7b7e | ||
|
|
10f52f2056 | ||
|
|
274c01ca74 | ||
|
|
3b82c6e38b | ||
|
|
f59a1011df | ||
|
|
82c0cb8929 | ||
|
|
2bdd9f6217 | ||
|
|
193e7d476d | ||
|
|
1b3e7d5ec4 | ||
|
|
767a8c49b0 | ||
|
|
4c43f7e9d0 | ||
|
|
3ef140e25d | ||
|
|
0a568a3e1e | ||
|
|
ef44491c69 | ||
|
|
089a2c99e2 | ||
|
|
311812c8cc | ||
|
|
a77ab59436 | ||
|
|
89fbfc6f71 | ||
|
|
28f3bcdba3 | ||
|
|
445715b9fd | ||
|
|
8843c36ec6 | ||
|
|
bd63f63b36 | ||
|
|
033e144e06 | ||
|
|
20d49f9a7c | ||
|
|
3b2caa1f5b | ||
|
|
c3769e4fce | ||
|
|
6d894dd6ee | ||
|
|
2db2b89eb1 | ||
|
|
e48c7165c5 | ||
|
|
506edaadd5 | ||
|
|
3f82a3a519 | ||
|
|
79cee864cb | ||
|
|
9f5ed938e5 | ||
|
|
4729efbd04 | ||
|
|
da9fbb8044 | ||
|
|
cf01163c8c | ||
|
|
5ba8c644e4 | ||
|
|
c833ff4a84 | ||
|
|
2a530a4bf4 | ||
|
|
6b4b653462 | ||
|
|
edb16a27d3 | ||
|
|
21df5d9bf6 | ||
|
|
73c972a604 | ||
|
|
8cdffef82a | ||
|
|
4034809281 | ||
|
|
ce6bffb65c | ||
|
|
e8bc4bedb4 | ||
|
|
b85a7ef317 | ||
|
|
9f7e310526 | ||
|
|
af7cb1d81e | ||
|
|
53ce2a2f7f | ||
|
|
fb0283e0ee | ||
|
|
5d0cfc31fa | ||
|
|
f30f0a0edc | ||
|
|
6aa538a61d | ||
|
|
7918a93a7f | ||
|
|
fe6ecb24b4 | ||
|
|
6db787d5e4 | ||
|
|
5b4385a084 | ||
|
|
4f69c3b850 | ||
|
|
c62a75ee17 | ||
|
|
d4e9c62d92 | ||
|
|
ac8a2467a5 | ||
|
|
dc1dd9ebb7 | ||
|
|
0c06d78a4f | ||
|
|
41c9ed28cd | ||
|
|
5608aa10a5 | ||
|
|
daa68a2a60 | ||
|
|
a408b5a4fe | ||
|
|
e264c97454 | ||
|
|
8790dec137 | ||
|
|
6480868e6e | ||
|
|
0578e43352 | ||
|
|
2a89d4835f | ||
|
|
5084c61016 | ||
|
|
b45f94d511 | ||
|
|
66a4752e13 | ||
|
|
4d4f78de81 | ||
|
|
0faa641c8a | ||
|
|
0f16d27a72 | ||
|
|
835ee456a5 | ||
|
|
17f3b7dbd5 | ||
|
|
171a429526 | ||
|
|
e4ee2b7c04 | ||
|
|
c85a062e23 | ||
|
|
873378fa1f | ||
|
|
4f00ad409e | ||
|
|
20b516ff11 | ||
|
|
8754bbfa89 | ||
|
|
6d92cc3d2b | ||
|
|
2f684bf4f3 | ||
|
|
45af21bf38 | ||
|
|
e6318a2d16 | ||
|
|
bef4c8a86c | ||
|
|
6e67864510 | ||
|
|
c2af4874f9 | ||
|
|
2001b91280 | ||
|
|
5460ee7edd | ||
|
|
40081e5ae7 | ||
|
|
a7c5b0ab46 | ||
|
|
e1a813e11f | ||
|
|
1dfeef0265 | ||
|
|
395610c117 | ||
|
|
ae96f82b82 | ||
|
|
2c610abcd0 | ||
|
|
d3591c8d9e |
@@ -1 +1 @@
|
||||
f31f13994768b5b07e29624406c9b053bf4bb26e1623ac2bc1e9d4a9477502d6
|
||||
075ed2142432dc59883bb52db8ac11270f952851d6400deae080f5468c7cb592
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"--privileged",
|
||||
"-e",
|
||||
"GIT_EDITOR=code --wait"
|
||||
// uncomment and edit the path in order to pass though local USB serial to the conatiner
|
||||
// uncomment and edit the path in order to pass through local USB serial to the container
|
||||
// , "--device=/dev/ttyACM0"
|
||||
],
|
||||
"appPort": 6052,
|
||||
|
||||
4
.github/actions/build-image/action.yaml
vendored
4
.github/actions/build-image/action.yaml
vendored
@@ -47,7 +47,7 @@ runs:
|
||||
|
||||
- name: Build and push to ghcr by digest
|
||||
id: build-ghcr
|
||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
env:
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
DOCKER_BUILD_RECORD_UPLOAD: false
|
||||
@@ -73,7 +73,7 @@ runs:
|
||||
|
||||
- name: Build and push to dockerhub by digest
|
||||
id: build-dockerhub
|
||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
env:
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
DOCKER_BUILD_RECORD_UPLOAD: false
|
||||
|
||||
2
.github/actions/restore-python/action.yml
vendored
2
.github/actions/restore-python/action.yml
vendored
@@ -22,7 +22,7 @@ runs:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: venv
|
||||
# yamllint disable-line rule:line-length
|
||||
|
||||
6
.github/workflows/auto-label-pr.yml
vendored
6
.github/workflows/auto-label-pr.yml
vendored
@@ -20,20 +20,20 @@ env:
|
||||
jobs:
|
||||
label:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.action != 'labeled' || github.event.sender.type != 'Bot'
|
||||
if: github.event.pull_request.state == 'open' && (github.event.action != 'labeled' || github.event.sender.type != 'Bot')
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v2
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v2
|
||||
with:
|
||||
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Auto Label PR
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{ steps.generate-token.outputs.token }}
|
||||
script: |
|
||||
|
||||
6
.github/workflows/ci-api-proto.yml
vendored
6
.github/workflows/ci-api-proto.yml
vendored
@@ -47,7 +47,7 @@ jobs:
|
||||
fi
|
||||
- if: failure()
|
||||
name: Review PR
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
await github.rest.pulls.createReview({
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
run: git diff
|
||||
- if: failure()
|
||||
name: Archive artifacts
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: generated-proto-files
|
||||
path: |
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
esphome/components/api/api_pb2_service.*
|
||||
- if: success()
|
||||
name: Dismiss review
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
let reviews = await github.rest.pulls.listReviews({
|
||||
|
||||
4
.github/workflows/ci-clang-tidy-hash.yml
vendored
4
.github/workflows/ci-clang-tidy-hash.yml
vendored
@@ -42,7 +42,7 @@ jobs:
|
||||
|
||||
- if: failure() && github.event.pull_request.head.repo.full_name == github.repository
|
||||
name: Request changes
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
await github.rest.pulls.createReview({
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
|
||||
- if: success() && github.event.pull_request.head.repo.full_name == github.repository
|
||||
name: Dismiss review
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
let reviews = await github.rest.pulls.listReviews({
|
||||
|
||||
34
.github/workflows/ci.yml
vendored
34
.github/workflows/ci.yml
vendored
@@ -47,7 +47,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: venv
|
||||
# yamllint disable-line rule:line-length
|
||||
@@ -159,7 +159,7 @@ jobs:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
- name: Save Python virtual environment cache
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: venv
|
||||
key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
|
||||
@@ -198,7 +198,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Restore components graph cache
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: .temp/components_graph.json
|
||||
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
|
||||
@@ -231,7 +231,7 @@ jobs:
|
||||
echo "benchmarks=$(echo "$output" | jq -r '.benchmarks')" >> $GITHUB_OUTPUT
|
||||
- name: Save components graph cache
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: .temp/components_graph.json
|
||||
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
|
||||
@@ -253,7 +253,7 @@ jobs:
|
||||
python-version: "3.13"
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: venv
|
||||
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
|
||||
@@ -387,14 +387,14 @@ jobs:
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref != 'refs/heads/dev'
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
||||
@@ -466,14 +466,14 @@ jobs:
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref != 'refs/heads/dev'
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
@@ -555,14 +555,14 @@ jobs:
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref != 'refs/heads/dev'
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
@@ -817,7 +817,7 @@ jobs:
|
||||
- name: Restore cached memory analysis
|
||||
id: cache-memory-analysis
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true'
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: memory-analysis-target.json
|
||||
key: ${{ steps.cache-key.outputs.cache-key }}
|
||||
@@ -841,7 +841,7 @@ jobs:
|
||||
|
||||
- name: Cache platformio
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
|
||||
@@ -883,7 +883,7 @@ jobs:
|
||||
|
||||
- name: Save memory analysis to cache
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success'
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: memory-analysis-target.json
|
||||
key: ${{ steps.cache-key.outputs.cache-key }}
|
||||
@@ -904,7 +904,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Upload memory analysis JSON
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: memory-analysis-target
|
||||
path: memory-analysis-target.json
|
||||
@@ -930,7 +930,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Cache platformio
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
|
||||
@@ -969,7 +969,7 @@ jobs:
|
||||
--platform "$platform"
|
||||
|
||||
- name: Upload memory analysis JSON
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: memory-analysis-pr
|
||||
path: memory-analysis-pr.json
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
CODEOWNERS
|
||||
|
||||
- name: Check codeowner approval and update label
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
with:
|
||||
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
|
||||
- name: Request reviews from component codeowners
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const { loadCodeowners, getEffectiveOwners } = require('./.github/scripts/codeowners.js');
|
||||
|
||||
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -58,7 +58,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
|
||||
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
@@ -86,6 +86,6 @@ jobs:
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
|
||||
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
2
.github/workflows/external-component-bot.yml
vendored
2
.github/workflows/external-component-bot.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Add external component comment
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
|
||||
2
.github/workflows/issue-codeowner-notify.yml
vendored
2
.github/workflows/issue-codeowner-notify.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Notify codeowners for component issues
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const owner = context.repo.owner;
|
||||
|
||||
2
.github/workflows/lock.yml
vendored
2
.github/workflows/lock.yml
vendored
@@ -8,4 +8,4 @@ on:
|
||||
|
||||
jobs:
|
||||
lock:
|
||||
uses: esphome/workflows/.github/workflows/lock.yml@main
|
||||
uses: esphome/workflows/.github/workflows/lock.yml@3c4e8446aa1029f1c346a482034b3ee1489077ca # 2026.4.0
|
||||
|
||||
2
.github/workflows/pr-title-check.yml
vendored
2
.github/workflows/pr-title-check.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const {
|
||||
|
||||
14
.github/workflows/release.yml
vendored
14
.github/workflows/release.yml
vendored
@@ -138,7 +138,7 @@ jobs:
|
||||
# version: ${{ needs.init.outputs.tag }}
|
||||
|
||||
- name: Upload digests
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: digests-${{ matrix.platform.arch }}
|
||||
path: /tmp/digests
|
||||
@@ -221,7 +221,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||
with:
|
||||
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
@@ -229,7 +229,7 @@ jobs:
|
||||
repositories: home-assistant-addon
|
||||
|
||||
- name: Trigger Workflow
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{ steps.generate-token.outputs.token }}
|
||||
script: |
|
||||
@@ -256,7 +256,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||
with:
|
||||
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
@@ -264,7 +264,7 @@ jobs:
|
||||
repositories: esphome-schema
|
||||
|
||||
- name: Trigger Workflow
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{ steps.generate-token.outputs.token }}
|
||||
script: |
|
||||
@@ -287,7 +287,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||
with:
|
||||
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
@@ -295,7 +295,7 @@ jobs:
|
||||
repositories: version-notifier
|
||||
|
||||
- name: Trigger Workflow
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{ steps.generate-token.outputs.token }}
|
||||
script: |
|
||||
|
||||
2
.github/workflows/status-check-labels.yml
vendored
2
.github/workflows/status-check-labels.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check for blocking labels
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const blockingLabels = ['needs-docs', 'merge-after-release', 'chained-pr'];
|
||||
|
||||
2
.github/workflows/sync-device-classes.yml
vendored
2
.github/workflows/sync-device-classes.yml
vendored
@@ -41,7 +41,7 @@ jobs:
|
||||
python script/run-in-env.py pre-commit run --all-files
|
||||
|
||||
- name: Commit changes
|
||||
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
|
||||
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
||||
with:
|
||||
commit-message: "Synchronise Device Classes from Home Assistant"
|
||||
committer: esphomebot <esphome@openhomefoundation.org>
|
||||
|
||||
@@ -11,7 +11,7 @@ ci:
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.15.10
|
||||
rev: v0.15.11
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -569,6 +569,7 @@ def wrap_to_code(name, comp):
|
||||
|
||||
@functools.wraps(comp.to_code)
|
||||
async def wrapped(conf):
|
||||
cg.add(cg.ComponentMarker(name))
|
||||
cg.add(cg.LineComment(f"{name}:"))
|
||||
if comp.config_schema is not None:
|
||||
conf_str = yaml_util.dump(conf)
|
||||
@@ -750,8 +751,15 @@ def upload_using_esptool(
|
||||
platformio_api.FlashImage(
|
||||
path=idedata.firmware_bin_path, offset=firmware_offset
|
||||
),
|
||||
*idedata.extra_flash_images,
|
||||
]
|
||||
for image in idedata.extra_flash_images:
|
||||
if not image.path.is_file():
|
||||
_LOGGER.warning(
|
||||
"Skipping missing flash image declared by platform: %s",
|
||||
image.path,
|
||||
)
|
||||
continue
|
||||
flash_images.append(image)
|
||||
|
||||
mcu = "esp8266"
|
||||
if CORE.is_esp32:
|
||||
|
||||
@@ -199,11 +199,10 @@ def validate_automation(extra_schema=None, extra_validators=None, single=False):
|
||||
return cv.Schema([schema])(value)
|
||||
except cv.Invalid as err2:
|
||||
if "extra keys not allowed" in str(err2) and len(err2.path) == 2:
|
||||
# pylint: disable=raise-missing-from
|
||||
raise err
|
||||
raise err from None
|
||||
if "Unable to find action" in str(err):
|
||||
raise err2
|
||||
raise cv.MultipleInvalid([err, err2])
|
||||
raise err2 from None
|
||||
raise cv.MultipleInvalid([err, err2]) from None
|
||||
elif isinstance(value, dict):
|
||||
if CONF_THEN in value:
|
||||
return [schema(value)]
|
||||
|
||||
@@ -151,8 +151,8 @@ class ConfigBundleCreator:
|
||||
|
||||
def __init__(self, config: dict[str, Any]) -> None:
|
||||
self._config = config
|
||||
self._config_dir = CORE.config_dir
|
||||
self._config_path = CORE.config_path
|
||||
self._config_dir = Path(CORE.config_dir).resolve()
|
||||
self._config_path = Path(CORE.config_path).resolve()
|
||||
self._files: list[BundleFile] = []
|
||||
self._seen_paths: set[Path] = set()
|
||||
self._secrets_paths: set[Path] = set()
|
||||
@@ -258,21 +258,36 @@ class ConfigBundleCreator:
|
||||
def _discover_yaml_includes(self) -> None:
|
||||
"""Discover YAML files loaded during config parsing.
|
||||
|
||||
We track files by wrapping _load_yaml_internal. The config has already
|
||||
been loaded at this point (bundle is a POST_CONFIG_ACTION), so we
|
||||
re-load just to discover the file list.
|
||||
Deliberately uses a fresh re-parse and force-loads every deferred
|
||||
``IncludeFile`` to include *all* potentially-reachable includes,
|
||||
even branches not selected by the local substitutions. Bundles are
|
||||
meant to be compiled on another system where command-line
|
||||
substitution overrides may choose a different branch — e.g.
|
||||
``!include network/${eth_model}/config.yaml`` must ship every
|
||||
candidate so the remote build can pick any one.
|
||||
|
||||
Entries with unresolved substitution variables in the filename
|
||||
path are skipped with a warning (they cannot be resolved without
|
||||
the substitution pass).
|
||||
|
||||
Secrets files are tracked separately so we can filter them to
|
||||
only include the keys this config actually references.
|
||||
"""
|
||||
# Must be a fresh parse: IncludeFile.load() caches its result in
|
||||
# _content, and we discover files by listening for loader calls. On
|
||||
# an already-parsed tree the cache is populated, .load() returns
|
||||
# without calling the loader, the listener never fires, and the
|
||||
# referenced files would be silently dropped from the bundle.
|
||||
with yaml_util.track_yaml_loads() as loaded_files:
|
||||
try:
|
||||
yaml_util.load_yaml(self._config_path)
|
||||
data = yaml_util.load_yaml(self._config_path)
|
||||
except EsphomeError:
|
||||
_LOGGER.debug(
|
||||
"Bundle: re-loading YAML for include discovery failed, "
|
||||
"proceeding with partial file list"
|
||||
)
|
||||
else:
|
||||
_force_load_include_files(data)
|
||||
|
||||
for fpath in loaded_files:
|
||||
if fpath == self._config_path.resolve():
|
||||
@@ -608,6 +623,57 @@ def _add_bytes_to_tar(tar: tarfile.TarFile, name: str, data: bytes) -> None:
|
||||
tar.addfile(info, io.BytesIO(data))
|
||||
|
||||
|
||||
def _force_load_include_files(obj: Any, _seen: set[int] | None = None) -> None:
|
||||
"""Recursively resolve any ``IncludeFile`` instances in a YAML tree.
|
||||
|
||||
Nested ``!include`` returns a deferred ``IncludeFile`` that is only
|
||||
resolved during the substitution pass. During bundle discovery we need
|
||||
the referenced files to actually load so the ``track_yaml_loads``
|
||||
listener fires for them.
|
||||
|
||||
``IncludeFile`` instances with unresolved substitution variables in the
|
||||
filename cannot be loaded — we skip and warn about those.
|
||||
"""
|
||||
if _seen is None:
|
||||
_seen = set()
|
||||
|
||||
if isinstance(obj, yaml_util.IncludeFile):
|
||||
if id(obj) in _seen:
|
||||
return
|
||||
_seen.add(id(obj))
|
||||
if obj.has_unresolved_expressions():
|
||||
_LOGGER.warning(
|
||||
"Bundle: cannot resolve !include %s (referenced from %s) "
|
||||
"with substitutions in path",
|
||||
obj.file,
|
||||
obj.parent_file,
|
||||
)
|
||||
return
|
||||
try:
|
||||
loaded = obj.load()
|
||||
except EsphomeError as err:
|
||||
_LOGGER.warning(
|
||||
"Bundle: failed to load !include %s (referenced from %s): %s",
|
||||
obj.file,
|
||||
obj.parent_file,
|
||||
err,
|
||||
)
|
||||
return
|
||||
_force_load_include_files(loaded, _seen)
|
||||
elif isinstance(obj, dict):
|
||||
if id(obj) in _seen:
|
||||
return
|
||||
_seen.add(id(obj))
|
||||
for value in obj.values():
|
||||
_force_load_include_files(value, _seen)
|
||||
elif isinstance(obj, (list, tuple)):
|
||||
if id(obj) in _seen:
|
||||
return
|
||||
_seen.add(id(obj))
|
||||
for item in obj:
|
||||
_force_load_include_files(item, _seen)
|
||||
|
||||
|
||||
def _resolve_include_path(include_path: Any) -> Path | None:
|
||||
"""Resolve an include path to absolute, skipping system includes."""
|
||||
if isinstance(include_path, str) and include_path.startswith("<"):
|
||||
|
||||
@@ -10,8 +10,10 @@
|
||||
# pylint: disable=unused-import
|
||||
from esphome.cpp_generator import ( # noqa: F401
|
||||
ArrayInitializer,
|
||||
ComponentMarker,
|
||||
Expression,
|
||||
FlashStringLiteral,
|
||||
IIFEUnsafeStatement,
|
||||
LineComment,
|
||||
LogStringLiteral,
|
||||
MockObj,
|
||||
|
||||
@@ -2,7 +2,11 @@ import logging
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor, voltage_sampler
|
||||
from esphome.components.esp32 import get_esp32_variant, include_builtin_idf_component
|
||||
from esphome.components.esp32 import (
|
||||
get_esp32_variant,
|
||||
include_builtin_idf_component,
|
||||
require_adc_oneshot_iram,
|
||||
)
|
||||
from esphome.components.nrf52.const import AIN_TO_GPIO, EXTRA_ADC
|
||||
from esphome.components.zephyr import (
|
||||
zephyr_add_overlay,
|
||||
@@ -24,6 +28,7 @@ from esphome.const import (
|
||||
PlatformFramework,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import (
|
||||
ATTENUATION_MODES,
|
||||
@@ -65,6 +70,13 @@ def validate_config(config):
|
||||
return config
|
||||
|
||||
|
||||
def _require_adc_iram(config: ConfigType) -> ConfigType:
|
||||
"""Register ADC oneshot IRAM requirement during config validation."""
|
||||
if CORE.is_esp32:
|
||||
require_adc_oneshot_iram()
|
||||
return config
|
||||
|
||||
|
||||
ADCSensor = adc_ns.class_(
|
||||
"ADCSensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler
|
||||
)
|
||||
@@ -95,6 +107,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s")),
|
||||
validate_config,
|
||||
_require_adc_iram,
|
||||
)
|
||||
|
||||
CONF_ADC_CHANNEL_ID = "adc_channel_id"
|
||||
|
||||
@@ -671,6 +671,7 @@ message SensorStateResponse {
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_SENSOR";
|
||||
option (no_delay) = true;
|
||||
option (speed_optimized) = true;
|
||||
|
||||
fixed32 key = 1 [(force) = true];
|
||||
float state = 2;
|
||||
@@ -777,9 +778,10 @@ message SubscribeLogsResponse {
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (log) = false;
|
||||
option (no_delay) = false;
|
||||
option (speed_optimized) = true;
|
||||
|
||||
LogLevel level = 1;
|
||||
bytes message = 3;
|
||||
LogLevel level = 1 [(force) = true];
|
||||
bytes message = 3 [(force) = true];
|
||||
}
|
||||
|
||||
// ==================== NOISE ENCRYPTION ====================
|
||||
@@ -1638,6 +1640,7 @@ message BluetoothLERawAdvertisementsResponse {
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_BLUETOOTH_PROXY";
|
||||
option (no_delay) = true;
|
||||
option (speed_optimized) = true;
|
||||
|
||||
repeated BluetoothLERawAdvertisement advertisements = 1 [(fixed_array_with_length_define) = "BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE"];
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ extend google.protobuf.MessageOptions {
|
||||
optional bool no_delay = 1040 [default=false];
|
||||
optional string base_class = 1041;
|
||||
optional bool inline_encode = 1042 [default=false];
|
||||
optional bool speed_optimized = 1043 [default=false];
|
||||
}
|
||||
|
||||
extend google.protobuf.FieldOptions {
|
||||
|
||||
@@ -745,7 +745,9 @@ uint32_t ListEntitiesSensorResponse::calculate_size() const {
|
||||
#endif
|
||||
return size;
|
||||
}
|
||||
uint8_t *SensorStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
|
||||
uint8_t *
|
||||
SensorStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
uint8_t *__restrict__ pos = buffer.get_pos();
|
||||
ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key);
|
||||
ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 2, this->state);
|
||||
@@ -755,7 +757,9 @@ uint8_t *SensorStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG
|
||||
#endif
|
||||
return pos;
|
||||
}
|
||||
uint32_t SensorStateResponse::calculate_size() const {
|
||||
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
|
||||
uint32_t
|
||||
SensorStateResponse::calculate_size() const {
|
||||
uint32_t size = 0;
|
||||
size += 5;
|
||||
size += ProtoSize::calc_float(1, this->state);
|
||||
@@ -912,16 +916,22 @@ bool SubscribeLogsRequest::decode_varint(uint32_t field_id, proto_varint_value_t
|
||||
}
|
||||
return true;
|
||||
}
|
||||
uint8_t *SubscribeLogsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
|
||||
uint8_t *
|
||||
SubscribeLogsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
uint8_t *__restrict__ pos = buffer.get_pos();
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, static_cast<uint32_t>(this->level));
|
||||
ProtoEncode::encode_bytes(pos PROTO_ENCODE_DEBUG_ARG, 3, this->message_ptr_, this->message_len_);
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, static_cast<uint32_t>(this->level), true);
|
||||
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 26);
|
||||
ProtoEncode::encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, this->message_len_);
|
||||
ProtoEncode::encode_raw(pos PROTO_ENCODE_DEBUG_ARG, this->message_ptr_, this->message_len_);
|
||||
return pos;
|
||||
}
|
||||
uint32_t SubscribeLogsResponse::calculate_size() const {
|
||||
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
|
||||
uint32_t
|
||||
SubscribeLogsResponse::calculate_size() const {
|
||||
uint32_t size = 0;
|
||||
size += this->level ? 2 : 0;
|
||||
size += ProtoSize::calc_length(1, this->message_len_);
|
||||
size += 2;
|
||||
size += ProtoSize::calc_length_force(1, this->message_len_);
|
||||
return size;
|
||||
}
|
||||
#ifdef USE_API_NOISE
|
||||
@@ -2328,7 +2338,9 @@ bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id,
|
||||
}
|
||||
return true;
|
||||
}
|
||||
uint8_t *BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
|
||||
uint8_t *
|
||||
BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
uint8_t *__restrict__ pos = buffer.get_pos();
|
||||
for (uint16_t i = 0; i < this->advertisements_len; i++) {
|
||||
auto &sub_msg = this->advertisements[i];
|
||||
@@ -2350,7 +2362,9 @@ uint8_t *BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer P
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
uint32_t BluetoothLERawAdvertisementsResponse::calculate_size() const {
|
||||
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
|
||||
uint32_t
|
||||
BluetoothLERawAdvertisementsResponse::calculate_size() const {
|
||||
uint32_t size = 0;
|
||||
for (uint16_t i = 0; i < this->advertisements_len; i++) {
|
||||
auto &sub_msg = this->advertisements[i];
|
||||
|
||||
@@ -298,33 +298,15 @@ constexpr uint32_t VARINT_MAX_2_BYTE = 1 << 14; // 16384
|
||||
class ProtoEncode {
|
||||
public:
|
||||
/// Write a multi-byte varint directly through a pos pointer.
|
||||
/// Unrolled based on the compile-time max varint length for T
|
||||
/// (5 bytes for uint32_t, 10 bytes for uint64_t). The explicit unroll gives
|
||||
/// the compiler straight-line code with early exits instead of a
|
||||
/// data-dependent back-edge branch, which measurably speeds up BLE raw
|
||||
/// advertisement MAC encoding (always 7-byte varints) among other hot paths.
|
||||
///
|
||||
/// Takes/returns pos by value so this function can remain out-of-line without
|
||||
/// forcing the caller to spill pos to memory on every byte write. Callers
|
||||
/// (which are ALWAYS_INLINE) should assign the returned pos back to their
|
||||
/// local. Code size win vs. inlining the whole unroll at every call site.
|
||||
template<typename T>
|
||||
static uint8_t *encode_varint_raw_loop(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, T value) {
|
||||
constexpr int MAX_VARINT_BYTES = (sizeof(T) * 8 + 6) / 7; // 5 for u32, 10 for u64
|
||||
#pragma GCC unroll 10
|
||||
for (int i = 0; i < MAX_VARINT_BYTES - 1; i++) {
|
||||
static inline void encode_varint_raw_loop(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, T value) {
|
||||
do {
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
|
||||
*pos++ = static_cast<uint8_t>(value) | 0x80;
|
||||
*pos++ = static_cast<uint8_t>(value | 0x80);
|
||||
value >>= 7;
|
||||
if (value <= 0x7F) {
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
|
||||
*pos++ = static_cast<uint8_t>(value);
|
||||
return pos;
|
||||
}
|
||||
}
|
||||
} while (value > 0x7F);
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
|
||||
*pos++ = static_cast<uint8_t>(value);
|
||||
return pos;
|
||||
}
|
||||
static inline void ESPHOME_ALWAYS_INLINE encode_varint_raw(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t value) {
|
||||
@@ -333,7 +315,7 @@ class ProtoEncode {
|
||||
*pos++ = static_cast<uint8_t>(value);
|
||||
return;
|
||||
}
|
||||
pos = encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value);
|
||||
encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value);
|
||||
}
|
||||
/// Encode a varint that is expected to be 1-2 bytes (e.g. zigzag RSSI, small lengths).
|
||||
static inline void ESPHOME_ALWAYS_INLINE encode_varint_raw_short(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
@@ -349,7 +331,7 @@ class ProtoEncode {
|
||||
*pos++ = static_cast<uint8_t>(value >> 7);
|
||||
return;
|
||||
}
|
||||
pos = encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value);
|
||||
encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value);
|
||||
}
|
||||
static inline void ESPHOME_ALWAYS_INLINE encode_varint_raw_64(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint64_t value) {
|
||||
@@ -358,7 +340,7 @@ class ProtoEncode {
|
||||
*pos++ = static_cast<uint8_t>(value);
|
||||
return;
|
||||
}
|
||||
pos = encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value);
|
||||
encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value);
|
||||
}
|
||||
static inline void ESPHOME_ALWAYS_INLINE encode_field_raw(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, uint32_t type) {
|
||||
@@ -420,7 +402,7 @@ class ProtoEncode {
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 1 + len);
|
||||
*pos++ = static_cast<uint8_t>(len);
|
||||
} else {
|
||||
pos = encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, len);
|
||||
encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, len);
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, len);
|
||||
}
|
||||
std::memcpy(pos, string, len);
|
||||
|
||||
@@ -83,7 +83,7 @@ def angle_to_position(value, min=-360, max=360):
|
||||
value = angle(min=min, max=max)(value)
|
||||
return (RESOLUTION + round(value * ANGLE_TO_POSITION)) % RESOLUTION
|
||||
except cv.Invalid as e:
|
||||
raise cv.Invalid(f"When using angle, {e.error_message}")
|
||||
raise cv.Invalid(f"When using angle, {e.error_message}") from e
|
||||
|
||||
|
||||
def percent_to_position(value):
|
||||
@@ -164,7 +164,7 @@ def has_valid_range_config():
|
||||
except cv.Invalid as e:
|
||||
raise cv.Invalid(
|
||||
f"The range between start and end position is invalid. It was was {range} but {e.error_message}"
|
||||
)
|
||||
) from e
|
||||
|
||||
return validator
|
||||
|
||||
|
||||
@@ -111,14 +111,14 @@ class ATM90E32Component : public PollingComponent,
|
||||
#endif
|
||||
float get_reference_voltage(uint8_t phase) {
|
||||
#ifdef USE_NUMBER
|
||||
return (phase >= 0 && phase < 3 && ref_voltages_[phase]) ? ref_voltages_[phase]->state : 120.0; // Default voltage
|
||||
return (phase < 3 && ref_voltages_[phase]) ? ref_voltages_[phase]->state : 120.0; // Default voltage
|
||||
#else
|
||||
return 120.0; // Default voltage
|
||||
#endif
|
||||
}
|
||||
float get_reference_current(uint8_t phase) {
|
||||
#ifdef USE_NUMBER
|
||||
return (phase >= 0 && phase < 3 && ref_currents_[phase]) ? ref_currents_[phase]->state : 5.0f; // Default current
|
||||
return (phase < 3 && ref_currents_[phase]) ? ref_currents_[phase]->state : 5.0f; // Default current
|
||||
#else
|
||||
return 5.0f; // Default current
|
||||
#endif
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.esp32 import add_idf_component, include_builtin_idf_component
|
||||
from esphome.components.esp32 import (
|
||||
add_idf_component,
|
||||
add_idf_sdkconfig_option,
|
||||
include_builtin_idf_component,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_BITS_PER_SAMPLE, CONF_NUM_CHANNELS, CONF_SAMPLE_RATE
|
||||
from esphome.core import CORE
|
||||
@@ -27,6 +31,7 @@ class AudioData:
|
||||
flac_support: bool = False
|
||||
mp3_support: bool = False
|
||||
opus_support: bool = False
|
||||
micro_decoder_support: bool = False
|
||||
|
||||
|
||||
def _get_data() -> AudioData:
|
||||
@@ -50,6 +55,11 @@ def request_opus_support() -> None:
|
||||
_get_data().opus_support = True
|
||||
|
||||
|
||||
def request_micro_decoder_support() -> None:
|
||||
"""Request micro-decoder library support for audio decoding."""
|
||||
_get_data().micro_decoder_support = True
|
||||
|
||||
|
||||
CONF_MIN_BITS_PER_SAMPLE = "min_bits_per_sample"
|
||||
CONF_MAX_BITS_PER_SAMPLE = "max_bits_per_sample"
|
||||
CONF_MIN_CHANNELS = "min_channels"
|
||||
@@ -208,6 +218,19 @@ async def to_code(config):
|
||||
)
|
||||
|
||||
data = _get_data()
|
||||
|
||||
if data.micro_decoder_support:
|
||||
add_idf_component(name="esphome/micro-decoder", ref="0.1.1")
|
||||
|
||||
# All codecs are enabled by default in micro-decoder, so disable the ones that aren't requested to save flash
|
||||
if not data.flac_support:
|
||||
add_idf_sdkconfig_option("CONFIG_MICRO_DECODER_CODEC_FLAC", False)
|
||||
if not data.mp3_support:
|
||||
add_idf_sdkconfig_option("CONFIG_MICRO_DECODER_CODEC_MP3", False)
|
||||
if not data.opus_support:
|
||||
add_idf_sdkconfig_option("CONFIG_MICRO_DECODER_CODEC_OPUS", False)
|
||||
|
||||
# Legacy audio_decoder.cpp support defines and components
|
||||
if data.flac_support:
|
||||
cg.add_define("USE_AUDIO_FLAC_SUPPORT")
|
||||
add_idf_component(name="esphome/micro-flac", ref="0.1.1")
|
||||
|
||||
@@ -116,7 +116,7 @@ def read_audio_file_and_type(file_config: ConfigType) -> tuple[bytes, MockObj]:
|
||||
raise cv.Invalid(
|
||||
f"Unable to determine audio file type of '{path}'. "
|
||||
f"Try re-encoding the file into a supported format. Details: {e}"
|
||||
)
|
||||
) from e
|
||||
|
||||
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["NONE"]
|
||||
if file_type == "wav":
|
||||
|
||||
@@ -332,8 +332,9 @@ def parse_multi_click_timing_str(value):
|
||||
try:
|
||||
state = cv.boolean(parts[0])
|
||||
except cv.Invalid:
|
||||
# pylint: disable=raise-missing-from
|
||||
raise cv.Invalid(f"First word must either be ON or OFF, not {parts[0]}")
|
||||
raise cv.Invalid(
|
||||
f"First word must either be ON or OFF, not {parts[0]}"
|
||||
) from None
|
||||
|
||||
if parts[1] != "for":
|
||||
raise cv.Invalid(f"Second word must be 'for', got {parts[1]}")
|
||||
@@ -350,7 +351,9 @@ def parse_multi_click_timing_str(value):
|
||||
try:
|
||||
length = cv.positive_time_period_milliseconds(parts[4])
|
||||
except cv.Invalid as err:
|
||||
raise cv.Invalid(f"Multi Click Grammar Parsing length failed: {err}")
|
||||
raise cv.Invalid(
|
||||
f"Multi Click Grammar Parsing length failed: {err}"
|
||||
) from err
|
||||
return {CONF_STATE: state, key: str(length)}
|
||||
|
||||
if parts[3] != "to":
|
||||
@@ -359,12 +362,16 @@ def parse_multi_click_timing_str(value):
|
||||
try:
|
||||
min_length = cv.positive_time_period_milliseconds(parts[2])
|
||||
except cv.Invalid as err:
|
||||
raise cv.Invalid(f"Multi Click Grammar Parsing minimum length failed: {err}")
|
||||
raise cv.Invalid(
|
||||
f"Multi Click Grammar Parsing minimum length failed: {err}"
|
||||
) from err
|
||||
|
||||
try:
|
||||
max_length = cv.positive_time_period_milliseconds(parts[4])
|
||||
except cv.Invalid as err:
|
||||
raise cv.Invalid(f"Multi Click Grammar Parsing minimum length failed: {err}")
|
||||
raise cv.Invalid(
|
||||
f"Multi Click Grammar Parsing maximum length failed: {err}"
|
||||
) from err
|
||||
|
||||
return {
|
||||
CONF_STATE: state,
|
||||
|
||||
@@ -65,3 +65,8 @@ async def to_code(config):
|
||||
@pins.PIN_SCHEMA_REGISTRY.register("bk72xx", PIN_SCHEMA)
|
||||
async def pin_to_code(config):
|
||||
return await libretiny.gpio.component_pin_to_code(config)
|
||||
|
||||
|
||||
# Called by writer.py; delegates to the shared libretiny implementation.
|
||||
def copy_files() -> None:
|
||||
libretiny.copy_files()
|
||||
|
||||
@@ -63,7 +63,7 @@ void BM8563::read_time() {
|
||||
rtc_time.day_of_week, rtc_time.hour, rtc_time.minute, rtc_time.second);
|
||||
|
||||
rtc_time.recalc_timestamp_utc(false);
|
||||
if (!rtc_time.is_valid()) {
|
||||
if (!rtc_time.is_valid(/*check_day_of_week=*/true, /*check_day_of_year=*/false)) {
|
||||
ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ CODEOWNERS = ["@neffs", "@kbx81"]
|
||||
DOMAIN = "bme68x_bsec2"
|
||||
|
||||
BSEC2_LIBRARY_VERSION = "1.10.2610"
|
||||
BME68x_LIBRARY_VERSION = "v1.3.40408"
|
||||
|
||||
CONF_ALGORITHM_OUTPUT = "algorithm_output"
|
||||
CONF_BME68X_BSEC2_ID = "bme68x_bsec2_id"
|
||||
@@ -170,7 +171,9 @@ async def to_code_base(config):
|
||||
with open(path, encoding="utf-8") as f:
|
||||
bsec2_iaq_config = f.read()
|
||||
except Exception as e:
|
||||
raise core.EsphomeError(f"Could not open binary configuration file {path}: {e}")
|
||||
raise core.EsphomeError(
|
||||
f"Could not open binary configuration file {path}: {e}"
|
||||
) from e
|
||||
|
||||
# Convert retrieved BSEC2 config to an array of ints
|
||||
rhs = [int(x) for x in bsec2_iaq_config.split(",")]
|
||||
@@ -184,16 +187,31 @@ async def to_code_base(config):
|
||||
if core.CORE.using_arduino:
|
||||
cg.add_library("Wire", None)
|
||||
cg.add_library("SPI", None)
|
||||
cg.add_library(
|
||||
"BME68x Sensor library",
|
||||
None,
|
||||
"https://github.com/boschsensortec/Bosch-BME68x-Library#v1.3.40408",
|
||||
)
|
||||
cg.add_library(
|
||||
"BSEC2 Software Library",
|
||||
None,
|
||||
f"https://github.com/boschsensortec/Bosch-BSEC2-Library.git#{BSEC2_LIBRARY_VERSION}",
|
||||
)
|
||||
|
||||
if core.CORE.is_esp32:
|
||||
from esphome.components.esp32 import add_idf_component
|
||||
|
||||
add_idf_component(
|
||||
name="boschsensortec/Bosch-BME68x-Library",
|
||||
repo="https://github.com/esphome-libs/Bosch-BME68x-Library",
|
||||
ref=BME68x_LIBRARY_VERSION,
|
||||
)
|
||||
add_idf_component(
|
||||
name="boschsensortec/Bosch-BSEC2-Library",
|
||||
repo="https://github.com/esphome-libs/Bosch-BSEC2-Library",
|
||||
ref=BSEC2_LIBRARY_VERSION,
|
||||
)
|
||||
else:
|
||||
cg.add_library(
|
||||
"BME68x Sensor library",
|
||||
None,
|
||||
f"https://github.com/boschsensortec/Bosch-BME68x-Library#{BME68x_LIBRARY_VERSION}",
|
||||
)
|
||||
cg.add_library(
|
||||
"BSEC2 Software Library",
|
||||
None,
|
||||
f"https://github.com/boschsensortec/Bosch-BSEC2-Library.git#{BSEC2_LIBRARY_VERSION}",
|
||||
)
|
||||
|
||||
cg.add_define("USE_BSEC2")
|
||||
|
||||
|
||||
@@ -162,7 +162,6 @@ async def canbus_action_to_code(config, action_id, template_arg, args):
|
||||
await cg.register_parented(var, config[CONF_CANBUS_ID])
|
||||
|
||||
if (can_id := config.get(CONF_CAN_ID)) is not None:
|
||||
can_id = await cg.templatable(can_id, args, cg.uint32)
|
||||
cg.add(var.set_can_id(can_id))
|
||||
cg.add(var.set_use_extended_id(config[CONF_USE_EXTENDED_ID]))
|
||||
|
||||
|
||||
@@ -102,8 +102,34 @@ CC1101Component::CC1101Component() {
|
||||
memset(this->pa_table_, 0, sizeof(this->pa_table_));
|
||||
}
|
||||
|
||||
void IRAM_ATTR CC1101Component::gpio_intr(CC1101Component *arg) { arg->enable_loop_soon_any_context(); }
|
||||
|
||||
void CC1101Component::setup() {
|
||||
this->spi_setup();
|
||||
|
||||
if (this->gdo0_pin_ != nullptr) {
|
||||
this->gdo0_pin_->setup();
|
||||
}
|
||||
|
||||
this->configure();
|
||||
if (this->is_failed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Defer pin mode setup until after all components have completed setup()
|
||||
// This handles the case where remote_transmitter runs after CC1101 and changes pin mode
|
||||
if (this->gdo0_pin_ != nullptr) {
|
||||
this->defer([this]() {
|
||||
this->gdo0_pin_->pin_mode(gpio::FLAG_INPUT);
|
||||
if (this->state_.PKT_FORMAT == static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO)) {
|
||||
this->gdo0_pin_->attach_interrupt(&CC1101Component::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::configure() {
|
||||
// Manual reset sequence per CC1101 datasheet section 19.1.2
|
||||
this->cs_->digital_write(true);
|
||||
delayMicroseconds(1);
|
||||
this->cs_->digital_write(false);
|
||||
@@ -126,11 +152,6 @@ void CC1101Component::setup() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup GDO0 pin if configured
|
||||
if (this->gdo0_pin_ != nullptr) {
|
||||
this->gdo0_pin_->setup();
|
||||
}
|
||||
|
||||
this->initialized_ = true;
|
||||
|
||||
for (uint8_t i = 0; i <= static_cast<uint8_t>(Register::TEST0); i++) {
|
||||
@@ -140,20 +161,11 @@ void CC1101Component::setup() {
|
||||
this->write_(static_cast<Register>(i));
|
||||
}
|
||||
this->set_output_power(this->output_power_requested_);
|
||||
|
||||
if (!this->enter_rx_()) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// Defer pin mode setup until after all components have completed setup()
|
||||
// This handles the case where remote_transmitter runs after CC1101 and changes pin mode
|
||||
if (this->gdo0_pin_ != nullptr) {
|
||||
this->defer([this]() { this->gdo0_pin_->pin_mode(gpio::FLAG_INPUT); });
|
||||
}
|
||||
|
||||
if (this->state_.PKT_FORMAT != static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO)) {
|
||||
this->disable_loop();
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::call_listeners_(const std::vector<uint8_t> &packet, float freq_offset, float rssi, uint8_t lqi) {
|
||||
@@ -164,6 +176,7 @@ void CC1101Component::call_listeners_(const std::vector<uint8_t> &packet, float
|
||||
}
|
||||
|
||||
void CC1101Component::loop() {
|
||||
this->disable_loop();
|
||||
if (this->state_.PKT_FORMAT != static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO) || this->gdo0_pin_ == nullptr ||
|
||||
!this->gdo0_pin_->digital_read()) {
|
||||
return;
|
||||
@@ -244,6 +257,7 @@ void CC1101Component::begin_tx() {
|
||||
this->write_(Register::PKTCTRL0, 0x32);
|
||||
ESP_LOGV(TAG, "Beginning TX sequence");
|
||||
if (this->gdo0_pin_ != nullptr) {
|
||||
this->gdo0_pin_->detach_interrupt();
|
||||
this->gdo0_pin_->pin_mode(gpio::FLAG_OUTPUT);
|
||||
}
|
||||
// Transition through IDLE to bypass CCA (Clear Channel Assessment) which can
|
||||
@@ -268,7 +282,7 @@ void CC1101Component::begin_rx() {
|
||||
|
||||
void CC1101Component::reset() {
|
||||
this->strobe_(Command::RES);
|
||||
this->setup();
|
||||
this->configure();
|
||||
}
|
||||
|
||||
void CC1101Component::set_idle() {
|
||||
@@ -673,10 +687,12 @@ void CC1101Component::set_packet_mode(bool value) {
|
||||
this->state_.GDO0_CFG = 0x0D;
|
||||
}
|
||||
if (this->initialized_) {
|
||||
if (value) {
|
||||
this->enable_loop();
|
||||
} else {
|
||||
this->disable_loop();
|
||||
if (this->gdo0_pin_ != nullptr) {
|
||||
if (value) {
|
||||
this->gdo0_pin_->attach_interrupt(&CC1101Component::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE);
|
||||
} else {
|
||||
this->gdo0_pin_->detach_interrupt();
|
||||
}
|
||||
}
|
||||
this->write_(Register::PKTCTRL0);
|
||||
this->write_(Register::PKTCTRL1);
|
||||
|
||||
@@ -25,6 +25,7 @@ class CC1101Component : public Component,
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
void configure();
|
||||
|
||||
// Actions
|
||||
void begin_tx();
|
||||
@@ -93,6 +94,7 @@ class CC1101Component : public Component,
|
||||
|
||||
// GDO pin for packet reception
|
||||
InternalGPIOPin *gdo0_pin_{nullptr};
|
||||
static void IRAM_ATTR gpio_intr(CC1101Component *arg);
|
||||
|
||||
// Packet handling
|
||||
void call_listeners_(const std::vector<uint8_t> &packet, float freq_offset, float rssi, uint8_t lqi);
|
||||
|
||||
@@ -44,7 +44,7 @@ void DS1307Component::read_time() {
|
||||
.year = uint16_t(ds1307_.reg.year + 10u * ds1307_.reg.year_10 + 2000),
|
||||
};
|
||||
rtc_time.recalc_timestamp_utc(false);
|
||||
if (!rtc_time.is_valid()) {
|
||||
if (!rtc_time.is_valid(/*check_day_of_week=*/true, /*check_day_of_year=*/false)) {
|
||||
ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -43,3 +43,11 @@ wave_4_26.extend(
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
ssd1677.extend(
|
||||
"waveshare-3.97in",
|
||||
width=800,
|
||||
height=480,
|
||||
mirror_x=True,
|
||||
)
|
||||
|
||||
@@ -671,11 +671,12 @@ def _is_framework_url(source: str) -> bool:
|
||||
# The default/recommended arduino framework version
|
||||
# - https://github.com/espressif/arduino-esp32/releases
|
||||
ARDUINO_FRAMEWORK_VERSION_LOOKUP = {
|
||||
"recommended": cv.Version(3, 3, 7),
|
||||
"latest": cv.Version(3, 3, 7),
|
||||
"dev": cv.Version(3, 3, 7),
|
||||
"recommended": cv.Version(3, 3, 8),
|
||||
"latest": cv.Version(3, 3, 8),
|
||||
"dev": cv.Version(3, 3, 8),
|
||||
}
|
||||
ARDUINO_PLATFORM_VERSION_LOOKUP = {
|
||||
cv.Version(3, 3, 8): cv.Version(55, 3, 38, "1"),
|
||||
cv.Version(3, 3, 7): cv.Version(55, 3, 37),
|
||||
cv.Version(3, 3, 6): cv.Version(55, 3, 36),
|
||||
cv.Version(3, 3, 5): cv.Version(55, 3, 35),
|
||||
@@ -695,6 +696,7 @@ ARDUINO_PLATFORM_VERSION_LOOKUP = {
|
||||
# These versions correspond to pioarduino/esp-idf releases
|
||||
# See: https://github.com/pioarduino/esp-idf/releases
|
||||
ARDUINO_IDF_VERSION_LOOKUP = {
|
||||
cv.Version(3, 3, 8): cv.Version(5, 5, 4),
|
||||
cv.Version(3, 3, 7): cv.Version(5, 5, 3, "1"),
|
||||
cv.Version(3, 3, 6): cv.Version(5, 5, 2),
|
||||
cv.Version(3, 3, 5): cv.Version(5, 5, 2),
|
||||
@@ -714,17 +716,15 @@ ARDUINO_IDF_VERSION_LOOKUP = {
|
||||
# The default/recommended esp-idf framework version
|
||||
# - https://github.com/espressif/esp-idf/releases
|
||||
ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
|
||||
"recommended": cv.Version(5, 5, 3, "1"),
|
||||
"latest": cv.Version(5, 5, 3, "1"),
|
||||
"recommended": cv.Version(5, 5, 4),
|
||||
"latest": cv.Version(5, 5, 4),
|
||||
"dev": cv.Version(5, 5, 4),
|
||||
}
|
||||
ESP_IDF_PLATFORM_VERSION_LOOKUP = {
|
||||
cv.Version(
|
||||
6, 0, 0
|
||||
): "https://github.com/pioarduino/platform-espressif32.git#prep_IDF6",
|
||||
cv.Version(
|
||||
5, 5, 4
|
||||
): "https://github.com/pioarduino/platform-espressif32.git#develop",
|
||||
cv.Version(5, 5, 4): cv.Version(55, 3, 38, "1"),
|
||||
cv.Version(5, 5, 3, "1"): cv.Version(55, 3, 37),
|
||||
cv.Version(5, 5, 3): cv.Version(55, 3, 37),
|
||||
cv.Version(5, 5, 2): cv.Version(55, 3, 37),
|
||||
@@ -744,8 +744,8 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = {
|
||||
# The platform-espressif32 version
|
||||
# - https://github.com/pioarduino/platform-espressif32/releases
|
||||
PLATFORM_VERSION_LOOKUP = {
|
||||
"recommended": cv.Version(55, 3, 37),
|
||||
"latest": cv.Version(55, 3, 37),
|
||||
"recommended": cv.Version(55, 3, 38, "1"),
|
||||
"latest": cv.Version(55, 3, 38, "1"),
|
||||
"dev": "https://github.com/pioarduino/platform-espressif32.git#develop",
|
||||
}
|
||||
|
||||
@@ -1058,6 +1058,7 @@ CONF_DISABLE_MBEDTLS_PEER_CERT = "disable_mbedtls_peer_cert"
|
||||
CONF_DISABLE_MBEDTLS_PKCS7 = "disable_mbedtls_pkcs7"
|
||||
CONF_DISABLE_REGI2C_IN_IRAM = "disable_regi2c_in_iram"
|
||||
CONF_DISABLE_FATFS = "disable_fatfs"
|
||||
CONF_ADC_ONESHOT_IN_IRAM = "adc_oneshot_in_iram"
|
||||
|
||||
# VFS requirement tracking
|
||||
# Components that need VFS features can call require_vfs_*() functions
|
||||
@@ -1071,6 +1072,7 @@ KEY_MBEDTLS_PEER_CERT_REQUIRED = "mbedtls_peer_cert_required"
|
||||
KEY_MBEDTLS_PKCS7_REQUIRED = "mbedtls_pkcs7_required"
|
||||
KEY_FATFS_REQUIRED = "fatfs_required"
|
||||
KEY_MBEDTLS_SHA512_REQUIRED = "mbedtls_sha512_required"
|
||||
KEY_ADC_ONESHOT_IRAM_REQUIRED = "adc_oneshot_iram_required"
|
||||
|
||||
|
||||
def require_vfs_select() -> None:
|
||||
@@ -1168,6 +1170,17 @@ def require_fatfs() -> None:
|
||||
CORE.data[KEY_ESP32][KEY_FATFS_REQUIRED] = True
|
||||
|
||||
|
||||
def require_adc_oneshot_iram() -> None:
|
||||
"""Mark that ADC oneshot IRAM safety is required by a component.
|
||||
|
||||
Call this from components that use the ADC oneshot driver. When flash cache is
|
||||
disabled (e.g., during NVS writes by WiFi, BLE, Zigbee, or power management),
|
||||
the ADC oneshot read function must be in IRAM to avoid crashes.
|
||||
This sets CONFIG_ADC_ONESHOT_CTRL_FUNC_IN_IRAM.
|
||||
"""
|
||||
CORE.data[KEY_ESP32][KEY_ADC_ONESHOT_IRAM_REQUIRED] = True
|
||||
|
||||
|
||||
def _parse_idf_component(value: str) -> ConfigType:
|
||||
"""Parse IDF component shorthand syntax like 'owner/component^version'"""
|
||||
# Match operator followed by version-like string (digit or *)
|
||||
@@ -1209,7 +1222,7 @@ FRAMEWORK_SCHEMA = cv.Schema(
|
||||
cv.Optional(CONF_IGNORE_EFUSE_CUSTOM_MAC, default=False): cv.boolean,
|
||||
cv.Optional(CONF_IGNORE_EFUSE_MAC_CRC, default=False): cv.boolean,
|
||||
cv.Optional(CONF_MINIMUM_CHIP_REVISION): cv.one_of(
|
||||
*ESP32_CHIP_REVISIONS
|
||||
*ESP32_CHIP_REVISIONS, string=True
|
||||
),
|
||||
cv.Optional(CONF_SRAM1_AS_IRAM, default=False): cv.boolean,
|
||||
# DHCP server is needed for WiFi AP mode. When WiFi component is used,
|
||||
@@ -1268,6 +1281,7 @@ FRAMEWORK_SCHEMA = cv.Schema(
|
||||
cv.Optional(CONF_DISABLE_MBEDTLS_PEER_CERT, default=True): cv.boolean,
|
||||
cv.Optional(CONF_DISABLE_MBEDTLS_PKCS7, default=True): cv.boolean,
|
||||
cv.Optional(CONF_DISABLE_REGI2C_IN_IRAM, default=True): cv.boolean,
|
||||
cv.Optional(CONF_ADC_ONESHOT_IN_IRAM, default=False): cv.boolean,
|
||||
cv.Optional(CONF_DISABLE_FATFS, default=True): cv.boolean,
|
||||
}
|
||||
),
|
||||
@@ -2068,6 +2082,16 @@ async def to_code(config):
|
||||
if advanced[CONF_DISABLE_REGI2C_IN_IRAM]:
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_REGI2C_CTRL_FUNC_IN_IRAM", False)
|
||||
|
||||
# Place ADC oneshot control functions in IRAM for cache safety
|
||||
# When flash cache is disabled (during NVS writes by WiFi, BLE, Zigbee, Thread,
|
||||
# power management, etc.), ADC reads will crash if these functions are in flash.
|
||||
# Components using ADC call require_adc_oneshot_iram() to force this.
|
||||
if (
|
||||
CORE.data[KEY_ESP32].get(KEY_ADC_ONESHOT_IRAM_REQUIRED, False)
|
||||
or advanced[CONF_ADC_ONESHOT_IN_IRAM]
|
||||
):
|
||||
add_idf_sdkconfig_option("CONFIG_ADC_ONESHOT_CTRL_FUNC_IN_IRAM", True)
|
||||
|
||||
# Disable FATFS support
|
||||
# Components that need FATFS (SD card, etc.) can call require_fatfs()
|
||||
if CORE.data[KEY_ESP32].get(KEY_FATFS_REQUIRED, False):
|
||||
|
||||
@@ -1960,6 +1960,10 @@ BOARDS = {
|
||||
"name": "Hornbill ESP32 Minima",
|
||||
"variant": VARIANT_ESP32,
|
||||
},
|
||||
"huidu_hd_wf1": {
|
||||
"name": "Huidu HD-WF1",
|
||||
"variant": VARIANT_ESP32S2,
|
||||
},
|
||||
"huidu_hd_wf2": {
|
||||
"name": "Huidu HD-WF2",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
@@ -2028,6 +2032,10 @@ BOARDS = {
|
||||
"name": "LilyGo T-Display-S3",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"lilygo-t-energy-s3": {
|
||||
"name": "LilyGo T-Energy-S3",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"lilygo-t3-s3": {
|
||||
"name": "LilyGo T3-S3",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
@@ -2289,10 +2297,18 @@ BOARDS = {
|
||||
"name": "S.ODI Ultra v1",
|
||||
"variant": VARIANT_ESP32,
|
||||
},
|
||||
"seeed_xiao_esp32_s3_plus": {
|
||||
"name": "Seeed Studio XIAO ESP32S3 Plus",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"seeed_xiao_esp32c3": {
|
||||
"name": "Seeed Studio XIAO ESP32C3",
|
||||
"variant": VARIANT_ESP32C3,
|
||||
},
|
||||
"seeed_xiao_esp32c5": {
|
||||
"name": "Seeed Studio XIAO ESP32C5",
|
||||
"variant": VARIANT_ESP32C5,
|
||||
},
|
||||
"seeed_xiao_esp32c6": {
|
||||
"name": "Seeed Studio XIAO ESP32C6",
|
||||
"variant": VARIANT_ESP32C6,
|
||||
|
||||
@@ -61,6 +61,9 @@ uint32_t arch_get_cpu_freq_hz() {
|
||||
}
|
||||
|
||||
TaskHandle_t loop_task_handle = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
static StaticTask_t loop_task_tcb; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
static StackType_t
|
||||
loop_task_stack[ESPHOME_LOOP_TASK_STACK_SIZE]; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
void loop_task(void *pv_params) {
|
||||
setup();
|
||||
@@ -73,9 +76,11 @@ extern "C" void app_main() {
|
||||
initArduino();
|
||||
esp32::setup_preferences();
|
||||
#if CONFIG_FREERTOS_UNICORE
|
||||
xTaskCreate(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, &loop_task_handle);
|
||||
loop_task_handle = xTaskCreateStatic(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, loop_task_stack,
|
||||
&loop_task_tcb);
|
||||
#else
|
||||
xTaskCreatePinnedToCore(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, &loop_task_handle, 1);
|
||||
loop_task_handle = xTaskCreateStaticPinnedToCore(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1,
|
||||
loop_task_stack, &loop_task_tcb, 1);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <nvs_flash.h>
|
||||
#include <cinttypes>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
@@ -12,9 +11,6 @@ namespace esphome::esp32 {
|
||||
|
||||
static const char *const TAG = "preferences";
|
||||
|
||||
// Buffer size for converting uint32_t to string: max "4294967295" (10 chars) + null terminator + 1 padding
|
||||
static constexpr size_t KEY_BUFFER_SIZE = 12;
|
||||
|
||||
struct NVSData {
|
||||
uint32_t key;
|
||||
SmallInlineBuffer<8> data; // Most prefs fit in 8 bytes (covers fan, cover, select, etc.)
|
||||
@@ -51,8 +47,8 @@ bool ESP32PreferenceBackend::load(uint8_t *data, size_t len) {
|
||||
}
|
||||
}
|
||||
|
||||
char key_str[KEY_BUFFER_SIZE];
|
||||
snprintf(key_str, sizeof(key_str), "%" PRIu32, this->key);
|
||||
char key_str[UINT32_MAX_STR_SIZE];
|
||||
uint32_to_str(key_str, this->key);
|
||||
size_t actual_len;
|
||||
esp_err_t err = nvs_get_blob(this->nvs_handle, key_str, nullptr, &actual_len);
|
||||
if (err != 0) {
|
||||
@@ -108,8 +104,8 @@ bool ESP32Preferences::sync() {
|
||||
uint32_t last_key = 0;
|
||||
|
||||
for (const auto &save : s_pending_save) {
|
||||
char key_str[KEY_BUFFER_SIZE];
|
||||
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
|
||||
char key_str[UINT32_MAX_STR_SIZE];
|
||||
uint32_to_str(key_str, save.key);
|
||||
ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str);
|
||||
if (this->is_changed_(this->nvs_handle, save, key_str)) {
|
||||
esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.data(), save.data.size());
|
||||
|
||||
@@ -78,6 +78,14 @@ def ota_esphome_final_validate(config):
|
||||
else:
|
||||
new_ota_conf.append(ota_conf)
|
||||
|
||||
if len(merged_ota_esphome_configs_by_port) > 1:
|
||||
raise cv.Invalid(
|
||||
f"Only a single port is supported for '{CONF_OTA}' "
|
||||
f"'{CONF_PLATFORM}: {CONF_ESPHOME}'. Got ports "
|
||||
f"{sorted(merged_ota_esphome_configs_by_port.keys())}. Consolidate "
|
||||
f"onto a single port; configs sharing a port are merged automatically."
|
||||
)
|
||||
|
||||
new_ota_conf.extend(merged_ota_esphome_configs_by_port.values())
|
||||
|
||||
full_conf[CONF_OTA] = new_ota_conf
|
||||
@@ -147,6 +155,8 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add(var.set_auth_password(config[CONF_PASSWORD]))
|
||||
cg.add_define("USE_OTA_PASSWORD")
|
||||
cg.add_define("USE_OTA_VERSION", config[CONF_VERSION])
|
||||
# Build flag so lwip_fast_select.c (a .c file that can't include defines.h) sees it.
|
||||
cg.add_build_flag("-DUSE_OTA_PLATFORM_ESPHOME")
|
||||
|
||||
await cg.register_component(var, config)
|
||||
await ota_to_code(var, config)
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/util.h"
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
#include "esphome/core/lwip_fast_select.h"
|
||||
#endif
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
@@ -28,6 +31,17 @@ static constexpr size_t OTA_BUFFER_SIZE = 1024; // buffer size
|
||||
static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 20000; // milliseconds for initial handshake
|
||||
static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 90000; // milliseconds for data transfer
|
||||
|
||||
// Single-instance pointer — multi-port configs are rejected in final_validate.
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
static ESPHomeOTAComponent *global_esphome_ota_component = nullptr;
|
||||
|
||||
// Called from any context (LwIP TCP/IP task, RP2040 user-IRQ).
|
||||
extern "C" void esphome_wake_ota_component_any_context() {
|
||||
if (global_esphome_ota_component != nullptr) {
|
||||
global_esphome_ota_component->enable_loop_soon_any_context();
|
||||
}
|
||||
}
|
||||
|
||||
void ESPHomeOTAComponent::setup() {
|
||||
this->server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0).release(); // monitored for incoming connections
|
||||
if (this->server_ == nullptr) {
|
||||
@@ -65,6 +79,14 @@ void ESPHomeOTAComponent::setup() {
|
||||
this->server_failed_(LOG_STR("listen"));
|
||||
return;
|
||||
}
|
||||
|
||||
// loop() self-disables on its first idle tick; no explicit disable_loop() needed here.
|
||||
global_esphome_ota_component = this;
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
// Filter fast-select wakes to this listener only. If the sock lookup returns nullptr,
|
||||
// no wakes fire and loop() falls back to the self-disable safety net.
|
||||
esphome_fast_select_set_ota_listener_sock(esphome_lwip_get_sock(this->server_->get_fd()));
|
||||
#endif
|
||||
}
|
||||
|
||||
void ESPHomeOTAComponent::dump_config() {
|
||||
@@ -81,13 +103,15 @@ void ESPHomeOTAComponent::dump_config() {
|
||||
}
|
||||
|
||||
void ESPHomeOTAComponent::loop() {
|
||||
// Skip handle_handshake_() call if no client connected and no incoming connections
|
||||
// This optimization reduces idle loop overhead when OTA is not active
|
||||
// Note: No need to check server_ for null as the component is marked failed in setup()
|
||||
// if server_ creation fails
|
||||
if (this->client_ != nullptr || this->server_->ready()) {
|
||||
this->handle_handshake_();
|
||||
// Self-disabling idle loop. Runs when a wake path marks us pending-enable (fast-select
|
||||
// listener filter, raw-TCP accept_fn_, or host select), finds no work, and goes back
|
||||
// to sleep. cleanup_connection_() deliberately leaves the loop enabled for one more
|
||||
// iteration so a connection queued mid-session is still caught here.
|
||||
if (this->client_ == nullptr && !this->server_->ready()) {
|
||||
this->disable_loop();
|
||||
return;
|
||||
}
|
||||
this->handle_handshake_();
|
||||
}
|
||||
|
||||
static const uint8_t FEATURE_SUPPORTS_COMPRESSION = 0x01;
|
||||
@@ -566,6 +590,9 @@ void ESPHomeOTAComponent::cleanup_connection_() {
|
||||
#ifdef USE_OTA_PASSWORD
|
||||
this->cleanup_auth_();
|
||||
#endif
|
||||
// Intentionally no disable_loop() — letting loop() run one more iteration catches
|
||||
// any connection that queued on the listener mid-session (otherwise the wake flag,
|
||||
// set while we were in LOOP state, would be lost to enable_pending_loops_()).
|
||||
}
|
||||
|
||||
void ESPHomeOTAComponent::yield_and_feed_watchdog_() {
|
||||
|
||||
@@ -221,7 +221,7 @@ class EthernetComponent final : public Component {
|
||||
int reset_pin_{-1};
|
||||
int phy_addr_spi_{-1};
|
||||
int clock_speed_;
|
||||
spi_host_device_t interface_{SPI3_HOST};
|
||||
spi_host_device_t interface_{SPI2_HOST};
|
||||
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
|
||||
uint32_t polling_interval_{0};
|
||||
#endif
|
||||
|
||||
@@ -325,7 +325,7 @@ def download_gfont(value):
|
||||
raise cv.Invalid(
|
||||
f"Could not download font at {url}, please check the fonts exists "
|
||||
f"at google fonts ({e})"
|
||||
)
|
||||
) from e
|
||||
match = re.search(r"src:\s+url\((.+)\)\s+format\('truetype'\);", req.text)
|
||||
if match is None:
|
||||
raise cv.Invalid(
|
||||
|
||||
@@ -108,8 +108,13 @@ async def globals_set_to_code(config, action_id, template_arg, args):
|
||||
full_id, paren = await cg.get_variable_with_full_id(config[CONF_ID])
|
||||
template_arg = cg.TemplateArguments(full_id.type, *template_arg)
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
# Use the global's value_type alias as the lambda return type so
|
||||
# TemplatableFn stores a direct function pointer instead of going through
|
||||
# the deprecated converting trampoline when the value expression deduces
|
||||
# to a different type (e.g. int literal assigned to a float global).
|
||||
value_type = cg.RawExpression(f"{full_id.type}::value_type")
|
||||
templ = await cg.templatable(
|
||||
config[CONF_VALUE], args, None, to_exp=cg.RawExpression, wrap_constant=True
|
||||
config[CONF_VALUE], args, value_type, to_exp=cg.RawExpression
|
||||
)
|
||||
cg.add(var.set_value(templ))
|
||||
return var
|
||||
|
||||
@@ -60,20 +60,35 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = await binary_sensor.new_binary_sensor(config)
|
||||
await cg.register_component(var, config)
|
||||
def _pin_shared_only_with_deep_sleep(pin_num: int) -> bool:
|
||||
"""Check if pin is shared exclusively with deep_sleep (wakeup pin)."""
|
||||
pin_key = (CORE.target_platform, CORE.target_platform, pin_num)
|
||||
pin_users = pins.PIN_SCHEMA_REGISTRY.pins_used.get(pin_key, [])
|
||||
if len(pin_users) != 2:
|
||||
return False
|
||||
return any(path and path[0] == "deep_sleep" for path, _, _ in pin_users)
|
||||
|
||||
pin = await cg.gpio_pin_expression(config[CONF_PIN])
|
||||
cg.add(var.set_pin(pin))
|
||||
|
||||
# Check for ESP8266 GPIO16 interrupt limitation
|
||||
# GPIO16 on ESP8266 is a special pin that doesn't support interrupts through
|
||||
# the Arduino attachInterrupt() function. This is the only known GPIO pin
|
||||
# across all supported platforms that has this limitation, so we handle it
|
||||
# here instead of in the platform-specific code.
|
||||
def _final_validate(config):
|
||||
use_interrupt = config[CONF_USE_INTERRUPT]
|
||||
if use_interrupt and CORE.is_esp8266 and config[CONF_PIN][CONF_NUMBER] == 16:
|
||||
if not use_interrupt:
|
||||
return config
|
||||
|
||||
pin_num = config[CONF_PIN][CONF_NUMBER]
|
||||
|
||||
# Expander pins (e.g. PCF8574, MCP23017) don't support direct interrupt
|
||||
# attachment — only internal/native GPIO pins do.
|
||||
if pins.PIN_SCHEMA_REGISTRY.get_key(config[CONF_PIN]) != CORE.target_platform:
|
||||
_LOGGER.info(
|
||||
"GPIO binary_sensor '%s': Pin is not an internal GPIO, "
|
||||
"falling back to polling mode.",
|
||||
config.get(CONF_NAME, config[CONF_ID]),
|
||||
)
|
||||
config[CONF_USE_INTERRUPT] = False
|
||||
return config
|
||||
|
||||
# GPIO16 on ESP8266 doesn't support interrupts through attachInterrupt().
|
||||
if CORE.is_esp8266 and pin_num == 16:
|
||||
_LOGGER.warning(
|
||||
"GPIO binary_sensor '%s': GPIO16 on ESP8266 doesn't support interrupts. "
|
||||
"Falling back to polling mode (same as in ESPHome <2025.7). "
|
||||
@@ -81,22 +96,45 @@ async def to_code(config):
|
||||
"performance with interrupts.",
|
||||
config.get(CONF_NAME, config[CONF_ID]),
|
||||
)
|
||||
use_interrupt = False
|
||||
config[CONF_USE_INTERRUPT] = False
|
||||
return config
|
||||
|
||||
# Check if pin is shared with other components (allow_other_uses)
|
||||
# When a pin is shared, interrupts can interfere with other components
|
||||
# (e.g., duty_cycle sensor) that need to monitor the pin's state changes
|
||||
if use_interrupt and config[CONF_PIN].get(CONF_ALLOW_OTHER_USES, False):
|
||||
_LOGGER.info(
|
||||
"GPIO binary_sensor '%s': Disabling interrupts because pin %s is shared with other components. "
|
||||
"The sensor will use polling mode for compatibility with other pin uses.",
|
||||
config.get(CONF_NAME, config[CONF_ID]),
|
||||
config[CONF_PIN][CONF_NUMBER],
|
||||
)
|
||||
use_interrupt = False
|
||||
# (e.g., duty_cycle sensor) that need to monitor the pin's state changes.
|
||||
# Exception: deep_sleep wakeup pins are compatible with interrupts when
|
||||
# the pin is only shared between this sensor and deep_sleep (count == 2).
|
||||
if config[CONF_PIN].get(CONF_ALLOW_OTHER_USES, False):
|
||||
if not _pin_shared_only_with_deep_sleep(pin_num):
|
||||
_LOGGER.info(
|
||||
"GPIO binary_sensor '%s': Disabling interrupts because pin %s is shared "
|
||||
"with other components. The sensor will use polling mode for "
|
||||
"compatibility with other pin uses.",
|
||||
config.get(CONF_NAME, config[CONF_ID]),
|
||||
pin_num,
|
||||
)
|
||||
config[CONF_USE_INTERRUPT] = False
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"GPIO binary_sensor '%s': Pin %s is shared with deep_sleep, "
|
||||
"keeping interrupts enabled.",
|
||||
config.get(CONF_NAME, config[CONF_ID]),
|
||||
pin_num,
|
||||
)
|
||||
|
||||
if use_interrupt:
|
||||
return config
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = await binary_sensor.new_binary_sensor(config)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
pin = await cg.gpio_pin_expression(config[CONF_PIN])
|
||||
cg.add(var.set_pin(pin))
|
||||
|
||||
if config[CONF_USE_INTERRUPT]:
|
||||
cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE]))
|
||||
else:
|
||||
# Only generate call when disabling interrupts (default is true)
|
||||
cg.add(var.set_use_interrupt(use_interrupt))
|
||||
cg.add(var.set_use_interrupt(False))
|
||||
|
||||
@@ -46,11 +46,6 @@ void GPIOBinarySensorStore::setup(InternalGPIOPin *pin, Component *component) {
|
||||
}
|
||||
|
||||
void GPIOBinarySensor::setup() {
|
||||
if (this->store_.use_interrupt_ && !this->pin_->is_internal()) {
|
||||
ESP_LOGD(TAG, "GPIO is not internal, falling back to polling mode");
|
||||
this->store_.use_interrupt_ = false;
|
||||
}
|
||||
|
||||
if (this->store_.use_interrupt_) {
|
||||
auto *internal_pin = static_cast<InternalGPIOPin *>(this->pin_);
|
||||
this->store_.setup(internal_pin, this);
|
||||
|
||||
@@ -127,6 +127,6 @@ async def to_code(config):
|
||||
cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE]))
|
||||
cg.add_build_flag("-Wno-error=overloaded-virtual")
|
||||
|
||||
cg.add_library("tonia/HeatpumpIR", "1.0.40")
|
||||
cg.add_library("tonia/HeatpumpIR", "1.0.41")
|
||||
if CORE.is_libretiny or CORE.is_esp32:
|
||||
CORE.add_platformio_option("lib_ignore", ["IRremoteESP8266"])
|
||||
|
||||
@@ -36,7 +36,7 @@ I2SAudioMicrophone = i2s_audio_ns.class_(
|
||||
)
|
||||
|
||||
INTERNAL_ADC_VARIANTS = [esp32.VARIANT_ESP32]
|
||||
PDM_VARIANTS = [esp32.VARIANT_ESP32, esp32.VARIANT_ESP32S3]
|
||||
PDM_VARIANTS = [esp32.VARIANT_ESP32, esp32.VARIANT_ESP32S3, esp32.VARIANT_ESP32P4]
|
||||
|
||||
|
||||
def _validate_esp32_variant(config):
|
||||
|
||||
@@ -283,7 +283,7 @@ async def to_code(config):
|
||||
try:
|
||||
return Image.open(path)
|
||||
except Exception as e:
|
||||
raise core.EsphomeError(f"Could not load image file {path}: {e}")
|
||||
raise core.EsphomeError(f"Could not load image file {path}: {e}") from e
|
||||
|
||||
# make a wide horizontal combined image.
|
||||
images = [load_image(x) for x in config[CONF_COLOR_PALETTE_IMAGES]]
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome {
|
||||
namespace ili9xxx {
|
||||
|
||||
|
||||
@@ -229,6 +229,10 @@ void ILI9XXXDisplay::update() {
|
||||
}
|
||||
|
||||
void ILI9XXXDisplay::display_() {
|
||||
// buffer may be null if allocation failed
|
||||
if (this->buffer_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
// check if something was displayed
|
||||
if ((this->x_high_ < this->x_low_) || (this->y_high_ < this->y_low_)) {
|
||||
return;
|
||||
|
||||
@@ -28,7 +28,6 @@ from esphome.const import (
|
||||
CONF_URL,
|
||||
)
|
||||
from esphome.core import CORE, HexInt
|
||||
from esphome.final_validate import full_config
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -676,12 +675,16 @@ def _final_validate(config):
|
||||
:param config:
|
||||
:return:
|
||||
"""
|
||||
fv = full_config.get()
|
||||
if "lvgl" in fv and not all(CONF_BYTE_ORDER in x for x in config):
|
||||
config = config.copy()
|
||||
for c in config:
|
||||
if not c.get(CONF_BYTE_ORDER):
|
||||
c[CONF_BYTE_ORDER] = "LITTLE_ENDIAN"
|
||||
config = config.copy()
|
||||
for c in config:
|
||||
if byte_order := c.get(CONF_BYTE_ORDER):
|
||||
if byte_order == "BIG_ENDIAN":
|
||||
_LOGGER.warning(
|
||||
"The image '%s' is configured with big-endian byte order, little-endian is expected",
|
||||
c.get(CONF_FILE),
|
||||
)
|
||||
else:
|
||||
c[CONF_BYTE_ORDER] = "LITTLE_ENDIAN"
|
||||
return config
|
||||
|
||||
|
||||
|
||||
@@ -189,7 +189,7 @@ Color Image::get_rgb_pixel_(int x, int y) const {
|
||||
}
|
||||
Color Image::get_rgb565_pixel_(int x, int y) const {
|
||||
const uint8_t *pos = this->data_start_ + (x + y * this->width_) * this->bpp_ / 8;
|
||||
uint16_t rgb565 = encode_uint16(progmem_read_byte(pos), progmem_read_byte(pos + 1));
|
||||
uint16_t rgb565 = encode_uint16(progmem_read_byte(pos + 1), progmem_read_byte(pos));
|
||||
auto r = (rgb565 & 0xF800) >> 11;
|
||||
auto g = (rgb565 & 0x07E0) >> 5;
|
||||
auto b = rgb565 & 0x001F;
|
||||
|
||||
@@ -360,8 +360,8 @@ void LD2410Component::handle_periodic_data_() {
|
||||
*/
|
||||
#ifdef USE_SENSOR
|
||||
SAFE_PUBLISH_SENSOR(this->moving_target_distance_sensor_,
|
||||
encode_uint16(this->buffer_data_[MOVING_TARGET_HIGH], this->buffer_data_[MOVING_TARGET_LOW]))
|
||||
SAFE_PUBLISH_SENSOR(this->moving_target_energy_sensor_, this->buffer_data_[MOVING_ENERGY])
|
||||
encode_uint16(this->buffer_data_[MOVING_TARGET_HIGH], this->buffer_data_[MOVING_TARGET_LOW]));
|
||||
SAFE_PUBLISH_SENSOR(this->moving_target_energy_sensor_, this->buffer_data_[MOVING_ENERGY]);
|
||||
SAFE_PUBLISH_SENSOR(this->still_target_distance_sensor_,
|
||||
encode_uint16(this->buffer_data_[STILL_TARGET_HIGH], this->buffer_data_[STILL_TARGET_LOW]));
|
||||
SAFE_PUBLISH_SENSOR(this->still_target_energy_sensor_, this->buffer_data_[STILL_ENERGY]);
|
||||
@@ -375,26 +375,26 @@ void LD2410Component::handle_periodic_data_() {
|
||||
Moving energy: 20~28th bytes
|
||||
*/
|
||||
for (uint8_t i = 0; i < TOTAL_GATES; i++) {
|
||||
SAFE_PUBLISH_SENSOR(this->gate_move_sensors_[i], this->buffer_data_[MOVING_SENSOR_START + i])
|
||||
SAFE_PUBLISH_SENSOR(this->gate_move_sensors_[i], this->buffer_data_[MOVING_SENSOR_START + i]);
|
||||
}
|
||||
/*
|
||||
Still energy: 29~37th bytes
|
||||
*/
|
||||
for (uint8_t i = 0; i < TOTAL_GATES; i++) {
|
||||
SAFE_PUBLISH_SENSOR(this->gate_still_sensors_[i], this->buffer_data_[STILL_SENSOR_START + i])
|
||||
SAFE_PUBLISH_SENSOR(this->gate_still_sensors_[i], this->buffer_data_[STILL_SENSOR_START + i]);
|
||||
}
|
||||
/*
|
||||
Light sensor: 38th bytes
|
||||
*/
|
||||
SAFE_PUBLISH_SENSOR(this->light_sensor_, this->buffer_data_[LIGHT_SENSOR])
|
||||
SAFE_PUBLISH_SENSOR(this->light_sensor_, this->buffer_data_[LIGHT_SENSOR]);
|
||||
} else {
|
||||
for (auto &gate_move_sensor : this->gate_move_sensors_) {
|
||||
SAFE_PUBLISH_SENSOR_UNKNOWN(gate_move_sensor)
|
||||
SAFE_PUBLISH_SENSOR_UNKNOWN(gate_move_sensor);
|
||||
}
|
||||
for (auto &gate_still_sensor : this->gate_still_sensors_) {
|
||||
SAFE_PUBLISH_SENSOR_UNKNOWN(gate_still_sensor)
|
||||
SAFE_PUBLISH_SENSOR_UNKNOWN(gate_still_sensor);
|
||||
}
|
||||
SAFE_PUBLISH_SENSOR_UNKNOWN(this->light_sensor_)
|
||||
SAFE_PUBLISH_SENSOR_UNKNOWN(this->light_sensor_);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
@@ -786,13 +786,12 @@ void LD2410Component::set_light_out_control() {
|
||||
}
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
// These could leak memory, but they are only set once prior to 'setup()' and should never be used again.
|
||||
void LD2410Component::set_gate_move_sensor(uint8_t gate, sensor::Sensor *s) {
|
||||
this->gate_move_sensors_[gate] = new SensorWithDedup<uint8_t>(s);
|
||||
this->gate_move_sensors_[gate].set_sensor(s);
|
||||
}
|
||||
|
||||
void LD2410Component::set_gate_still_sensor(uint8_t gate, sensor::Sensor *s) {
|
||||
this->gate_still_sensors_[gate] = new SensorWithDedup<uint8_t>(s);
|
||||
this->gate_still_sensors_[gate].set_sensor(s);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -129,8 +129,8 @@ class LD2410Component : public Component, public uart::UARTDevice {
|
||||
std::array<number::Number *, TOTAL_GATES> gate_still_threshold_numbers_{};
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
std::array<SensorWithDedup<uint8_t> *, TOTAL_GATES> gate_move_sensors_{};
|
||||
std::array<SensorWithDedup<uint8_t> *, TOTAL_GATES> gate_still_sensors_{};
|
||||
std::array<SensorWithDedup<uint8_t>, TOTAL_GATES> gate_move_sensors_{};
|
||||
std::array<SensorWithDedup<uint8_t>, TOTAL_GATES> gate_still_sensors_{};
|
||||
#endif
|
||||
};
|
||||
|
||||
|
||||
@@ -397,12 +397,12 @@ void LD2412Component::handle_periodic_data_() {
|
||||
*/
|
||||
#ifdef USE_SENSOR
|
||||
SAFE_PUBLISH_SENSOR(this->moving_target_distance_sensor_,
|
||||
encode_uint16(this->buffer_data_[MOVING_TARGET_HIGH], this->buffer_data_[MOVING_TARGET_LOW]))
|
||||
SAFE_PUBLISH_SENSOR(this->moving_target_energy_sensor_, this->buffer_data_[MOVING_ENERGY])
|
||||
encode_uint16(this->buffer_data_[MOVING_TARGET_HIGH], this->buffer_data_[MOVING_TARGET_LOW]));
|
||||
SAFE_PUBLISH_SENSOR(this->moving_target_energy_sensor_, this->buffer_data_[MOVING_ENERGY]);
|
||||
SAFE_PUBLISH_SENSOR(this->still_target_distance_sensor_,
|
||||
encode_uint16(this->buffer_data_[STILL_TARGET_HIGH], this->buffer_data_[STILL_TARGET_LOW]))
|
||||
SAFE_PUBLISH_SENSOR(this->still_target_energy_sensor_, this->buffer_data_[STILL_ENERGY])
|
||||
if (this->detection_distance_sensor_ != nullptr) {
|
||||
encode_uint16(this->buffer_data_[STILL_TARGET_HIGH], this->buffer_data_[STILL_TARGET_LOW]));
|
||||
SAFE_PUBLISH_SENSOR(this->still_target_energy_sensor_, this->buffer_data_[STILL_ENERGY]);
|
||||
if (this->detection_distance_sensor_.has_sensor()) {
|
||||
int new_detect_distance = 0;
|
||||
if (target_state != 0x00 && (target_state & MOVE_BITMASK)) {
|
||||
new_detect_distance =
|
||||
@@ -410,7 +410,7 @@ void LD2412Component::handle_periodic_data_() {
|
||||
} else if (target_state != 0x00) {
|
||||
new_detect_distance = encode_uint16(this->buffer_data_[STILL_TARGET_HIGH], this->buffer_data_[STILL_TARGET_LOW]);
|
||||
}
|
||||
this->detection_distance_sensor_->publish_state_if_not_dup(new_detect_distance);
|
||||
this->detection_distance_sensor_.publish_state_if_not_dup(new_detect_distance);
|
||||
}
|
||||
if (engineering_mode) {
|
||||
// Engineering mode needs at least LIGHT_SENSOR + 1 bytes
|
||||
@@ -423,27 +423,27 @@ void LD2412Component::handle_periodic_data_() {
|
||||
Moving energy: 20~28th bytes
|
||||
*/
|
||||
for (uint8_t i = 0; i < TOTAL_GATES; i++) {
|
||||
SAFE_PUBLISH_SENSOR(this->gate_move_sensors_[i], this->buffer_data_[MOVING_SENSOR_START + i])
|
||||
SAFE_PUBLISH_SENSOR(this->gate_move_sensors_[i], this->buffer_data_[MOVING_SENSOR_START + i]);
|
||||
}
|
||||
/*
|
||||
Still energy: 29~37th bytes
|
||||
*/
|
||||
for (uint8_t i = 0; i < TOTAL_GATES; i++) {
|
||||
SAFE_PUBLISH_SENSOR(this->gate_still_sensors_[i], this->buffer_data_[STILL_SENSOR_START + i])
|
||||
SAFE_PUBLISH_SENSOR(this->gate_still_sensors_[i], this->buffer_data_[STILL_SENSOR_START + i]);
|
||||
}
|
||||
/*
|
||||
Light sensor value
|
||||
*/
|
||||
SAFE_PUBLISH_SENSOR(this->light_sensor_, this->buffer_data_[LIGHT_SENSOR])
|
||||
SAFE_PUBLISH_SENSOR(this->light_sensor_, this->buffer_data_[LIGHT_SENSOR]);
|
||||
}
|
||||
} else {
|
||||
for (auto &gate_move_sensor : this->gate_move_sensors_) {
|
||||
SAFE_PUBLISH_SENSOR_UNKNOWN(gate_move_sensor)
|
||||
SAFE_PUBLISH_SENSOR_UNKNOWN(gate_move_sensor);
|
||||
}
|
||||
for (auto &gate_still_sensor : this->gate_still_sensors_) {
|
||||
SAFE_PUBLISH_SENSOR_UNKNOWN(gate_still_sensor)
|
||||
SAFE_PUBLISH_SENSOR_UNKNOWN(gate_still_sensor);
|
||||
}
|
||||
SAFE_PUBLISH_SENSOR_UNKNOWN(this->light_sensor_)
|
||||
SAFE_PUBLISH_SENSOR_UNKNOWN(this->light_sensor_);
|
||||
}
|
||||
#endif
|
||||
// the radar module won't tell us when it's done, so we just have to keep polling...
|
||||
@@ -846,12 +846,11 @@ void LD2412Component::set_light_out_control() {
|
||||
}
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
// These could leak memory, but they are only set once prior to 'setup()' and should never be used again.
|
||||
void LD2412Component::set_gate_move_sensor(uint8_t gate, sensor::Sensor *s) {
|
||||
this->gate_move_sensors_[gate] = new SensorWithDedup<uint8_t>(s);
|
||||
this->gate_move_sensors_[gate].set_sensor(s);
|
||||
}
|
||||
void LD2412Component::set_gate_still_sensor(uint8_t gate, sensor::Sensor *s) {
|
||||
this->gate_still_sensors_[gate] = new SensorWithDedup<uint8_t>(s);
|
||||
this->gate_still_sensors_[gate].set_sensor(s);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -133,8 +133,8 @@ class LD2412Component : public Component, public uart::UARTDevice {
|
||||
std::array<number::Number *, TOTAL_GATES> gate_still_threshold_numbers_{};
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
std::array<SensorWithDedup<uint8_t> *, TOTAL_GATES> gate_move_sensors_{};
|
||||
std::array<SensorWithDedup<uint8_t> *, TOTAL_GATES> gate_still_sensors_{};
|
||||
std::array<SensorWithDedup<uint8_t>, TOTAL_GATES> gate_move_sensors_{};
|
||||
std::array<SensorWithDedup<uint8_t>, TOTAL_GATES> gate_still_sensors_{};
|
||||
#endif
|
||||
};
|
||||
|
||||
|
||||
@@ -565,6 +565,7 @@ void LD2450Component::handle_periodic_data_() {
|
||||
SAFE_PUBLISH_SENSOR(this->still_target_count_sensor_, still_target_count);
|
||||
// Moving Target Count
|
||||
SAFE_PUBLISH_SENSOR(this->moving_target_count_sensor_, moving_target_count);
|
||||
|
||||
#endif
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
@@ -872,33 +873,32 @@ void LD2450Component::query_target_tracking_mode_() { this->send_command_(CMD_QU
|
||||
void LD2450Component::query_zone_() { this->send_command_(CMD_QUERY_ZONE, nullptr, 0); }
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
// These could leak memory, but they are only set once prior to 'setup()' and should never be used again.
|
||||
void LD2450Component::set_move_x_sensor(uint8_t target, sensor::Sensor *s) {
|
||||
this->move_x_sensors_[target] = new SensorWithDedup<int16_t>(s);
|
||||
this->move_x_sensors_[target].set_sensor(s);
|
||||
}
|
||||
void LD2450Component::set_move_y_sensor(uint8_t target, sensor::Sensor *s) {
|
||||
this->move_y_sensors_[target] = new SensorWithDedup<int16_t>(s);
|
||||
this->move_y_sensors_[target].set_sensor(s);
|
||||
}
|
||||
void LD2450Component::set_move_speed_sensor(uint8_t target, sensor::Sensor *s) {
|
||||
this->move_speed_sensors_[target] = new SensorWithDedup<int16_t>(s);
|
||||
this->move_speed_sensors_[target].set_sensor(s);
|
||||
}
|
||||
void LD2450Component::set_move_angle_sensor(uint8_t target, sensor::Sensor *s) {
|
||||
this->move_angle_sensors_[target] = new SensorWithDedup<float>(s);
|
||||
this->move_angle_sensors_[target].set_sensor(s);
|
||||
}
|
||||
void LD2450Component::set_move_distance_sensor(uint8_t target, sensor::Sensor *s) {
|
||||
this->move_distance_sensors_[target] = new SensorWithDedup<uint16_t>(s);
|
||||
this->move_distance_sensors_[target].set_sensor(s);
|
||||
}
|
||||
void LD2450Component::set_move_resolution_sensor(uint8_t target, sensor::Sensor *s) {
|
||||
this->move_resolution_sensors_[target] = new SensorWithDedup<uint16_t>(s);
|
||||
this->move_resolution_sensors_[target].set_sensor(s);
|
||||
}
|
||||
void LD2450Component::set_zone_target_count_sensor(uint8_t zone, sensor::Sensor *s) {
|
||||
this->zone_target_count_sensors_[zone] = new SensorWithDedup<uint8_t>(s);
|
||||
this->zone_target_count_sensors_[zone].set_sensor(s);
|
||||
}
|
||||
void LD2450Component::set_zone_still_target_count_sensor(uint8_t zone, sensor::Sensor *s) {
|
||||
this->zone_still_target_count_sensors_[zone] = new SensorWithDedup<uint8_t>(s);
|
||||
this->zone_still_target_count_sensors_[zone].set_sensor(s);
|
||||
}
|
||||
void LD2450Component::set_zone_moving_target_count_sensor(uint8_t zone, sensor::Sensor *s) {
|
||||
this->zone_moving_target_count_sensors_[zone] = new SensorWithDedup<uint8_t>(s);
|
||||
this->zone_moving_target_count_sensors_[zone].set_sensor(s);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
|
||||
@@ -182,15 +182,15 @@ class LD2450Component : public Component, public uart::UARTDevice {
|
||||
ZoneOfNumbers zone_numbers_[MAX_ZONES];
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
std::array<SensorWithDedup<int16_t> *, MAX_TARGETS> move_x_sensors_{};
|
||||
std::array<SensorWithDedup<int16_t> *, MAX_TARGETS> move_y_sensors_{};
|
||||
std::array<SensorWithDedup<int16_t> *, MAX_TARGETS> move_speed_sensors_{};
|
||||
std::array<SensorWithDedup<float> *, MAX_TARGETS> move_angle_sensors_{};
|
||||
std::array<SensorWithDedup<uint16_t> *, MAX_TARGETS> move_distance_sensors_{};
|
||||
std::array<SensorWithDedup<uint16_t> *, MAX_TARGETS> move_resolution_sensors_{};
|
||||
std::array<SensorWithDedup<uint8_t> *, MAX_ZONES> zone_target_count_sensors_{};
|
||||
std::array<SensorWithDedup<uint8_t> *, MAX_ZONES> zone_still_target_count_sensors_{};
|
||||
std::array<SensorWithDedup<uint8_t> *, MAX_ZONES> zone_moving_target_count_sensors_{};
|
||||
std::array<SensorWithDedup<int16_t>, MAX_TARGETS> move_x_sensors_{};
|
||||
std::array<SensorWithDedup<int16_t>, MAX_TARGETS> move_y_sensors_{};
|
||||
std::array<SensorWithDedup<int16_t>, MAX_TARGETS> move_speed_sensors_{};
|
||||
std::array<SensorWithDedup<float>, MAX_TARGETS> move_angle_sensors_{};
|
||||
std::array<SensorWithDedup<uint16_t>, MAX_TARGETS> move_distance_sensors_{};
|
||||
std::array<SensorWithDedup<uint16_t>, MAX_TARGETS> move_resolution_sensors_{};
|
||||
std::array<SensorWithDedup<uint8_t>, MAX_ZONES> zone_target_count_sensors_{};
|
||||
std::array<SensorWithDedup<uint8_t>, MAX_ZONES> zone_still_target_count_sensors_{};
|
||||
std::array<SensorWithDedup<uint8_t>, MAX_ZONES> zone_moving_target_count_sensors_{};
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
std::array<text_sensor::TextSensor *, MAX_TARGETS> direction_text_sensors_{};
|
||||
|
||||
@@ -11,28 +11,20 @@
|
||||
|
||||
#define SUB_SENSOR_WITH_DEDUP(name, dedup_type) \
|
||||
protected: \
|
||||
ld24xx::SensorWithDedup<dedup_type> *name##_sensor_{nullptr}; \
|
||||
ld24xx::SensorWithDedup<dedup_type> name##_sensor_{}; \
|
||||
\
|
||||
public: \
|
||||
void set_##name##_sensor(sensor::Sensor *sensor) { \
|
||||
this->name##_sensor_ = new ld24xx::SensorWithDedup<dedup_type>(sensor); \
|
||||
}
|
||||
void set_##name##_sensor(sensor::Sensor *sensor) { this->name##_sensor_.set_sensor(sensor); }
|
||||
#endif
|
||||
|
||||
#define LOG_SENSOR_WITH_DEDUP_SAFE(tag, name, sensor) \
|
||||
if ((sensor) != nullptr) { \
|
||||
LOG_SENSOR(tag, name, (sensor)->sens); \
|
||||
if ((sensor).has_sensor()) { \
|
||||
LOG_SENSOR(tag, name, (sensor).get_sensor()); \
|
||||
}
|
||||
|
||||
#define SAFE_PUBLISH_SENSOR(sensor, value) \
|
||||
if ((sensor) != nullptr) { \
|
||||
(sensor)->publish_state_if_not_dup(value); \
|
||||
}
|
||||
#define SAFE_PUBLISH_SENSOR(sensor, value) (sensor).publish_state_if_not_dup(value)
|
||||
|
||||
#define SAFE_PUBLISH_SENSOR_UNKNOWN(sensor) \
|
||||
if ((sensor) != nullptr) { \
|
||||
(sensor)->publish_state_unknown(); \
|
||||
}
|
||||
#define SAFE_PUBLISH_SENSOR_UNKNOWN(sensor) (sensor).publish_state_unknown()
|
||||
|
||||
#define highbyte(val) (uint8_t)((val) >> 8)
|
||||
#define lowbyte(val) (uint8_t)((val) &0xff)
|
||||
@@ -70,25 +62,33 @@ inline void format_version_str(const uint8_t *version, std::span<char, 20> buffe
|
||||
}
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
// Helper class to store a sensor with a deduplicator & publish state only when the value changes
|
||||
/// Sensor with deduplication — sensor may be null, null check is internal.
|
||||
/// Stored inline, no heap allocation. Does nothing when no sensor is set.
|
||||
template<typename T> class SensorWithDedup {
|
||||
public:
|
||||
SensorWithDedup(sensor::Sensor *sens) : sens(sens) {}
|
||||
void set_sensor(sensor::Sensor *sens) {
|
||||
this->sens_ = sens;
|
||||
this->dedup_ = {};
|
||||
}
|
||||
|
||||
void publish_state_if_not_dup(T state) {
|
||||
if (this->publish_dedup.next(state)) {
|
||||
this->sens->publish_state(static_cast<float>(state));
|
||||
if (this->sens_ != nullptr && this->dedup_.next(state)) {
|
||||
this->sens_->publish_state(static_cast<float>(state));
|
||||
}
|
||||
}
|
||||
|
||||
void publish_state_unknown() {
|
||||
if (this->publish_dedup.next_unknown()) {
|
||||
this->sens->publish_state(NAN);
|
||||
if (this->sens_ != nullptr && this->dedup_.next_unknown()) {
|
||||
this->sens_->publish_state(NAN);
|
||||
}
|
||||
}
|
||||
|
||||
sensor::Sensor *sens;
|
||||
Deduplicator<T> publish_dedup;
|
||||
bool has_sensor() const { return this->sens_ != nullptr; }
|
||||
sensor::Sensor *get_sensor() const { return this->sens_; }
|
||||
|
||||
protected:
|
||||
sensor::Sensor *sens_{nullptr};
|
||||
Deduplicator<T> dedup_;
|
||||
};
|
||||
#endif
|
||||
} // namespace esphome::ld24xx
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
@@ -24,6 +25,7 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.core.config import BOARD_MAX_LENGTH
|
||||
from esphome.helpers import copy_file_if_changed
|
||||
from esphome.storage_json import StorageJSON
|
||||
|
||||
from . import gpio # noqa
|
||||
@@ -465,6 +467,11 @@ async def component_to_code(config):
|
||||
# it for project source files only. GCC uses the last -O flag.
|
||||
build_src_flags += " -Os"
|
||||
cg.add_platformio_option("build_src_flags", build_src_flags)
|
||||
# IRAM_ATTR is a no-op on BK72xx (SDK masks FIQ+IRQ around flash ops).
|
||||
# On other families, patch_linker.py routes .sram.text into the right
|
||||
# RAM-executable output section and prints a post-link placement summary.
|
||||
if FAMILY_COMPONENT[config[CONF_FAMILY]] != COMPONENT_BK72XX:
|
||||
cg.add_platformio_option("extra_scripts", ["pre:patch_linker.py"])
|
||||
# dummy version code
|
||||
cg.add_define("USE_ARDUINO_VERSION_CODE", cg.RawExpression("VERSION_CODE(0, 0, 0)"))
|
||||
# decrease web server stack size (16k words -> 4k words)
|
||||
@@ -549,3 +556,13 @@ async def component_to_code(config):
|
||||
_configure_lwip(config)
|
||||
|
||||
await cg.register_component(var, config)
|
||||
|
||||
|
||||
# Called by writer.py
|
||||
def copy_files() -> None:
|
||||
script_dir = Path(__file__).parent
|
||||
patch_linker_file = script_dir / "patch_linker.py.script"
|
||||
copy_file_if_changed(
|
||||
patch_linker_file,
|
||||
CORE.relative_build_path("patch_linker.py"),
|
||||
)
|
||||
|
||||
@@ -79,6 +79,11 @@ async def to_code(config):
|
||||
@pins.PIN_SCHEMA_REGISTRY.register("{COMPONENT_LOWER}", PIN_SCHEMA)
|
||||
async def pin_to_code(config):
|
||||
return await libretiny.gpio.component_pin_to_code(config)
|
||||
|
||||
|
||||
# Called by writer.py; delegates to the shared libretiny implementation.
|
||||
def copy_files() -> None:
|
||||
libretiny.copy_files()
|
||||
'''
|
||||
|
||||
BASE_CODE_BOARDS = '''
|
||||
|
||||
171
esphome/components/libretiny/patch_linker.py.script
Normal file
171
esphome/components/libretiny/patch_linker.py.script
Normal file
@@ -0,0 +1,171 @@
|
||||
# pylint: disable=E0602
|
||||
Import("env") # noqa
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
# ESPHome marks ISR code IRAM_ATTR, which on LibreTiny maps to a per-family
|
||||
# section routed into RAM-executable memory (see esphome/core/hal.h).
|
||||
#
|
||||
# This script is NOT loaded on BK72xx (IRAM_ATTR is a no-op there; the SDK
|
||||
# masks FIQ+IRQ around flash writes). On the remaining families:
|
||||
# - RTL8710B: hal.h uses section(".image2.ram.text"); stock linker consumes it.
|
||||
# - RTL8720C: hal.h uses section(".sram.text"); stock linker consumes it.
|
||||
# - LN882H: stock linker has no glob for ".sram.text", so we inject
|
||||
# KEEP(*(.sram.text*)) into ".flash_copysection" (> RAM0 AT> FLASH).
|
||||
#
|
||||
# All families also get a post-link summary showing where IRAM_ATTR landed.
|
||||
|
||||
|
||||
_MARKER = "/* esphome .sram.text */"
|
||||
# Strong assignments (not PROVIDE) so the symbols are always emitted in the
|
||||
# ELF; PROVIDE symbols with no references can be garbage-collected.
|
||||
_KEEP_LINE = (
|
||||
" __esphome_sram_text_start = .; "
|
||||
"KEEP(*(.sram.text*)) "
|
||||
"__esphome_sram_text_end = .; "
|
||||
+ _MARKER + "\n"
|
||||
)
|
||||
_LN_COPY = re.compile(r"(\.flash_copysection\s*:\s*\{\s*\n)")
|
||||
|
||||
|
||||
def _detect(env):
|
||||
prefix = "USE_LIBRETINY_VARIANT_"
|
||||
# CPPDEFINES may hold strings or (name, value) tuples; BUILD_FLAGS holds
|
||||
# the raw "-DNAME" strings. PlatformIO populates both, but the exact order
|
||||
# vs. extra_scripts varies, so check both to be robust.
|
||||
for token in env.get("CPPDEFINES", []):
|
||||
if isinstance(token, (list, tuple)):
|
||||
token = token[0]
|
||||
if isinstance(token, str) and token.startswith(prefix):
|
||||
return token[len(prefix):]
|
||||
for flag in env.get("BUILD_FLAGS", []):
|
||||
if isinstance(flag, str) and "-D" + prefix in flag:
|
||||
name = flag.split("-D", 1)[1].split("=", 1)[0].strip()
|
||||
if name.startswith(prefix):
|
||||
return name[len(prefix):]
|
||||
return None
|
||||
|
||||
|
||||
KNOWN_VARIANTS = frozenset({
|
||||
"LN882H",
|
||||
"RTL8710B",
|
||||
"RTL8720C",
|
||||
})
|
||||
|
||||
|
||||
def _inject_keep(host_section):
|
||||
"""Return a patcher that injects _KEEP_LINE at the top of `host_section`."""
|
||||
def patch(content):
|
||||
if _MARKER in content:
|
||||
return content
|
||||
return host_section.sub(r"\1" + _KEEP_LINE, content, count=1)
|
||||
return patch
|
||||
|
||||
|
||||
# Variants not listed here intentionally have no .ld patcher:
|
||||
# - RTL8710B: hal.h uses section(".image2.ram.text") which the stock linker
|
||||
# already routes into .ram_image2.text (> BD_RAM).
|
||||
# - RTL8720C: stock linker already consumes *(.sram.text*).
|
||||
# - BK72xx (all): SDK masks FIQ+IRQ around flash writes, IRAM_ATTR is no-op.
|
||||
_PATCHERS_BY_VARIANT = {
|
||||
"LN882H": (_inject_keep(_LN_COPY),),
|
||||
}
|
||||
|
||||
|
||||
def _patchers_for(variant):
|
||||
return _PATCHERS_BY_VARIANT.get(variant, ())
|
||||
|
||||
|
||||
def _pre_link(target, source, env):
|
||||
build_dir = env.subst("$BUILD_DIR")
|
||||
ld_files = [f for f in os.listdir(build_dir) if f.endswith(".ld")]
|
||||
patched = 0
|
||||
for name in ld_files:
|
||||
path = os.path.join(build_dir, name)
|
||||
with open(path, "r", encoding="utf-8") as fh:
|
||||
original = fh.read()
|
||||
if _MARKER in original:
|
||||
patched += 1
|
||||
continue
|
||||
content = original
|
||||
for fn in _patchers:
|
||||
content = fn(content)
|
||||
if content != original:
|
||||
with open(path, "w", encoding="utf-8") as fh:
|
||||
fh.write(content)
|
||||
print("ESPHome: patched {} for IRAM_ATTR placement".format(name))
|
||||
patched += 1
|
||||
if not patched:
|
||||
raise RuntimeError(
|
||||
"ESPHome: no .ld in {} was patched for IRAM_ATTR. Update the "
|
||||
"regex in patch_linker.py.script (_PATCHERS_BY_VARIANT).".format(
|
||||
build_dir
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# Substrings matched against demangled names as a fallback on RTL8720C,
|
||||
# where we cannot inject __esphome_sram_text_start/end markers.
|
||||
_FALLBACK_SUBSTRINGS = ("wake_loop_any_context", "wake_loop_isrsafe",
|
||||
"enable_loop_soon_any_context")
|
||||
|
||||
|
||||
def _post_link(target, source, env):
|
||||
"""Print where IRAM_ATTR ended up so users can confirm at a glance."""
|
||||
elf = env.subst("$BUILD_DIR/${PROGNAME}.elf")
|
||||
if not os.path.isfile(elf):
|
||||
return
|
||||
nm = env.subst("$NM")
|
||||
try:
|
||||
out = subprocess.check_output(
|
||||
[nm, "--defined-only", "--demangle", elf], text=True
|
||||
)
|
||||
except (OSError, subprocess.CalledProcessError) as exc:
|
||||
print("ESPHome: IRAM_ATTR summary unavailable (nm failed: {})".format(exc))
|
||||
return
|
||||
start = end = None
|
||||
fallback = []
|
||||
for line in out.splitlines():
|
||||
parts = line.split(maxsplit=2)
|
||||
if len(parts) != 3:
|
||||
continue
|
||||
addr_str, _kind, name = parts
|
||||
if name == "__esphome_sram_text_start":
|
||||
start = int(addr_str, 16)
|
||||
elif name == "__esphome_sram_text_end":
|
||||
end = int(addr_str, 16)
|
||||
elif "veneer" not in name and any(s in name for s in _FALLBACK_SUBSTRINGS):
|
||||
fallback.append(int(addr_str, 16))
|
||||
print("ESPHome: IRAM_ATTR placement summary ({}):".format(_variant))
|
||||
if start is not None and end is not None:
|
||||
print(" .sram.text: {} bytes at 0x{:08x} - 0x{:08x}".format(end - start, start, end))
|
||||
elif fallback:
|
||||
lo, hi = min(fallback), max(fallback)
|
||||
print(" IRAM symbols at 0x{:08x} - 0x{:08x} (approx {} bytes)".format(lo, hi, hi - lo))
|
||||
else:
|
||||
print(" no IRAM_ATTR symbols found")
|
||||
|
||||
|
||||
if (_variant := _detect(env)) is None:
|
||||
raise RuntimeError(
|
||||
"ESPHome: could not determine LibreTiny variant from build flags. "
|
||||
"patch_linker.py needs USE_LIBRETINY_VARIANT_* to route IRAM_ATTR "
|
||||
"into SRAM; without it, ISR handlers would silently end up in flash."
|
||||
)
|
||||
if _variant not in KNOWN_VARIANTS:
|
||||
raise RuntimeError(
|
||||
"ESPHome: unknown LibreTiny variant {!r}; patch_linker.py does not "
|
||||
"know how to route IRAM_ATTR into SRAM for this family. Update "
|
||||
"patch_linker.py.script before shipping firmware.".format(_variant)
|
||||
)
|
||||
|
||||
if _patchers := _patchers_for(_variant):
|
||||
# LibreTiny writes the processed .ld templates into $BUILD_DIR during its
|
||||
# own builder setup, which may run after this script. Register the patch
|
||||
# as a pre-link action so it executes once the linker scripts exist.
|
||||
env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", _pre_link)
|
||||
|
||||
# Post-link summary for every family that reaches this script.
|
||||
env.AddPostAction("$BUILD_DIR/${PROGNAME}.elf", _post_link)
|
||||
@@ -3,7 +3,6 @@
|
||||
#include "preferences.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cinttypes>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
@@ -11,9 +10,6 @@ namespace esphome::libretiny {
|
||||
|
||||
static const char *const TAG = "preferences";
|
||||
|
||||
// Buffer size for converting uint32_t to string: max "4294967295" (10 chars) + null terminator + 1 padding
|
||||
static constexpr size_t KEY_BUFFER_SIZE = 12;
|
||||
|
||||
struct NVSData {
|
||||
uint32_t key;
|
||||
SmallInlineBuffer<8> data; // Most prefs fit in 8 bytes (covers fan, cover, select, etc.)
|
||||
@@ -50,8 +46,8 @@ bool LibreTinyPreferenceBackend::load(uint8_t *data, size_t len) {
|
||||
}
|
||||
}
|
||||
|
||||
char key_str[KEY_BUFFER_SIZE];
|
||||
snprintf(key_str, sizeof(key_str), "%" PRIu32, this->key);
|
||||
char key_str[UINT32_MAX_STR_SIZE];
|
||||
uint32_to_str(key_str, this->key);
|
||||
fdb_blob_make(this->blob, data, len);
|
||||
size_t actual_len = fdb_kv_get_blob(this->db, key_str, this->blob);
|
||||
if (actual_len != len) {
|
||||
@@ -92,8 +88,8 @@ bool LibreTinyPreferences::sync() {
|
||||
uint32_t last_key = 0;
|
||||
|
||||
for (const auto &save : s_pending_save) {
|
||||
char key_str[KEY_BUFFER_SIZE];
|
||||
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
|
||||
char key_str[UINT32_MAX_STR_SIZE];
|
||||
uint32_to_str(key_str, save.key);
|
||||
ESP_LOGVV(TAG, "Checking if FDB data %s has changed", key_str);
|
||||
if (this->is_changed_(&this->db, save, key_str)) {
|
||||
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size());
|
||||
|
||||
@@ -58,6 +58,12 @@ void AddressableLightTransformer::start() {
|
||||
// our transition will handle brightness, disable brightness in correction.
|
||||
this->light_.correction_.set_local_brightness(255);
|
||||
this->target_color_ *= to_uint8_scale(end_values.get_brightness() * end_values.get_state());
|
||||
|
||||
// Uniformity scan is deferred to the first apply() call. start() can run before the underlying
|
||||
// LED output's setup() has allocated its frame buffer (e.g. on_boot at priority > HARDWARE
|
||||
// triggering a transition), and reading through ESPColorView would deref a null buffer.
|
||||
this->uniform_start_scanned_ = false;
|
||||
this->uniform_start_is_uniform_ = false;
|
||||
}
|
||||
|
||||
inline constexpr uint8_t subtract_scaled_difference(uint8_t a, uint8_t b, int32_t scale) {
|
||||
@@ -97,12 +103,57 @@ optional<LightColorValues> AddressableLightTransformer::apply() {
|
||||
// non-linear when applying small deltas.
|
||||
|
||||
if (smoothed_progress > this->last_transition_progress_ && this->last_transition_progress_ < 1.f) {
|
||||
int32_t scale = int32_t(256.f * std::max((1.f - smoothed_progress) / (1.f - this->last_transition_progress_), 0.f));
|
||||
for (auto led : this->light_) {
|
||||
led.set_rgbw(subtract_scaled_difference(this->target_color_.red, led.get_red(), scale),
|
||||
subtract_scaled_difference(this->target_color_.green, led.get_green(), scale),
|
||||
subtract_scaled_difference(this->target_color_.blue, led.get_blue(), scale),
|
||||
subtract_scaled_difference(this->target_color_.white, led.get_white(), scale));
|
||||
// Lazy uniformity scan: deferred from start() so the LED output's setup() has run and the
|
||||
// frame buffer is valid. When every LED already has the same color (the common case: plain
|
||||
// turn_on/turn_off on a uniform strip), interpolate math-only against a single start color.
|
||||
// Avoiding the per-step read-back through the 8-bit stored byte prevents gamma round-trip
|
||||
// quantization from stalling the fade at low values (e.g. gamma 2.8 pre-gamma values <27
|
||||
// round to stored 0, freezing progress).
|
||||
if (!this->uniform_start_scanned_) {
|
||||
this->uniform_start_scanned_ = true;
|
||||
if (this->light_.size() > 0) {
|
||||
Color first = this->light_[0].get();
|
||||
bool uniform = true;
|
||||
for (int32_t i = 1; i < this->light_.size(); i++) {
|
||||
if (this->light_[i].get() != first) {
|
||||
uniform = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (uniform) {
|
||||
this->uniform_start_color_ = first;
|
||||
this->uniform_start_is_uniform_ = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this->uniform_start_is_uniform_) {
|
||||
// All LEDs started at the same color: compute the interpolated value once and write it to
|
||||
// every LED. No read-back, so each LED's stored byte advances through every gamma threshold
|
||||
// as smoothed_progress crosses it, instead of stalling at 0 for low pre-gamma values.
|
||||
//
|
||||
// Trade-off: any mid-transition writes to individual LEDs (e.g. from a user lambda) will be
|
||||
// overwritten on the next apply() here. The fallback path below would have respected them
|
||||
// via its read-back. Concurrent per-LED mutation during a transition isn't a pattern we
|
||||
// support, so this is acceptable.
|
||||
// lerp(start, target, progress) via existing helper: target - (target-start)*(1-progress).
|
||||
const Color &start = this->uniform_start_color_;
|
||||
int32_t remaining = int32_t(256.f * (1.f - smoothed_progress));
|
||||
uint8_t r = subtract_scaled_difference(this->target_color_.red, start.red, remaining);
|
||||
uint8_t g = subtract_scaled_difference(this->target_color_.green, start.green, remaining);
|
||||
uint8_t b = subtract_scaled_difference(this->target_color_.blue, start.blue, remaining);
|
||||
uint8_t w = subtract_scaled_difference(this->target_color_.white, start.white, remaining);
|
||||
for (auto led : this->light_) {
|
||||
led.set_rgbw(r, g, b, w);
|
||||
}
|
||||
} else {
|
||||
int32_t scale =
|
||||
int32_t(256.f * std::max((1.f - smoothed_progress) / (1.f - this->last_transition_progress_), 0.f));
|
||||
for (auto led : this->light_) {
|
||||
led.set_rgbw(subtract_scaled_difference(this->target_color_.red, led.get_red(), scale),
|
||||
subtract_scaled_difference(this->target_color_.green, led.get_green(), scale),
|
||||
subtract_scaled_difference(this->target_color_.blue, led.get_blue(), scale),
|
||||
subtract_scaled_difference(this->target_color_.white, led.get_white(), scale));
|
||||
}
|
||||
}
|
||||
this->last_transition_progress_ = smoothed_progress;
|
||||
this->light_.schedule_show();
|
||||
|
||||
@@ -115,6 +115,9 @@ class AddressableLightTransformer : public LightTransformer {
|
||||
AddressableLight &light_;
|
||||
float last_transition_progress_{0.0f};
|
||||
Color target_color_{};
|
||||
Color uniform_start_color_{};
|
||||
bool uniform_start_scanned_{false};
|
||||
bool uniform_start_is_uniform_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::light
|
||||
|
||||
@@ -22,4 +22,20 @@ uint8_t ESPColorCorrection::gamma_uncorrect_(uint8_t value) const {
|
||||
return (target - a <= b - target) ? lo : lo + 1;
|
||||
}
|
||||
|
||||
Color ESPColorCorrection::color_uncorrect(Color color) const {
|
||||
// uncorrected = corrected^(1/gamma) / (max_brightness * local_brightness)
|
||||
return Color(this->color_uncorrect_red(color.red), this->color_uncorrect_green(color.green),
|
||||
this->color_uncorrect_blue(color.blue), this->color_uncorrect_white(color.white));
|
||||
}
|
||||
|
||||
uint8_t ESPColorCorrection::color_uncorrect_channel_(uint8_t value, uint8_t max_brightness) const {
|
||||
if (max_brightness == 0 || this->local_brightness_ == 0)
|
||||
return 0;
|
||||
// Use 32-bit intermediates: when max_brightness and local_brightness_ are small but non-zero,
|
||||
// (uncorrected / max_brightness) * 255 can exceed 65535 before the std::min(255) clamp runs.
|
||||
uint32_t uncorrected = this->gamma_uncorrect_(value) * 255UL;
|
||||
uint32_t res = ((uncorrected / max_brightness) * 255UL) / this->local_brightness_;
|
||||
return static_cast<uint8_t>(std::min(res, uint32_t(255)));
|
||||
}
|
||||
|
||||
} // namespace esphome::light
|
||||
|
||||
@@ -46,38 +46,18 @@ class ESPColorCorrection {
|
||||
uint8_t res = esp_scale8_twice(white, this->max_brightness_.white, this->local_brightness_);
|
||||
return this->gamma_correct_(res);
|
||||
}
|
||||
inline Color color_uncorrect(Color color) const ESPHOME_ALWAYS_INLINE {
|
||||
// uncorrected = corrected^(1/gamma) / (max_brightness * local_brightness)
|
||||
return Color(this->color_uncorrect_red(color.red), this->color_uncorrect_green(color.green),
|
||||
this->color_uncorrect_blue(color.blue), this->color_uncorrect_white(color.white));
|
||||
}
|
||||
Color color_uncorrect(Color color) const;
|
||||
inline uint8_t color_uncorrect_red(uint8_t red) const ESPHOME_ALWAYS_INLINE {
|
||||
if (this->max_brightness_.red == 0 || this->local_brightness_ == 0)
|
||||
return 0;
|
||||
uint16_t uncorrected = this->gamma_uncorrect_(red) * 255UL;
|
||||
uint16_t res = ((uncorrected / this->max_brightness_.red) * 255UL) / this->local_brightness_;
|
||||
return (uint8_t) std::min(res, uint16_t(255));
|
||||
return this->color_uncorrect_channel_(red, this->max_brightness_.red);
|
||||
}
|
||||
inline uint8_t color_uncorrect_green(uint8_t green) const ESPHOME_ALWAYS_INLINE {
|
||||
if (this->max_brightness_.green == 0 || this->local_brightness_ == 0)
|
||||
return 0;
|
||||
uint16_t uncorrected = this->gamma_uncorrect_(green) * 255UL;
|
||||
uint16_t res = ((uncorrected / this->max_brightness_.green) * 255UL) / this->local_brightness_;
|
||||
return (uint8_t) std::min(res, uint16_t(255));
|
||||
return this->color_uncorrect_channel_(green, this->max_brightness_.green);
|
||||
}
|
||||
inline uint8_t color_uncorrect_blue(uint8_t blue) const ESPHOME_ALWAYS_INLINE {
|
||||
if (this->max_brightness_.blue == 0 || this->local_brightness_ == 0)
|
||||
return 0;
|
||||
uint16_t uncorrected = this->gamma_uncorrect_(blue) * 255UL;
|
||||
uint16_t res = ((uncorrected / this->max_brightness_.blue) * 255UL) / this->local_brightness_;
|
||||
return (uint8_t) std::min(res, uint16_t(255));
|
||||
return this->color_uncorrect_channel_(blue, this->max_brightness_.blue);
|
||||
}
|
||||
inline uint8_t color_uncorrect_white(uint8_t white) const ESPHOME_ALWAYS_INLINE {
|
||||
if (this->max_brightness_.white == 0 || this->local_brightness_ == 0)
|
||||
return 0;
|
||||
uint16_t uncorrected = this->gamma_uncorrect_(white) * 255UL;
|
||||
uint16_t res = ((uncorrected / this->max_brightness_.white) * 255UL) / this->local_brightness_;
|
||||
return (uint8_t) std::min(res, uint16_t(255));
|
||||
return this->color_uncorrect_channel_(white, this->max_brightness_.white);
|
||||
}
|
||||
|
||||
protected:
|
||||
@@ -85,6 +65,9 @@ class ESPColorCorrection {
|
||||
uint8_t gamma_correct_(uint8_t value) const;
|
||||
/// Reverse gamma: binary search the forward PROGMEM table
|
||||
uint8_t gamma_uncorrect_(uint8_t value) const;
|
||||
/// Shared body of color_uncorrect_{red,green,blue,white}. Kept out-of-line
|
||||
/// to avoid duplicating two 16-bit divides at every call site.
|
||||
uint8_t color_uncorrect_channel_(uint8_t value, uint8_t max_brightness) const;
|
||||
|
||||
const uint16_t *gamma_table_{nullptr};
|
||||
Color max_brightness_{255, 255, 255, 255};
|
||||
|
||||
@@ -65,3 +65,8 @@ async def to_code(config):
|
||||
@pins.PIN_SCHEMA_REGISTRY.register("ln882x", PIN_SCHEMA)
|
||||
async def pin_to_code(config):
|
||||
return await libretiny.gpio.component_pin_to_code(config)
|
||||
|
||||
|
||||
# Called by writer.py; delegates to the shared libretiny implementation.
|
||||
def copy_files() -> None:
|
||||
libretiny.copy_files()
|
||||
|
||||
@@ -44,6 +44,7 @@ from esphome.core import CORE, ID, Lambda
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.final_validate import full_config
|
||||
from esphome.helpers import write_file_if_changed
|
||||
from esphome.writer import clean_build
|
||||
from esphome.yaml_util import load_yaml
|
||||
|
||||
from . import defines as df, helpers, lv_validation as lvalid, widgets
|
||||
@@ -341,7 +342,7 @@ async def to_code(configs):
|
||||
df.LOGGER.info("LVGL will use hardware rotation via display driver")
|
||||
else:
|
||||
rotation_type = RotationType.ROTATION_SOFTWARE
|
||||
if get_esp32_variant() == VARIANT_ESP32P4:
|
||||
if CORE.is_esp32 and get_esp32_variant() == VARIANT_ESP32P4:
|
||||
df.LOGGER.info("LVGL will use software rotation (PPA accelerated)")
|
||||
else:
|
||||
df.LOGGER.info("LVGL will use software rotation")
|
||||
@@ -451,7 +452,8 @@ async def to_code(configs):
|
||||
df.add_define(f"LV_DRAW_SW_SUPPORT_{fmt}", "1")
|
||||
|
||||
lv_conf_h_file = CORE.relative_src_path(LV_CONF_FILENAME)
|
||||
write_file_if_changed(lv_conf_h_file, generate_lv_conf_h())
|
||||
if write_file_if_changed(lv_conf_h_file, generate_lv_conf_h()):
|
||||
clean_build(clear_pio_cache=False)
|
||||
cg.add_build_flag("-DLV_CONF_H=1")
|
||||
# handle windows paths in a way that doesn't break the generated C++
|
||||
lv_conf_h_path = Path(lv_conf_h_file).as_posix()
|
||||
|
||||
@@ -76,16 +76,17 @@ inline void lv_style_set_text_font(lv_style_t *style, const font::Font *font) {
|
||||
}
|
||||
#endif
|
||||
#if defined(USE_LVGL_IMAGE) && defined(USE_IMAGE)
|
||||
// Shortcut / overload, so that the source of an image can easily be updated
|
||||
// from within a lambda.
|
||||
inline void lv_image_set_src(lv_obj_t *obj, image::Image *image) { lv_image_set_src(obj, image->get_lv_image_dsc()); }
|
||||
#if LV_USE_IMAGE
|
||||
// Shortcut / overload, so that the source of an image widget can easily be updated from within a lambda.
|
||||
inline void lv_image_set_src(lv_obj_t *obj, image::Image *image) { ::lv_image_set_src(obj, image->get_lv_image_dsc()); }
|
||||
#endif // LV_USE_IMAGE
|
||||
|
||||
inline void lv_obj_set_style_bitmap_mask_src(lv_obj_t *obj, image::Image *image, lv_style_selector_t selector) {
|
||||
lv_obj_set_style_bitmap_mask_src(obj, image->get_lv_image_dsc(), selector);
|
||||
::lv_obj_set_style_bitmap_mask_src(obj, image->get_lv_image_dsc(), selector);
|
||||
}
|
||||
|
||||
inline void lv_obj_set_style_bg_image_src(lv_obj_t *obj, image::Image *image, lv_style_selector_t selector) {
|
||||
lv_obj_set_style_bg_image_src(obj, image->get_lv_image_dsc(), selector);
|
||||
::lv_obj_set_style_bg_image_src(obj, image->get_lv_image_dsc(), selector);
|
||||
}
|
||||
#endif // USE_LVGL_IMAGE
|
||||
#ifdef USE_LVGL_ANIMIMG
|
||||
|
||||
@@ -170,7 +170,7 @@ async def to_code(config):
|
||||
cg.add_library("LEAmDNS", None)
|
||||
|
||||
if CORE.is_esp32:
|
||||
add_idf_component(name="espressif/mdns", ref="1.10.0")
|
||||
add_idf_component(name="espressif/mdns", ref="1.11.0")
|
||||
|
||||
cg.add_define("USE_MDNS")
|
||||
|
||||
|
||||
@@ -451,6 +451,8 @@ async def to_code(config):
|
||||
ota.request_ota_state_listeners()
|
||||
|
||||
esp32.add_idf_component(name="espressif/esp-tflite-micro", ref="1.3.3~1")
|
||||
# Pin esp-nn for stable future builds (esp-tflite-micro depends on esp-nn)
|
||||
esp32.add_idf_component(name="espressif/esp-nn", ref="1.1.2")
|
||||
|
||||
cg.add_build_flag("-DTF_LITE_STATIC_MEMORY")
|
||||
cg.add_build_flag("-DTF_LITE_DISABLE_X86_NEON")
|
||||
|
||||
@@ -29,14 +29,6 @@ void VADModel::log_model_config() {
|
||||
bool StreamingModel::load_model_() {
|
||||
RAMAllocator<uint8_t> arena_allocator;
|
||||
|
||||
if (this->tensor_arena_ == nullptr) {
|
||||
this->tensor_arena_ = arena_allocator.allocate(this->tensor_arena_size_);
|
||||
if (this->tensor_arena_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Could not allocate the streaming model's tensor arena.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this->var_arena_ == nullptr) {
|
||||
this->var_arena_ = arena_allocator.allocate(STREAMING_MODEL_VARIABLE_ARENA_SIZE);
|
||||
if (this->var_arena_ == nullptr) {
|
||||
@@ -53,6 +45,26 @@ bool StreamingModel::load_model_() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Probe for the actual required tensor arena size if not yet determined
|
||||
if (!this->tensor_arena_size_probed_) {
|
||||
size_t probed_size = this->probe_arena_size_();
|
||||
if (probed_size > 0) {
|
||||
ESP_LOGD(TAG, "Probed tensor arena size: %zu bytes", probed_size);
|
||||
this->tensor_arena_size_ = probed_size;
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Arena size probe failed, using manifest size: %zu bytes", this->tensor_arena_size_);
|
||||
}
|
||||
this->tensor_arena_size_probed_ = true;
|
||||
}
|
||||
|
||||
if (this->tensor_arena_ == nullptr) {
|
||||
this->tensor_arena_ = arena_allocator.allocate(this->tensor_arena_size_);
|
||||
if (this->tensor_arena_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Could not allocate the streaming model's tensor arena.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this->interpreter_ == nullptr) {
|
||||
this->interpreter_ =
|
||||
make_unique<tflite::MicroInterpreter>(tflite::GetModel(this->model_start_), this->streaming_op_resolver_,
|
||||
@@ -94,6 +106,70 @@ bool StreamingModel::load_model_() {
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t StreamingModel::probe_arena_size_() {
|
||||
RAMAllocator<uint8_t> arena_allocator;
|
||||
|
||||
// Try with the manifest size first, then escalates to 1.5, then 2x if it fails. Different platforms and different
|
||||
// versions of the esp-nn library require different amounts of memory, so the manifest size may not always be correct,
|
||||
// and probing allows us to find the actual required size for the current build and platform. Aligns test sizes to 16
|
||||
// bytes.
|
||||
size_t attempt_sizes[] = {(this->tensor_arena_size_ + 15) & ~15, (this->tensor_arena_size_ * 3 / 2 + 15) & ~15,
|
||||
(this->tensor_arena_size_ * 2 + 15) & ~15};
|
||||
|
||||
for (size_t attempt_size : attempt_sizes) {
|
||||
uint8_t *probe_arena = arena_allocator.allocate(attempt_size);
|
||||
if (probe_arena == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verify the model works at all with this arena size
|
||||
auto probe_interpreter = make_unique<tflite::MicroInterpreter>(
|
||||
tflite::GetModel(this->model_start_), this->streaming_op_resolver_, probe_arena, attempt_size, this->mrv_);
|
||||
|
||||
if (probe_interpreter->AllocateTensors() != kTfLiteOk) {
|
||||
probe_interpreter.reset();
|
||||
arena_allocator.deallocate(probe_arena, attempt_size);
|
||||
this->ma_ = tflite::MicroAllocator::Create(this->var_arena_, STREAMING_MODEL_VARIABLE_ARENA_SIZE);
|
||||
this->mrv_ = tflite::MicroResourceVariables::Create(this->ma_, 20);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try to shrink the arena. Start with arena_used_bytes() + 16 (rounded to 16-byte alignment).
|
||||
// If that works, use it. Otherwise, try midpoints between that and the full size until one succeeds.
|
||||
size_t lower = (probe_interpreter->arena_used_bytes() + 16 + 15) & ~15;
|
||||
probe_interpreter.reset();
|
||||
this->ma_ = tflite::MicroAllocator::Create(this->var_arena_, STREAMING_MODEL_VARIABLE_ARENA_SIZE);
|
||||
this->mrv_ = tflite::MicroResourceVariables::Create(this->ma_, 20);
|
||||
|
||||
size_t upper = attempt_size;
|
||||
|
||||
while (lower < upper) {
|
||||
auto test_interpreter = make_unique<tflite::MicroInterpreter>(
|
||||
tflite::GetModel(this->model_start_), this->streaming_op_resolver_, probe_arena, lower, this->mrv_);
|
||||
|
||||
bool ok = test_interpreter->AllocateTensors() == kTfLiteOk;
|
||||
|
||||
test_interpreter.reset();
|
||||
this->ma_ = tflite::MicroAllocator::Create(this->var_arena_, STREAMING_MODEL_VARIABLE_ARENA_SIZE);
|
||||
this->mrv_ = tflite::MicroResourceVariables::Create(this->ma_, 20);
|
||||
|
||||
if (ok) {
|
||||
// Found a working size smaller than the full arena
|
||||
upper = lower + 16; // Pad by 16 bytes to be safe for future allocations
|
||||
break;
|
||||
}
|
||||
|
||||
// Try the midpoint between current attempt and full size
|
||||
lower = ((lower + upper) / 2 + 15) & ~15;
|
||||
}
|
||||
|
||||
arena_allocator.deallocate(probe_arena, attempt_size);
|
||||
return upper;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void StreamingModel::unload_model() {
|
||||
this->interpreter_.reset();
|
||||
|
||||
|
||||
@@ -63,6 +63,10 @@ class StreamingModel {
|
||||
/// @brief Allocates tensor and variable arenas and sets up the model interpreter
|
||||
/// @return True if successful, false otherwise
|
||||
bool load_model_();
|
||||
/// @brief Probes the actual required tensor arena size by trial allocation.
|
||||
/// Tries the manifest size first, then 2x if that fails.
|
||||
/// @return The required arena size rounded up to 16-byte alignment, or 0 on failure.
|
||||
size_t probe_arena_size_();
|
||||
/// @brief Returns true if successfully registered the streaming model's TensorFlow operations
|
||||
bool register_streaming_ops_(tflite::MicroMutableOpResolver<20> &op_resolver);
|
||||
|
||||
@@ -70,6 +74,7 @@ class StreamingModel {
|
||||
|
||||
bool loaded_{false};
|
||||
bool enabled_{true};
|
||||
bool tensor_arena_size_probed_{false};
|
||||
bool unprocessed_probability_status_{false};
|
||||
uint8_t current_stride_step_{0};
|
||||
int16_t ignore_windows_{-MIN_SLICES_BEFORE_DETECTION};
|
||||
|
||||
@@ -28,7 +28,8 @@ void AirConditioner::on_status_change() {
|
||||
if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_OK &&
|
||||
this->base_.getCapabilities().supportFrostProtectionPreset() && !this->frost_protection_set_) {
|
||||
// Read existing presets (set by codegen), append frost protection, write back
|
||||
const auto &existing = this->get_traits().get_supported_custom_presets();
|
||||
auto traits = this->get_traits();
|
||||
const auto &existing = traits.get_supported_custom_presets();
|
||||
bool found = false;
|
||||
for (const char *p : existing) {
|
||||
if (strcmp(p, Constants::FREEZE_PROTECTION) == 0) {
|
||||
|
||||
@@ -195,7 +195,7 @@ def model_schema(config):
|
||||
"big_endian", "little_endian", lower=True
|
||||
),
|
||||
model.option(CONF_COLOR_DEPTH, 16): cv.one_of(*color_depth, lower=True),
|
||||
model.option(CONF_DRAW_ROUNDING, 2): power_of_two,
|
||||
model.option(CONF_DRAW_ROUNDING, 1): power_of_two,
|
||||
model.option(CONF_PIXEL_MODE, DISPLAY_16BIT): cv.one_of(
|
||||
*pixel_modes, lower=True
|
||||
),
|
||||
@@ -297,9 +297,9 @@ def _final_validate(config):
|
||||
|
||||
buffer_size = color_depth // 8 * width * height // frac
|
||||
# Target a buffer size of 20kB, except for large displays, which shouldn't end up here
|
||||
fraction = min(20000.0, buffer_size // 16) / buffer_size
|
||||
fraction = min(20000.0, buffer_size // 4) / buffer_size
|
||||
config[CONF_BUFFER_SIZE] = 1.0 / next(
|
||||
x for x in range(2, 17) if fraction >= 1 / x
|
||||
(x for x in range(2, 8) if fraction >= 1 / x), 8
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -234,9 +234,9 @@ class MipiSpi : public display::Display,
|
||||
}
|
||||
|
||||
void dump_config() override {
|
||||
internal_dump_config(this->model_, this->get_width(), this->get_height(), OFFSET_WIDTH, OFFSET_HEIGHT, MADCTL,
|
||||
this->invert_colors_, DISPLAYPIXEL * 8, IS_BIG_ENDIAN, this->brightness_, this->cs_,
|
||||
this->reset_pin_, this->dc_pin_, this->mode_, this->data_rate_, BUS_TYPE,
|
||||
internal_dump_config(this->model_, this->get_width(), this->get_height(), OFFSET_WIDTH, OFFSET_HEIGHT,
|
||||
(uint8_t) MADCTL, this->invert_colors_, DISPLAYPIXEL * 8, IS_BIG_ENDIAN, this->brightness_,
|
||||
this->cs_, this->reset_pin_, this->dc_pin_, this->mode_, this->data_rate_, BUS_TYPE,
|
||||
HAS_HARDWARE_ROTATION);
|
||||
}
|
||||
|
||||
@@ -305,7 +305,7 @@ class MipiSpi : public display::Display,
|
||||
this->write_command_(BRIGHTNESS, this->brightness_.value());
|
||||
|
||||
// calculate new madctl value from base value adjusted for rotation
|
||||
uint8_t madctl = MADCTL; // lower 8 bits only
|
||||
uint8_t madctl = (uint8_t) MADCTL; // lower 8 bits only
|
||||
constexpr bool use_flips = (MADCTL & MADCTL_FLIP_FLAG) != 0;
|
||||
constexpr uint8_t x_mask = use_flips ? MADCTL_XFLIP : MADCTL_MX;
|
||||
constexpr uint8_t y_mask = use_flips ? MADCTL_YFLIP : MADCTL_MY;
|
||||
@@ -546,13 +546,12 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
|
||||
}
|
||||
// for updates with a small buffer, we repeatedly call the writer_ function, clipping the height to a fraction of
|
||||
// the display height,
|
||||
for (this->start_line_ = 0; this->start_line_ < this->get_height_internal();
|
||||
this->start_line_ += this->get_height_internal() / FRACTION) {
|
||||
auto increment = (this->get_height_internal() / FRACTION / ROUNDING) * ROUNDING;
|
||||
for (this->start_line_ = 0; this->start_line_ < this->get_height_internal(); this->start_line_ = this->end_line_) {
|
||||
#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE
|
||||
auto lap = millis();
|
||||
#endif
|
||||
this->end_line_ =
|
||||
clamp_at_most(this->start_line_ + this->get_height_internal() / FRACTION, this->get_height_internal());
|
||||
this->end_line_ = clamp_at_most(this->start_line_ + increment, this->get_height_internal());
|
||||
if (this->auto_clear_enabled_) {
|
||||
this->clear();
|
||||
}
|
||||
@@ -574,12 +573,13 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
|
||||
// Some chips require that the drawing window be aligned on certain boundaries
|
||||
this->x_low_ = this->x_low_ / ROUNDING * ROUNDING;
|
||||
this->y_low_ = this->y_low_ / ROUNDING * ROUNDING;
|
||||
this->x_high_ = (this->x_high_ + ROUNDING) / ROUNDING * ROUNDING - 1;
|
||||
this->y_high_ = (this->y_high_ + ROUNDING) / ROUNDING * ROUNDING - 1;
|
||||
this->x_high_ = round_buffer(this->x_high_ + 1) - 1;
|
||||
this->y_high_ = clamp_at_most(round_buffer(this->y_high_ + 1) - 1, this->end_line_ - 1);
|
||||
int w = this->x_high_ - this->x_low_ + 1;
|
||||
int h = this->y_high_ - this->y_low_ + 1;
|
||||
this->write_to_display_(this->x_low_, this->y_low_, w, h, this->buffer_, this->x_low_,
|
||||
this->y_low_ - this->start_line_, round_buffer(this->get_width_internal()) - w);
|
||||
this->y_low_ - this->start_line_,
|
||||
round_buffer(this->get_width_internal()) - w - this->x_low_);
|
||||
// invalidate watermarks
|
||||
this->x_low_ = this->get_width_internal();
|
||||
this->y_low_ = this->get_height_internal();
|
||||
|
||||
@@ -15,7 +15,7 @@ from esphome.components.mipi import (
|
||||
import esphome.config_validation as cv
|
||||
|
||||
from .amoled import CO5300
|
||||
from .ili import ILI9488_A
|
||||
from .ili import ILI9488_A, ST7789V
|
||||
from .jc import AXS15231
|
||||
|
||||
DriverChip(
|
||||
@@ -243,3 +243,15 @@ ST7789P.extend(
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
ST7789V.extend(
|
||||
"WAVESHARE-ESP32-C6-LCD-1.47",
|
||||
width=172,
|
||||
height=320,
|
||||
offset_width=34,
|
||||
invert_colors=True,
|
||||
data_rate="40MHz",
|
||||
reset_pin=21,
|
||||
cs_pin=14,
|
||||
dc_pin={"number": 15, "ignore_strapping_warning": True},
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace esphome::mitsubishi_cn105 {
|
||||
static const char *const TAG = "mitsubishi_cn105.climate";
|
||||
|
||||
static constexpr std::array MODE_MAP{
|
||||
std::pair{MitsubishiCN105::Mode::AUTO, climate::CLIMATE_MODE_AUTO},
|
||||
std::pair{MitsubishiCN105::Mode::AUTO, climate::CLIMATE_MODE_HEAT_COOL},
|
||||
std::pair{MitsubishiCN105::Mode::HEAT, climate::CLIMATE_MODE_HEAT},
|
||||
std::pair{MitsubishiCN105::Mode::DRY, climate::CLIMATE_MODE_DRY},
|
||||
std::pair{MitsubishiCN105::Mode::COOL, climate::CLIMATE_MODE_COOL},
|
||||
@@ -76,23 +76,13 @@ void MitsubishiCN105Climate::loop() {
|
||||
climate::ClimateTraits MitsubishiCN105Climate::traits() {
|
||||
climate::ClimateTraits traits;
|
||||
|
||||
traits.set_supported_modes({
|
||||
climate::CLIMATE_MODE_OFF,
|
||||
climate::CLIMATE_MODE_COOL,
|
||||
climate::CLIMATE_MODE_HEAT,
|
||||
climate::CLIMATE_MODE_DRY,
|
||||
climate::CLIMATE_MODE_FAN_ONLY,
|
||||
climate::CLIMATE_MODE_AUTO,
|
||||
});
|
||||
for (const auto &p : MODE_MAP) {
|
||||
traits.add_supported_mode(p.second);
|
||||
}
|
||||
|
||||
traits.set_supported_fan_modes({
|
||||
climate::CLIMATE_FAN_AUTO,
|
||||
climate::CLIMATE_FAN_QUIET,
|
||||
climate::CLIMATE_FAN_LOW,
|
||||
climate::CLIMATE_FAN_MEDIUM,
|
||||
climate::CLIMATE_FAN_MIDDLE,
|
||||
climate::CLIMATE_FAN_HIGH,
|
||||
});
|
||||
for (const auto &p : FAN_MODE_MAP) {
|
||||
traits.add_supported_fan_mode(p.second);
|
||||
}
|
||||
|
||||
traits.set_visual_min_temperature(16.0f);
|
||||
traits.set_visual_max_temperature(31.0f);
|
||||
|
||||
@@ -36,8 +36,9 @@ bool Nextion::send_command_(const std::string &command) {
|
||||
}
|
||||
|
||||
#ifdef USE_NEXTION_COMMAND_SPACING
|
||||
if (!this->connection_state_.ignore_is_setup_ && !this->command_pacer_.can_send()) {
|
||||
ESP_LOGN(TAG, "Command spacing: delaying command '%s'", command.c_str());
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
if (!this->connection_state_.ignore_is_setup_ && !this->command_pacer_.can_send(now)) {
|
||||
ESP_LOGN(TAG, "Command spacing: delaying '%s'", command.c_str());
|
||||
return false;
|
||||
}
|
||||
#endif // USE_NEXTION_COMMAND_SPACING
|
||||
@@ -48,6 +49,16 @@ bool Nextion::send_command_(const std::string &command) {
|
||||
const uint8_t to_send[3] = {0xFF, 0xFF, 0xFF};
|
||||
this->write_array(to_send, sizeof(to_send));
|
||||
|
||||
#ifdef USE_NEXTION_COMMAND_SPACING
|
||||
// Mark sent immediately after writing to UART. The pacer enforces inter-command
|
||||
// spacing from the transmit side. Marking on ACK (0x01) would leave last_command_time_
|
||||
// at zero indefinitely, making can_send() always return true and spacing a no-op.
|
||||
// ignore_is_setup_ commands (setup/init sequence) bypass spacing intentionally.
|
||||
if (!this->connection_state_.ignore_is_setup_) {
|
||||
this->command_pacer_.mark_sent(now);
|
||||
}
|
||||
#endif // USE_NEXTION_COMMAND_SPACING
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -253,11 +264,8 @@ bool Nextion::send_command(const char *command) {
|
||||
if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_) || this->is_sleeping())
|
||||
return false;
|
||||
|
||||
if (this->send_command_(command)) {
|
||||
this->add_no_result_to_queue_("command");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
this->add_no_result_to_queue_with_command_("command", command);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Nextion::send_command_printf(const char *format, ...) {
|
||||
@@ -274,11 +282,8 @@ bool Nextion::send_command_printf(const char *format, ...) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this->send_command_(buffer)) {
|
||||
this->add_no_result_to_queue_("command_printf");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
this->add_no_result_to_queue_with_command_("command_printf", buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifdef NEXTION_PROTOCOL_LOG
|
||||
@@ -349,25 +354,43 @@ void Nextion::loop() {
|
||||
}
|
||||
|
||||
#ifdef USE_NEXTION_COMMAND_SPACING
|
||||
// Try to send any pending commands if spacing allows
|
||||
this->process_pending_in_queue_();
|
||||
#ifdef USE_NEXTION_WAVEFORM
|
||||
if (!this->waveform_queue_.empty()) {
|
||||
this->check_pending_waveform_();
|
||||
}
|
||||
#endif // USE_NEXTION_WAVEFORM
|
||||
#endif // USE_NEXTION_COMMAND_SPACING
|
||||
}
|
||||
|
||||
#ifdef USE_NEXTION_COMMAND_SPACING
|
||||
void Nextion::process_pending_in_queue_() {
|
||||
if (this->nextion_queue_.empty() || !this->command_pacer_.can_send()) {
|
||||
return;
|
||||
}
|
||||
#ifdef USE_NEXTION_MAX_COMMANDS_PER_LOOP
|
||||
size_t commands_sent = 0;
|
||||
#endif // USE_NEXTION_MAX_COMMANDS_PER_LOOP
|
||||
|
||||
// Check if first item in queue has a pending command
|
||||
auto *front_item = this->nextion_queue_.front();
|
||||
if (front_item && !front_item->pending_command.empty()) {
|
||||
if (this->send_command_(front_item->pending_command)) {
|
||||
// Command sent successfully, clear the pending command
|
||||
front_item->pending_command.clear();
|
||||
ESP_LOGVV(TAG, "Pending command sent: %s", front_item->component->get_variable_name().c_str());
|
||||
for (auto *item : this->nextion_queue_) {
|
||||
if (item == nullptr || item->pending_command.empty()) {
|
||||
continue; // Already sent, waiting for ACK — skip, don't stop
|
||||
}
|
||||
|
||||
#ifdef USE_NEXTION_MAX_COMMANDS_PER_LOOP
|
||||
if (++commands_sent > this->max_commands_per_loop_) {
|
||||
ESP_LOGV(TAG, "Pending cmds: loop limit reached, deferring");
|
||||
break;
|
||||
}
|
||||
#endif // USE_NEXTION_MAX_COMMANDS_PER_LOOP
|
||||
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
if (!this->command_pacer_.can_send(now)) {
|
||||
break; // Spacing not elapsed, stop for this loop iteration
|
||||
}
|
||||
|
||||
if (!this->send_command_(item->pending_command)) {
|
||||
break; // Unexpected send failure, stop
|
||||
}
|
||||
item->pending_command.clear();
|
||||
ESP_LOGVV(TAG, "Pending cmd sent: %s", item->component->get_variable_name().c_str());
|
||||
}
|
||||
}
|
||||
#endif // USE_NEXTION_COMMAND_SPACING
|
||||
@@ -470,10 +493,6 @@ void Nextion::process_nextion_commands_() {
|
||||
this->setup_callback_.call();
|
||||
}
|
||||
}
|
||||
#ifdef USE_NEXTION_COMMAND_SPACING
|
||||
this->command_pacer_.mark_sent(); // Here is where we should mark the command as sent
|
||||
ESP_LOGN(TAG, "Command spacing: marked command sent");
|
||||
#endif
|
||||
break;
|
||||
case 0x02: // invalid Component ID or name was used
|
||||
ESP_LOGW(TAG, "Invalid component ID/name");
|
||||
@@ -1079,10 +1098,18 @@ void Nextion::add_no_result_to_queue_(const std::string &variable_name) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief
|
||||
* @brief Send a command and enqueue it for response tracking.
|
||||
*
|
||||
* @param variable_name Variable name for the queue
|
||||
* @param command
|
||||
* Callers are responsible for checking is_sleeping() before calling this
|
||||
* method. The sleep guard is deliberately absent here because some callers
|
||||
* (e.g. add_no_result_to_queue_with_ignore_sleep_printf_()) are explicitly
|
||||
* sleep-safe and must bypass it.
|
||||
*
|
||||
* If USE_NEXTION_COMMAND_SPACING is enabled and the pacer is not ready,
|
||||
* the command is saved in the queue entry for retry rather than dropped.
|
||||
*
|
||||
* @param variable_name Name of the variable or component associated with the command.
|
||||
* @param command The raw command string to send.
|
||||
*/
|
||||
void Nextion::add_no_result_to_queue_with_command_(const std::string &variable_name, const std::string &command) {
|
||||
if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_) || command.empty())
|
||||
@@ -1263,9 +1290,22 @@ void Nextion::add_to_get_queue(NextionComponentBase *component) {
|
||||
|
||||
std::string command = "get " + component->get_variable_name_to_send();
|
||||
|
||||
#ifdef USE_NEXTION_COMMAND_SPACING
|
||||
// Always enqueue first so the response handler is present when the command
|
||||
// is eventually sent. Store the command for retry if spacing blocked it;
|
||||
// process_pending_in_queue_() will transmit it when the pacer allows.
|
||||
nextion_queue->pending_command = command;
|
||||
this->nextion_queue_.push_back(nextion_queue);
|
||||
if (this->send_command_(command)) {
|
||||
nextion_queue->pending_command.clear();
|
||||
}
|
||||
#else // USE_NEXTION_COMMAND_SPACING
|
||||
if (this->send_command_(command)) {
|
||||
this->nextion_queue_.push_back(nextion_queue);
|
||||
} else {
|
||||
delete nextion_queue; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
}
|
||||
#endif // USE_NEXTION_COMMAND_SPACING
|
||||
}
|
||||
|
||||
#ifdef USE_NEXTION_WAVEFORM
|
||||
@@ -1309,10 +1349,10 @@ void Nextion::check_pending_waveform_() {
|
||||
char command[24]; // "addt " + uint8 + "," + uint8 + "," + uint8 + null = max 17 chars
|
||||
buf_append_printf(command, sizeof(command), 0, "addt %u,%u,%zu", component->get_component_id(),
|
||||
component->get_wave_channel_id(), buffer_to_send);
|
||||
if (!this->send_command_(command)) {
|
||||
delete nb; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
this->waveform_queue_.pop();
|
||||
}
|
||||
// If spacing or setup state blocks the send, leave the entry at the front
|
||||
// of waveform_queue_ for retry on the next loop iteration via
|
||||
// check_pending_waveform_(). Only pop on a successful send.
|
||||
this->send_command_(command);
|
||||
}
|
||||
#endif // USE_NEXTION_WAVEFORM
|
||||
|
||||
|
||||
@@ -55,15 +55,20 @@ class NextionCommandPacer {
|
||||
uint8_t get_spacing() const { return spacing_ms_; }
|
||||
|
||||
/**
|
||||
* @brief Check if enough time has passed to send next command
|
||||
* @return true if enough time has passed since last command
|
||||
* @brief Check if enough time has passed to send the next command.
|
||||
* @param now Current timestamp in milliseconds (use App.get_loop_component_start_time()
|
||||
* for consistency with the rest of the queue timing).
|
||||
* @return true if the spacing interval has elapsed since the last command was sent.
|
||||
*/
|
||||
bool can_send() const { return (millis() - last_command_time_) >= spacing_ms_; }
|
||||
bool can_send(uint32_t now) const { return (now - last_command_time_) >= spacing_ms_; }
|
||||
|
||||
/**
|
||||
* @brief Mark a command as sent, updating the timing
|
||||
* @brief Record the transmit timestamp for the most recently sent command.
|
||||
* @param now Current timestamp in milliseconds, as returned by
|
||||
* App.get_loop_component_start_time(). Must use the same clock
|
||||
* source as can_send() to avoid unsigned underflow.
|
||||
*/
|
||||
void mark_sent() { last_command_time_ = millis(); }
|
||||
void mark_sent(uint32_t now) { last_command_time_ = now; }
|
||||
|
||||
private:
|
||||
uint8_t spacing_ms_;
|
||||
|
||||
@@ -45,6 +45,18 @@ def is_remote_package(package_config: dict) -> bool:
|
||||
return CONF_URL in package_config
|
||||
|
||||
|
||||
def is_package_definition(value: object) -> bool:
|
||||
"""Returns True if the value looks like a package definition rather than a config fragment.
|
||||
|
||||
Package definitions are IncludeFile objects, git URL shorthand strings, or
|
||||
remote package dicts (containing a ``url:`` key). Config fragments are
|
||||
plain dicts that represent component configuration.
|
||||
"""
|
||||
return isinstance(value, (yaml_util.IncludeFile, str)) or (
|
||||
isinstance(value, dict) and is_remote_package(value)
|
||||
)
|
||||
|
||||
|
||||
def valid_package_contents(package_config: dict) -> dict:
|
||||
"""Validate that a package looks like a plausible ESPHome config fragment.
|
||||
|
||||
@@ -309,20 +321,23 @@ def _walk_packages(
|
||||
return config
|
||||
packages = config[CONF_PACKAGES]
|
||||
|
||||
if not isinstance(packages, (dict, list)):
|
||||
raise cv.Invalid(
|
||||
f"Packages must be a key to value mapping or list, got {type(packages)} instead"
|
||||
)
|
||||
|
||||
with cv.prepend_path(CONF_PACKAGES):
|
||||
if isinstance(packages, yaml_util.IncludeFile):
|
||||
# If the packages key is an IncludeFile, resolve it first before processing.
|
||||
packages, _ = resolve_include(packages, [], context, strict_undefined=False)
|
||||
if not isinstance(packages, (dict, list)):
|
||||
raise cv.Invalid(
|
||||
f"Packages must be a key to value mapping or list, got {type(packages)} instead"
|
||||
)
|
||||
|
||||
if not isinstance(packages, dict):
|
||||
_walk_package_list(packages, callback, context)
|
||||
elif (result := _walk_package_dict(packages, callback, context)) is not None:
|
||||
if not validate_deprecated:
|
||||
if not validate_deprecated or any(
|
||||
is_package_definition(v) for v in packages.values()
|
||||
):
|
||||
raise result
|
||||
# Fallback: treat the dict as a single deprecated package.
|
||||
# Note: this catches *any* cv.Invalid from the callback, which may
|
||||
# mask real validation errors in named package dicts.
|
||||
# This block can be removed once the single-package
|
||||
# deprecation period (2026.7.0) is over.
|
||||
config[CONF_PACKAGES] = [packages]
|
||||
@@ -461,6 +476,9 @@ class _PackageProcessor:
|
||||
self, package_config: dict | str, context_vars: ContextVars | None
|
||||
) -> dict:
|
||||
"""Resolve a single package and recurse into any nested packages."""
|
||||
from_remote = isinstance(package_config, dict) and is_remote_package(
|
||||
package_config
|
||||
)
|
||||
package_config = self.resolve_package(package_config, context_vars)
|
||||
self.collect_substitutions(package_config)
|
||||
|
||||
@@ -470,7 +488,18 @@ class _PackageProcessor:
|
||||
# Push context from !include vars on the package root and on the packages key
|
||||
context_vars = push_context(package_config, context_vars)
|
||||
context_vars = push_context(package_config[CONF_PACKAGES], context_vars)
|
||||
return _walk_packages(package_config, self.process_package, context_vars)
|
||||
# Disable the deprecated single-package fallback for remote
|
||||
# packages. _process_remote_package returns dicts with
|
||||
# already-resolved values that is_package_definition cannot
|
||||
# distinguish from config fragments, so the fallback would
|
||||
# always fire and mask real errors with wrong paths
|
||||
# (packages->0 instead of packages-><name>).
|
||||
return _walk_packages(
|
||||
package_config,
|
||||
self.process_package,
|
||||
context_vars,
|
||||
validate_deprecated=not from_remote,
|
||||
)
|
||||
|
||||
|
||||
def do_packages_pass(
|
||||
|
||||
@@ -44,7 +44,7 @@ void PCF85063Component::read_time() {
|
||||
.year = uint16_t(pcf85063_.reg.year + 10u * pcf85063_.reg.year_10 + 2000),
|
||||
};
|
||||
rtc_time.recalc_timestamp_utc(false);
|
||||
if (!rtc_time.is_valid()) {
|
||||
if (!rtc_time.is_valid(/*check_day_of_week=*/true, /*check_day_of_year=*/false)) {
|
||||
ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ void PCF8563Component::read_time() {
|
||||
.year = uint16_t(pcf8563_.reg.year + 10u * pcf8563_.reg.year_10 + 2000),
|
||||
};
|
||||
rtc_time.recalc_timestamp_utc(false);
|
||||
if (!rtc_time.is_valid()) {
|
||||
if (!rtc_time.is_valid(/*check_day_of_week=*/true, /*check_day_of_year=*/false)) {
|
||||
ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ void QMC5883LComponent::update() {
|
||||
// ROL_PNT in setup and reading 7 bytes starting at the status register.
|
||||
// If status and all three axes are desired, using ROL_PNT saves you 3 bytes.
|
||||
// But simply not reading status saves you 4 bytes always and is much simpler.
|
||||
if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG) {
|
||||
if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE) {
|
||||
err = this->read_register(QMC5883L_REGISTER_STATUS, &status, 1);
|
||||
if (err != i2c::ERROR_OK) {
|
||||
char buf[32];
|
||||
@@ -165,7 +165,7 @@ void QMC5883LComponent::update() {
|
||||
temp = int16_t(raw_temp) * 0.01f;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Got x=%0.02fµT y=%0.02fµT z=%0.02fµT heading=%0.01f° temperature=%0.01f°C status=%u", x, y, z, heading,
|
||||
ESP_LOGV(TAG, "Got x=%0.02fµT y=%0.02fµT z=%0.02fµT heading=%0.01f° temperature=%0.01f°C status=%u", x, y, z, heading,
|
||||
temp, status);
|
||||
|
||||
if (this->x_sensor_ != nullptr)
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#include <WiFi.h>
|
||||
#include <pico/cyw43_arch.h> // For cyw43_arch_lwip_begin/end (LwIPLock)
|
||||
#elif defined(USE_ETHERNET)
|
||||
#include <LwipEthernet.h> // For ethernet_arch_lwip_begin/end (LwIPLock)
|
||||
#include <lwip_wrap.h> // For LWIPMutex — LwIPLock mirrors its semantics (see below)
|
||||
#include "esphome/components/ethernet/ethernet_component.h"
|
||||
#endif
|
||||
#include <hardware/structs/rosc.h>
|
||||
@@ -43,9 +43,18 @@ IRAM_ATTR InterruptLock::~InterruptLock() { restore_interrupts(state_); }
|
||||
// main loop, corrupting the shared rx_buf_ pbuf chain (use-after-free, pbuf_cat
|
||||
// assertion failures). See esphome#10681.
|
||||
//
|
||||
// WiFi uses cyw43_arch_lwip_begin/end; Ethernet uses ethernet_arch_lwip_begin/end.
|
||||
// Both acquire the async_context recursive mutex to prevent IRQ callbacks from
|
||||
// firing during critical sections.
|
||||
// WiFi uses cyw43_arch_lwip_begin/end.
|
||||
//
|
||||
// For wired Ethernet, taking only the async_context lock is NOT enough. The
|
||||
// W5500 GPIO IRQ path (LwipIntfDev::_irq) checks arduino-pico's `__inLWIP`
|
||||
// counter to decide whether to defer packet processing. If we hold the
|
||||
// async_context lock without bumping `__inLWIP`, an interrupt-driven packet
|
||||
// arrival re-enters lwIP from IRQ context and corrupts pbufs (the `pbuf_cat`
|
||||
// assertion crash on wiznet-w5500-evb-pico). We mirror arduino-pico's
|
||||
// LWIPMutex (cores/rp2040/lwip_wrap.h) exactly: bump `__inLWIP`, take the
|
||||
// lock, and on release re-unmask any GPIO IRQs that were deferred while we
|
||||
// held it. We can't `using LwIPLock = LWIPMutex;` in helpers.h because
|
||||
// pulling lwip_wrap.h there poisons many TUs with lwIP types.
|
||||
//
|
||||
// When neither WiFi nor Ethernet is configured, this is a no-op since
|
||||
// there's no network stack and no lwip callbacks to race with.
|
||||
@@ -53,8 +62,18 @@ IRAM_ATTR InterruptLock::~InterruptLock() { restore_interrupts(state_); }
|
||||
LwIPLock::LwIPLock() { cyw43_arch_lwip_begin(); }
|
||||
LwIPLock::~LwIPLock() { cyw43_arch_lwip_end(); }
|
||||
#elif defined(USE_ETHERNET)
|
||||
LwIPLock::LwIPLock() { ethernet_arch_lwip_begin(); }
|
||||
LwIPLock::~LwIPLock() { ethernet_arch_lwip_end(); }
|
||||
LwIPLock::LwIPLock() {
|
||||
__inLWIP++;
|
||||
ethernet_arch_lwip_begin();
|
||||
}
|
||||
LwIPLock::~LwIPLock() {
|
||||
ethernet_arch_lwip_end();
|
||||
__inLWIP--;
|
||||
if (__needsIRQEN && !__inLWIP) {
|
||||
__needsIRQEN = false;
|
||||
ethernet_arch_lwip_gpio_unmask();
|
||||
}
|
||||
}
|
||||
#else
|
||||
LwIPLock::LwIPLock() {}
|
||||
LwIPLock::~LwIPLock() {}
|
||||
|
||||
@@ -65,3 +65,8 @@ async def to_code(config):
|
||||
@pins.PIN_SCHEMA_REGISTRY.register("rtl87xx", PIN_SCHEMA)
|
||||
async def pin_to_code(config):
|
||||
return await libretiny.gpio.component_pin_to_code(config)
|
||||
|
||||
|
||||
# Called by writer.py; delegates to the shared libretiny implementation.
|
||||
def copy_files() -> None:
|
||||
libretiny.copy_files()
|
||||
|
||||
@@ -32,40 +32,101 @@ void RuntimeStatsCollector::log_stats_() {
|
||||
" Period stats (last %" PRIu32 "ms): %zu active components",
|
||||
this->log_interval_, count);
|
||||
|
||||
if (count == 0) {
|
||||
return;
|
||||
// Sum component time so we can derive main-loop overhead
|
||||
// (active loop time minus time attributable to component loop()s).
|
||||
// Period sum iterates the active-in-period subset; total sum must iterate
|
||||
// all components since total_active_time_us_ includes iterations where
|
||||
// currently-idle components previously ran.
|
||||
uint64_t period_component_sum_us = 0;
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
period_component_sum_us += sorted[i]->runtime_stats_.period_time_us;
|
||||
}
|
||||
uint64_t total_component_sum_us = 0;
|
||||
for (auto *component : components) {
|
||||
total_component_sum_us += component->runtime_stats_.total_time_us;
|
||||
}
|
||||
|
||||
// Sort by period runtime (descending)
|
||||
std::sort(sorted, sorted + count, compare_period_time);
|
||||
if (count > 0) {
|
||||
// Sort by period runtime (descending)
|
||||
std::sort(sorted, sorted + count, compare_period_time);
|
||||
|
||||
// Log top components by period runtime
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
const auto &stats = sorted[i]->runtime_stats_;
|
||||
ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.3fms, max=%.2fms, total=%.1fms",
|
||||
LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.period_count,
|
||||
stats.period_count > 0 ? stats.period_time_us / (float) stats.period_count / 1000.0f : 0.0f,
|
||||
stats.period_max_time_us / 1000.0f, stats.period_time_us / 1000.0f);
|
||||
// Log top components by period runtime
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
const auto &stats = sorted[i]->runtime_stats_;
|
||||
ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.3fms, max=%.2fms, total=%.1fms",
|
||||
LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.period_count,
|
||||
stats.period_count > 0 ? stats.period_time_us / (float) stats.period_count / 1000.0f : 0.0f,
|
||||
stats.period_max_time_us / 1000.0f, stats.period_time_us / 1000.0f);
|
||||
}
|
||||
}
|
||||
|
||||
// Main-loop overhead for the period: active wall time minus component time.
|
||||
// active = sum of per-iteration loop time excluding yield/sleep.
|
||||
if (this->period_active_count_ > 0) {
|
||||
uint64_t active = this->period_active_time_us_;
|
||||
uint64_t overhead = active > period_component_sum_us ? active - period_component_sum_us : 0;
|
||||
// Use double for µs→ms conversion so multi-day uptimes (where total
|
||||
// microsecond counters exceed float's ~7-digit mantissa) keep resolution.
|
||||
ESP_LOGI(TAG,
|
||||
" main_loop: iters=%" PRIu64 ", active_avg=%.3fms, active_max=%.2fms, active_total=%.1fms, "
|
||||
"overhead_total=%.1fms",
|
||||
this->period_active_count_,
|
||||
static_cast<double>(active) / static_cast<double>(this->period_active_count_) / 1000.0,
|
||||
static_cast<double>(this->period_active_max_us_) / 1000.0, static_cast<double>(active) / 1000.0,
|
||||
static_cast<double>(overhead) / 1000.0);
|
||||
uint64_t before = this->period_before_time_us_;
|
||||
uint64_t tail = this->period_tail_time_us_;
|
||||
uint64_t accounted = before + tail;
|
||||
uint64_t inter = overhead > accounted ? overhead - accounted : 0;
|
||||
ESP_LOGI(TAG, " main_loop_overhead_section: before=%.1fms, tail=%.1fms, inter_component=%.1fms",
|
||||
static_cast<double>(before) / 1000.0, static_cast<double>(tail) / 1000.0,
|
||||
static_cast<double>(inter) / 1000.0);
|
||||
}
|
||||
|
||||
// Log total stats since boot (only for active components - idle ones haven't changed)
|
||||
ESP_LOGI(TAG, " Total stats (since boot): %zu active components", count);
|
||||
|
||||
// Re-sort by total runtime for all-time stats
|
||||
std::sort(sorted, sorted + count, compare_total_time);
|
||||
if (count > 0) {
|
||||
// Re-sort by total runtime for all-time stats
|
||||
std::sort(sorted, sorted + count, compare_total_time);
|
||||
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
const auto &stats = sorted[i]->runtime_stats_;
|
||||
ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.3fms, max=%.2fms, total=%.1fms",
|
||||
LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.total_count,
|
||||
stats.total_count > 0 ? stats.total_time_us / (float) stats.total_count / 1000.0f : 0.0f,
|
||||
stats.total_max_time_us / 1000.0f, stats.total_time_us / 1000.0);
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
const auto &stats = sorted[i]->runtime_stats_;
|
||||
ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.3fms, max=%.2fms, total=%.1fms",
|
||||
LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.total_count,
|
||||
stats.total_count > 0 ? stats.total_time_us / (float) stats.total_count / 1000.0f : 0.0f,
|
||||
stats.total_max_time_us / 1000.0f, stats.total_time_us / 1000.0);
|
||||
}
|
||||
}
|
||||
|
||||
if (this->total_active_count_ > 0) {
|
||||
uint64_t active = this->total_active_time_us_;
|
||||
uint64_t overhead = active > total_component_sum_us ? active - total_component_sum_us : 0;
|
||||
ESP_LOGI(TAG,
|
||||
" main_loop: iters=%" PRIu64 ", active_avg=%.3fms, active_max=%.2fms, active_total=%.1fms, "
|
||||
"overhead_total=%.1fms",
|
||||
this->total_active_count_,
|
||||
static_cast<double>(active) / static_cast<double>(this->total_active_count_) / 1000.0,
|
||||
static_cast<double>(this->total_active_max_us_) / 1000.0, static_cast<double>(active) / 1000.0,
|
||||
static_cast<double>(overhead) / 1000.0);
|
||||
uint64_t before = this->total_before_time_us_;
|
||||
uint64_t tail = this->total_tail_time_us_;
|
||||
uint64_t accounted = before + tail;
|
||||
uint64_t inter = overhead > accounted ? overhead - accounted : 0;
|
||||
ESP_LOGI(TAG, " main_loop_overhead_section: before=%.1fms, tail=%.1fms, inter_component=%.1fms",
|
||||
static_cast<double>(before) / 1000.0, static_cast<double>(tail) / 1000.0,
|
||||
static_cast<double>(inter) / 1000.0);
|
||||
}
|
||||
|
||||
// Reset period stats
|
||||
for (auto *component : components) {
|
||||
component->runtime_stats_.reset_period();
|
||||
}
|
||||
this->period_active_count_ = 0;
|
||||
this->period_active_time_us_ = 0;
|
||||
this->period_active_max_us_ = 0;
|
||||
this->period_before_time_us_ = 0;
|
||||
this->period_tail_time_us_ = 0;
|
||||
}
|
||||
|
||||
bool RuntimeStatsCollector::compare_period_time(Component *a, Component *b) {
|
||||
|
||||
@@ -29,6 +29,31 @@ class RuntimeStatsCollector {
|
||||
// Process any pending stats printing (should be called after component loop)
|
||||
void process_pending_stats(uint32_t current_time);
|
||||
|
||||
// Record the wall time of one main loop iteration excluding the yield/sleep.
|
||||
// Called once per loop from Application::loop().
|
||||
// active_us = total time between loop start and just before yield.
|
||||
// before_us = time spent in before_loop_tasks_ (scheduler + ISR enable_loop).
|
||||
// tail_us = time spent in after_loop_tasks_ + the trailing record/stats prefix.
|
||||
// Residual overhead at log time = active − Σ(component) − before − tail,
|
||||
// which captures per-iteration inter-component bookkeeping (set_current_component,
|
||||
// WarnIfComponentBlockingGuard construction/destruction, feed_wdt_with_time calls,
|
||||
// the for-loop itself).
|
||||
void record_loop_active(uint32_t active_us, uint32_t before_us, uint32_t tail_us) {
|
||||
this->period_active_count_++;
|
||||
this->period_active_time_us_ += active_us;
|
||||
if (active_us > this->period_active_max_us_)
|
||||
this->period_active_max_us_ = active_us;
|
||||
this->total_active_count_++;
|
||||
this->total_active_time_us_ += active_us;
|
||||
if (active_us > this->total_active_max_us_)
|
||||
this->total_active_max_us_ = active_us;
|
||||
|
||||
this->period_before_time_us_ += before_us;
|
||||
this->total_before_time_us_ += before_us;
|
||||
this->period_tail_time_us_ += tail_us;
|
||||
this->total_tail_time_us_ += tail_us;
|
||||
}
|
||||
|
||||
protected:
|
||||
void log_stats_();
|
||||
// Static comparators — member functions have friend access, lambdas do not
|
||||
@@ -37,6 +62,22 @@ class RuntimeStatsCollector {
|
||||
|
||||
uint32_t log_interval_;
|
||||
uint32_t next_log_time_{0};
|
||||
|
||||
// Main loop active-time stats (wall time per iteration, excluding yield/sleep).
|
||||
// Counters are uint64_t — at sub-millisecond loop times a uint32_t can wrap in
|
||||
// a few weeks of uptime, which is well within ESPHome device lifetimes.
|
||||
uint64_t period_active_count_{0};
|
||||
uint64_t period_active_time_us_{0};
|
||||
uint32_t period_active_max_us_{0};
|
||||
uint64_t total_active_count_{0};
|
||||
uint64_t total_active_time_us_{0};
|
||||
uint32_t total_active_max_us_{0};
|
||||
|
||||
// Split of overhead sections — accumulated per iteration.
|
||||
uint64_t period_before_time_us_{0};
|
||||
uint64_t total_before_time_us_{0};
|
||||
uint64_t period_tail_time_us_{0};
|
||||
uint64_t total_tail_time_us_{0};
|
||||
};
|
||||
|
||||
} // namespace runtime_stats
|
||||
|
||||
@@ -81,7 +81,7 @@ void RX8130Component::read_time() {
|
||||
.year = static_cast<uint16_t>(bcd2dec(date[6]) + 2000),
|
||||
};
|
||||
rtc_time.recalc_timestamp_utc(false);
|
||||
if (!rtc_time.is_valid()) {
|
||||
if (!rtc_time.is_valid(/*check_day_of_week=*/true, /*check_day_of_year=*/false)) {
|
||||
ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -87,7 +87,10 @@ async def to_code(config):
|
||||
config[CONF_REBOOT_TIMEOUT],
|
||||
config[CONF_BOOT_IS_GOOD_AFTER],
|
||||
)
|
||||
cg.add(RawExpression(f"if ({condition}) return"))
|
||||
# Wrap in IIFEUnsafeStatement so cpp_main_section emits this
|
||||
# component's block flat rather than inside an IIFE lambda —
|
||||
# the `return` must exit setup() itself, not just the lambda.
|
||||
cg.add(cg.IIFEUnsafeStatement(RawExpression(f"if ({condition}) return")))
|
||||
|
||||
CORE.data[CONF_SAFE_MODE] = {}
|
||||
CORE.data[CONF_SAFE_MODE][KEY_PAST_SAFE_MODE] = True
|
||||
|
||||
@@ -84,7 +84,7 @@ def get_firmware(value):
|
||||
req = requests.get(url, timeout=30)
|
||||
req.raise_for_status()
|
||||
except requests.exceptions.RequestException as e:
|
||||
raise cv.Invalid(f"Could not download firmware file ({url}): {e}")
|
||||
raise cv.Invalid(f"Could not download firmware file ({url}): {e}") from e
|
||||
|
||||
h = hashlib.new("sha256")
|
||||
h.update(req.content)
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
#include "esphome/core/wake.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef USE_OTA_PLATFORM_ESPHOME
|
||||
extern "C" void esphome_wake_ota_component_any_context();
|
||||
#endif
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
#include <coredecls.h> // For esp_schedule()
|
||||
#elif defined(USE_RP2040)
|
||||
@@ -854,6 +858,10 @@ err_t LWIPRawListenImpl::accept_fn_(struct tcp_pcb *newpcb, err_t err) {
|
||||
tcp_err(newpcb, LWIPRawListenImpl::s_queued_err_fn);
|
||||
tcp_recv(newpcb, LWIPRawListenImpl::s_queued_recv_fn);
|
||||
LWIP_LOG("Accepted connection, queue size: %d", this->accepted_socket_count_);
|
||||
#ifdef USE_OTA_PLATFORM_ESPHOME
|
||||
// Must run before wake_loop_any_context() so flags are visible when the main task wakes.
|
||||
esphome_wake_ota_component_any_context();
|
||||
#endif
|
||||
// Wake the main loop immediately so it can accept the new connection.
|
||||
esphome::wake_loop_any_context();
|
||||
return ERR_OK;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user