mirror of
https://github.com/esphome/esphome.git
synced 2025-11-03 08:31:47 +00:00
Compare commits
173 Commits
fix_clang_
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e792d0791 | ||
|
|
42833c85f5 | ||
|
|
a41c7b2b3c | ||
|
|
4dd3c90663 | ||
|
|
0f0cd1f706 | ||
|
|
4a5e6576c8 | ||
|
|
cf76c3a747 | ||
|
|
3f05fd82e5 | ||
|
|
34244afea1 | ||
|
|
4838eff382 | ||
|
|
712421b82b | ||
|
|
7a1297ec84 | ||
|
|
40f919eaa6 | ||
|
|
01ae86145a | ||
|
|
17ab20ef61 | ||
|
|
1509ed8d23 | ||
|
|
3e17767f6a | ||
|
|
19e275dc02 | ||
|
|
86402be9e3 | ||
|
|
8a8a80e107 | ||
|
|
79378a930e | ||
|
|
c822ec152f | ||
|
|
50e7ce55e7 | ||
|
|
70ea3af578 | ||
|
|
338190abec | ||
|
|
425c88ee94 | ||
|
|
f6946c0b9a | ||
|
|
edde2fc94c | ||
|
|
1fc3165b58 | ||
|
|
d25121a55c | ||
|
|
55af818629 | ||
|
|
c662697ca7 | ||
|
|
e28c152298 | ||
|
|
0b4d445794 | ||
|
|
4d1d37a911 | ||
|
|
8df5a3a630 | ||
|
|
5a5894eaa3 | ||
|
|
d9d2d2f6b9 | ||
|
|
30f2a4395f | ||
|
|
292abd1187 | ||
|
|
6d0527ff2a | ||
|
|
fd64585f99 | ||
|
|
077cce9848 | ||
|
|
bd87e56bc7 | ||
|
|
58235049e3 | ||
|
|
29ed3c20af | ||
|
|
08aae39ea4 | ||
|
|
03fd114371 | ||
|
|
918650f15a | ||
|
|
287f65cbaf | ||
|
|
f18c70a256 | ||
|
|
6fb490f49b | ||
|
|
66cf7c3a3b | ||
|
|
f29021b5ef | ||
|
|
7549ca4d39 | ||
|
|
33e7a2101b | ||
|
|
59a216bfcb | ||
|
|
09d89000ad | ||
|
|
b6c9ece0e6 | ||
|
|
7169556722 | ||
|
|
f6e4c0cb52 | ||
|
|
f3634edc22 | ||
|
|
a609343cb6 | ||
|
|
5528c3c765 | ||
|
|
0d805355f5 | ||
|
|
99f48ae51c | ||
|
|
25e4aafd71 | ||
|
|
4f2d54be4e | ||
|
|
249cd7415b | ||
|
|
78d780105b | ||
|
|
466d4522bc | ||
|
|
e462217500 | ||
|
|
f1bce262ed | ||
|
|
7ed7e7ad26 | ||
|
|
08b8454555 | ||
|
|
0119e17f04 | ||
|
|
c3f40de844 | ||
|
|
7dd829cfca | ||
|
|
da19673f51 | ||
|
|
f5e32d03d0 | ||
|
|
f3b69383fd | ||
|
|
aba72809d3 | ||
|
|
85205a28d2 | ||
|
|
285e006637 | ||
|
|
5647f36900 | ||
|
|
1e9309ffff | ||
|
|
ce8a6a6c43 | ||
|
|
dfb4b31bf9 | ||
|
|
31b1b50ad9 | ||
|
|
3377080272 | ||
|
|
d65ad69338 | ||
|
|
dfa69173ea | ||
|
|
f44615cc8d | ||
|
|
bda4769bd3 | ||
|
|
14b057f54e | ||
|
|
e26b5874d7 | ||
|
|
00f22e5c36 | ||
|
|
51e080c2d3 | ||
|
|
3c18558003 | ||
|
|
7394cbf773 | ||
|
|
1577a46efd | ||
|
|
e212ed024d | ||
|
|
5fdd90c71a | ||
|
|
6929bdb415 | ||
|
|
2c85ba037e | ||
|
|
2440bbdceb | ||
|
|
3ac8eb7696 | ||
|
|
6a478b9070 | ||
|
|
a32a1d11fb | ||
|
|
daeb8ef88c | ||
|
|
febee437d6 | ||
|
|
de2f475dbd | ||
|
|
fa3ec6f732 | ||
|
|
e490aec6b4 | ||
|
|
8da8095a6a | ||
|
|
ab14c0cd72 | ||
|
|
917deac7cb | ||
|
|
3d21adecd3 | ||
|
|
5b023f9369 | ||
|
|
6c2ce5cacf | ||
|
|
d23e25f099 | ||
|
|
9b78098eec | ||
|
|
7e5b82c5f3 | ||
|
|
2864e989bd | ||
|
|
6efe346cc5 | ||
|
|
f2f6c597ef | ||
|
|
b91b12d77a | ||
|
|
7f567bdfbe | ||
|
|
f2de8df556 | ||
|
|
1c67a61945 | ||
|
|
77141d3e83 | ||
|
|
f592f79bce | ||
|
|
6edbb94529 | ||
|
|
d37eb59fd7 | ||
|
|
e2b3617df3 | ||
|
|
e1c851cab8 | ||
|
|
146b067d62 | ||
|
|
5b15827009 | ||
|
|
0de79ba291 | ||
|
|
e3aaf6a144 | ||
|
|
78ffeb30fb | ||
|
|
2c1927fd12 | ||
|
|
c6ae1a5909 | ||
|
|
9c712744be | ||
|
|
ae50a09b4e | ||
|
|
1ea80594c6 | ||
|
|
8500323d39 | ||
|
|
6f7db2f5f7 | ||
|
|
9922c65912 | ||
|
|
f2469077d9 | ||
|
|
742eca92d8 | ||
|
|
548913b471 | ||
|
|
a05c5ea240 | ||
|
|
8e8a2bde95 | ||
|
|
80265a6bd2 | ||
|
|
87e9a7a1bd | ||
|
|
3aedfe8be3 | ||
|
|
7f2cc47ed6 | ||
|
|
a5542e0d2b | ||
|
|
66afe4a9be | ||
|
|
0ae9009e41 | ||
|
|
0b2f5fcd7e | ||
|
|
7a2887e2ed | ||
|
|
cd2d3f061d | ||
|
|
73f5d01c2d | ||
|
|
0938609f7a | ||
|
|
77203f0cb4 | ||
|
|
040130e357 | ||
|
|
85959e3004 | ||
|
|
a809a13729 | ||
|
|
3b6ff615e8 | ||
|
|
05216db5f0 | ||
|
|
9f668b0c4b |
@@ -1 +1 @@
|
||||
d7693a1e996cacd4a3d1c9a16336799c2a8cc3db02e4e74084151ce964581248
|
||||
3d46b63015d761c85ca9cb77ab79a389509e5776701fb22aed16e7b79d432c0c
|
||||
|
||||
22
.github/workflows/auto-label-pr.yml
vendored
22
.github/workflows/auto-label-pr.yml
vendored
@@ -53,6 +53,7 @@ jobs:
|
||||
'new-target-platform',
|
||||
'merging-to-release',
|
||||
'merging-to-beta',
|
||||
'chained-pr',
|
||||
'core',
|
||||
'small-pr',
|
||||
'dashboard',
|
||||
@@ -140,6 +141,8 @@ jobs:
|
||||
labels.add('merging-to-release');
|
||||
} else if (baseRef === 'beta') {
|
||||
labels.add('merging-to-beta');
|
||||
} else if (baseRef !== 'dev') {
|
||||
labels.add('chained-pr');
|
||||
}
|
||||
|
||||
return labels;
|
||||
@@ -413,7 +416,7 @@ jobs:
|
||||
}
|
||||
|
||||
// Generate review messages
|
||||
function generateReviewMessages(finalLabels) {
|
||||
function generateReviewMessages(finalLabels, originalLabelCount) {
|
||||
const messages = [];
|
||||
const prAuthor = context.payload.pull_request.user.login;
|
||||
|
||||
@@ -427,15 +430,15 @@ jobs:
|
||||
.reduce((sum, file) => sum + (file.deletions || 0), 0);
|
||||
const nonTestChanges = (totalAdditions - testAdditions) - (totalDeletions - testDeletions);
|
||||
|
||||
const tooManyLabels = finalLabels.length > MAX_LABELS;
|
||||
const tooManyLabels = originalLabelCount > MAX_LABELS;
|
||||
const tooManyChanges = nonTestChanges > TOO_BIG_THRESHOLD;
|
||||
|
||||
let message = `${TOO_BIG_MARKER}\n### 📦 Pull Request Size\n\n`;
|
||||
|
||||
if (tooManyLabels && tooManyChanges) {
|
||||
message += `This PR is too large with ${nonTestChanges} line changes (excluding tests) and affects ${finalLabels.length} different components/areas.`;
|
||||
message += `This PR is too large with ${nonTestChanges} line changes (excluding tests) and affects ${originalLabelCount} different components/areas.`;
|
||||
} else if (tooManyLabels) {
|
||||
message += `This PR affects ${finalLabels.length} different components/areas.`;
|
||||
message += `This PR affects ${originalLabelCount} different components/areas.`;
|
||||
} else {
|
||||
message += `This PR is too large with ${nonTestChanges} line changes (excluding tests).`;
|
||||
}
|
||||
@@ -463,8 +466,8 @@ jobs:
|
||||
}
|
||||
|
||||
// Handle reviews
|
||||
async function handleReviews(finalLabels) {
|
||||
const reviewMessages = generateReviewMessages(finalLabels);
|
||||
async function handleReviews(finalLabels, originalLabelCount) {
|
||||
const reviewMessages = generateReviewMessages(finalLabels, originalLabelCount);
|
||||
const hasReviewableLabels = finalLabels.some(label =>
|
||||
['too-big', 'needs-codeowners'].includes(label)
|
||||
);
|
||||
@@ -528,8 +531,8 @@ jobs:
|
||||
const apiData = await fetchApiData();
|
||||
const baseRef = context.payload.pull_request.base.ref;
|
||||
|
||||
// Early exit for non-dev branches
|
||||
if (baseRef !== 'dev') {
|
||||
// Early exit for release and beta branches only
|
||||
if (baseRef === 'release' || baseRef === 'beta') {
|
||||
const branchLabels = await detectMergeBranch();
|
||||
const finalLabels = Array.from(branchLabels);
|
||||
|
||||
@@ -624,6 +627,7 @@ jobs:
|
||||
|
||||
// Handle too many labels (only for non-mega PRs)
|
||||
const tooManyLabels = finalLabels.length > MAX_LABELS;
|
||||
const originalLabelCount = finalLabels.length;
|
||||
|
||||
if (tooManyLabels && !isMegaPR && !finalLabels.includes('too-big')) {
|
||||
finalLabels = ['too-big'];
|
||||
@@ -632,7 +636,7 @@ jobs:
|
||||
console.log('Computed labels:', finalLabels.join(', '));
|
||||
|
||||
// Handle reviews
|
||||
await handleReviews(finalLabels);
|
||||
await handleReviews(finalLabels, originalLabelCount);
|
||||
|
||||
// Apply labels
|
||||
if (finalLabels.length > 0) {
|
||||
|
||||
2
.github/workflows/ci-api-proto.yml
vendored
2
.github/workflows/ci-api-proto.yml
vendored
@@ -62,7 +62,7 @@ jobs:
|
||||
run: git diff
|
||||
- if: failure()
|
||||
name: Archive artifacts
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: generated-proto-files
|
||||
path: |
|
||||
|
||||
93
.github/workflows/ci.yml
vendored
93
.github/workflows/ci.yml
vendored
@@ -114,7 +114,7 @@ jobs:
|
||||
matrix:
|
||||
python-version:
|
||||
- "3.11"
|
||||
- "3.14"
|
||||
- "3.13"
|
||||
os:
|
||||
- ubuntu-latest
|
||||
- macOS-latest
|
||||
@@ -123,9 +123,9 @@ jobs:
|
||||
# Minimize CI resource usage
|
||||
# by only running the Python version
|
||||
# version used for docker images on Windows and macOS
|
||||
- python-version: "3.14"
|
||||
- python-version: "3.13"
|
||||
os: windows-latest
|
||||
- python-version: "3.14"
|
||||
- python-version: "3.13"
|
||||
os: macOS-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs:
|
||||
@@ -178,6 +178,9 @@ jobs:
|
||||
component-test-count: ${{ steps.determine.outputs.component-test-count }}
|
||||
changed-cpp-file-count: ${{ steps.determine.outputs.changed-cpp-file-count }}
|
||||
memory_impact: ${{ steps.determine.outputs.memory-impact }}
|
||||
cpp-unit-tests-run-all: ${{ steps.determine.outputs.cpp-unit-tests-run-all }}
|
||||
cpp-unit-tests-components: ${{ steps.determine.outputs.cpp-unit-tests-components }}
|
||||
component-test-batches: ${{ steps.determine.outputs.component-test-batches }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
@@ -210,6 +213,9 @@ jobs:
|
||||
echo "component-test-count=$(echo "$output" | jq -r '.component_test_count')" >> $GITHUB_OUTPUT
|
||||
echo "changed-cpp-file-count=$(echo "$output" | jq -r '.changed_cpp_file_count')" >> $GITHUB_OUTPUT
|
||||
echo "memory-impact=$(echo "$output" | jq -c '.memory_impact')" >> $GITHUB_OUTPUT
|
||||
echo "cpp-unit-tests-run-all=$(echo "$output" | jq -r '.cpp_unit_tests_run_all')" >> $GITHUB_OUTPUT
|
||||
echo "cpp-unit-tests-components=$(echo "$output" | jq -c '.cpp_unit_tests_components')" >> $GITHUB_OUTPUT
|
||||
echo "component-test-batches=$(echo "$output" | jq -c '.component_test_batches')" >> $GITHUB_OUTPUT
|
||||
|
||||
integration-tests:
|
||||
name: Run integration tests
|
||||
@@ -247,6 +253,33 @@ jobs:
|
||||
. venv/bin/activate
|
||||
pytest -vv --no-cov --tb=native -n auto tests/integration/
|
||||
|
||||
cpp-unit-tests:
|
||||
name: Run C++ unit tests
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- common
|
||||
- determine-jobs
|
||||
if: github.event_name == 'pull_request' && (needs.determine-jobs.outputs.cpp-unit-tests-run-all == 'true' || needs.determine-jobs.outputs.cpp-unit-tests-components != '[]')
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
|
||||
- name: Run cpp_unit_test.py
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
if [ "${{ needs.determine-jobs.outputs.cpp-unit-tests-run-all }}" = "true" ]; then
|
||||
script/cpp_unit_test.py --all
|
||||
else
|
||||
ARGS=$(echo '${{ needs.determine-jobs.outputs.cpp-unit-tests-components }}' | jq -r '.[] | @sh' | xargs)
|
||||
script/cpp_unit_test.py $ARGS
|
||||
fi
|
||||
|
||||
clang-tidy-single:
|
||||
name: ${{ matrix.name }}
|
||||
runs-on: ubuntu-24.04
|
||||
@@ -427,7 +460,7 @@ jobs:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 1
|
||||
max-parallel: 2
|
||||
matrix:
|
||||
include:
|
||||
- id: clang-tidy
|
||||
@@ -505,59 +538,18 @@ jobs:
|
||||
run: script/ci-suggest-changes
|
||||
if: always()
|
||||
|
||||
test-build-components-splitter:
|
||||
name: Split components for intelligent grouping (40 weighted per batch)
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- common
|
||||
- determine-jobs
|
||||
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0
|
||||
outputs:
|
||||
matrix: ${{ steps.split.outputs.components }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Split components intelligently based on bus configurations
|
||||
id: split
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
|
||||
# Use intelligent splitter that groups components with same bus configs
|
||||
components='${{ needs.determine-jobs.outputs.changed-components-with-tests }}'
|
||||
|
||||
# Only isolate directly changed components when targeting dev branch
|
||||
# For beta/release branches, group everything for faster CI
|
||||
if [[ "${{ github.base_ref }}" == beta* ]] || [[ "${{ github.base_ref }}" == release* ]]; then
|
||||
directly_changed='[]'
|
||||
echo "Target branch: ${{ github.base_ref }} - grouping all components"
|
||||
else
|
||||
directly_changed='${{ needs.determine-jobs.outputs.directly-changed-components-with-tests }}'
|
||||
echo "Target branch: ${{ github.base_ref }} - isolating directly changed components"
|
||||
fi
|
||||
|
||||
echo "Splitting components intelligently..."
|
||||
output=$(python3 script/split_components_for_ci.py --components "$components" --directly-changed "$directly_changed" --batch-size 40 --output github)
|
||||
|
||||
echo "$output" >> $GITHUB_OUTPUT
|
||||
|
||||
test-build-components-split:
|
||||
name: Test components batch (${{ matrix.components }})
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- common
|
||||
- determine-jobs
|
||||
- test-build-components-splitter
|
||||
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: ${{ (startsWith(github.base_ref, 'beta') || startsWith(github.base_ref, 'release')) && 8 || 4 }}
|
||||
matrix:
|
||||
components: ${{ fromJson(needs.test-build-components-splitter.outputs.matrix) }}
|
||||
components: ${{ fromJson(needs.determine-jobs.outputs.component-test-batches) }}
|
||||
steps:
|
||||
- name: Show disk space
|
||||
run: |
|
||||
@@ -818,7 +810,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Upload memory analysis JSON
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: memory-analysis-target
|
||||
path: memory-analysis-target.json
|
||||
@@ -882,7 +874,7 @@ jobs:
|
||||
--platform "$platform"
|
||||
|
||||
- name: Upload memory analysis JSON
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: memory-analysis-pr
|
||||
path: memory-analysis-pr.json
|
||||
@@ -912,13 +904,13 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Download target analysis JSON
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
name: memory-analysis-target
|
||||
path: ./memory-analysis
|
||||
continue-on-error: true
|
||||
- name: Download PR analysis JSON
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
name: memory-analysis-pr
|
||||
path: ./memory-analysis
|
||||
@@ -949,7 +941,6 @@ jobs:
|
||||
- clang-tidy-nosplit
|
||||
- clang-tidy-split
|
||||
- determine-jobs
|
||||
- test-build-components-splitter
|
||||
- test-build-components-split
|
||||
- pre-commit-ci-lite
|
||||
- memory-impact-target-branch
|
||||
|
||||
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@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
|
||||
uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.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@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
|
||||
uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -138,7 +138,7 @@ jobs:
|
||||
# version: ${{ needs.init.outputs.tag }}
|
||||
|
||||
- name: Upload digests
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: digests-${{ matrix.platform.arch }}
|
||||
path: /tmp/digests
|
||||
@@ -171,7 +171,7 @@ jobs:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
pattern: digests-*
|
||||
path: /tmp/digests
|
||||
|
||||
1
.github/workflows/status-check-labels.yml
vendored
1
.github/workflows/status-check-labels.yml
vendored
@@ -14,6 +14,7 @@ jobs:
|
||||
label:
|
||||
- needs-docs
|
||||
- merge-after-release
|
||||
- chained-pr
|
||||
steps:
|
||||
- name: Check for ${{ matrix.label }} label
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
|
||||
@@ -11,7 +11,7 @@ ci:
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.14.1
|
||||
rev: v0.14.3
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -201,6 +201,7 @@ esphome/components/havells_solar/* @sourabhjaiswal
|
||||
esphome/components/hbridge/fan/* @WeekendWarrior
|
||||
esphome/components/hbridge/light/* @DotNetDann
|
||||
esphome/components/hbridge/switch/* @dwmw2
|
||||
esphome/components/hdc2010/* @optimusprimespace @ssieb
|
||||
esphome/components/he60r/* @clydebarrow
|
||||
esphome/components/heatpumpir/* @rob-deutsch
|
||||
esphome/components/hitachi_ac424/* @sourabhjaiswal
|
||||
|
||||
@@ -207,14 +207,14 @@ def choose_upload_log_host(
|
||||
if has_mqtt_logging():
|
||||
resolved.append("MQTT")
|
||||
|
||||
if has_api() and has_non_ip_address():
|
||||
if has_api() and has_non_ip_address() and has_resolvable_address():
|
||||
resolved.extend(_resolve_with_cache(CORE.address, purpose))
|
||||
|
||||
elif purpose == Purpose.UPLOADING:
|
||||
if has_ota() and has_mqtt_ip_lookup():
|
||||
resolved.append("MQTTIP")
|
||||
|
||||
if has_ota() and has_non_ip_address():
|
||||
if has_ota() and has_non_ip_address() and has_resolvable_address():
|
||||
resolved.extend(_resolve_with_cache(CORE.address, purpose))
|
||||
else:
|
||||
resolved.append(device)
|
||||
@@ -318,7 +318,17 @@ def has_resolvable_address() -> bool:
|
||||
"""Check if CORE.address is resolvable (via mDNS, DNS, or is an IP address)."""
|
||||
# Any address (IP, mDNS hostname, or regular DNS hostname) is resolvable
|
||||
# The resolve_ip_address() function in helpers.py handles all types via AsyncResolver
|
||||
return CORE.address is not None
|
||||
if CORE.address is None:
|
||||
return False
|
||||
|
||||
if has_ip_address():
|
||||
return True
|
||||
|
||||
if has_mdns():
|
||||
return True
|
||||
|
||||
# .local mDNS hostnames are only resolvable if mDNS is enabled
|
||||
return not CORE.address.endswith(".local")
|
||||
|
||||
|
||||
def mqtt_get_ip(config: ConfigType, username: str, password: str, client_id: str):
|
||||
|
||||
@@ -231,9 +231,22 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
api_component = (name, mem)
|
||||
break
|
||||
|
||||
# Combine all components to analyze: top ESPHome + all external + API if not already included
|
||||
components_to_analyze = list(top_esphome_components) + list(
|
||||
top_external_components
|
||||
# Also include wifi_stack and other important system components if they exist
|
||||
system_components_to_include = [
|
||||
# Empty list - we've finished debugging symbol categorization
|
||||
# Add component names here if you need to debug their symbols
|
||||
]
|
||||
system_components = [
|
||||
(name, mem)
|
||||
for name, mem in components
|
||||
if name in system_components_to_include
|
||||
]
|
||||
|
||||
# Combine all components to analyze: top ESPHome + all external + API if not already included + system components
|
||||
components_to_analyze = (
|
||||
list(top_esphome_components)
|
||||
+ list(top_external_components)
|
||||
+ system_components
|
||||
)
|
||||
if api_component and api_component not in components_to_analyze:
|
||||
components_to_analyze.append(api_component)
|
||||
|
||||
@@ -127,40 +127,39 @@ SYMBOL_PATTERNS = {
|
||||
"tryget_socket_unconn",
|
||||
"cs_create_ctrl_sock",
|
||||
"netbuf_alloc",
|
||||
"tcp_", # TCP protocol functions
|
||||
"udp_", # UDP protocol functions
|
||||
"lwip_", # LwIP stack functions
|
||||
"eagle_lwip", # ESP-specific LwIP functions
|
||||
"new_linkoutput", # Link output function
|
||||
"acd_", # Address Conflict Detection (ACD)
|
||||
"eth_", # Ethernet functions
|
||||
"mac_enable_bb", # MAC baseband enable
|
||||
"reassemble_and_dispatch", # Packet reassembly
|
||||
],
|
||||
# dhcp must come before libc to avoid "dhcp_select" matching "select" pattern
|
||||
"dhcp": ["dhcp", "handle_dhcp"],
|
||||
"ipv6_stack": ["nd6_", "ip6_", "mld6_", "icmp6_", "icmp6_input"],
|
||||
"wifi_stack": [
|
||||
"ieee80211",
|
||||
"hostap",
|
||||
"sta_",
|
||||
"ap_",
|
||||
"scan_",
|
||||
"wifi_",
|
||||
"wpa_",
|
||||
"wps_",
|
||||
"esp_wifi",
|
||||
"cnx_",
|
||||
"wpa3_",
|
||||
"sae_",
|
||||
"wDev_",
|
||||
"ic_",
|
||||
"mac_",
|
||||
"esf_buf",
|
||||
"gWpaSm",
|
||||
"sm_WPA",
|
||||
"eapol_",
|
||||
"owe_",
|
||||
"wifiLowLevelInit",
|
||||
"s_do_mapping",
|
||||
"gScanStruct",
|
||||
"ppSearchTxframe",
|
||||
"ppMapWaitTxq",
|
||||
"ppFillAMPDUBar",
|
||||
"ppCheckTxConnTrafficIdle",
|
||||
"ppCalTkipMic",
|
||||
# Order matters! More specific categories must come before general ones.
|
||||
# mdns must come before bluetooth to avoid "_mdns_disable_pcb" matching "ble_" pattern
|
||||
"mdns_lib": ["mdns"],
|
||||
# memory_mgmt must come before wifi_stack to catch mmu_hal_* symbols
|
||||
"memory_mgmt": [
|
||||
"mem_",
|
||||
"memory_",
|
||||
"tlsf_",
|
||||
"memp_",
|
||||
"pbuf_",
|
||||
"pbuf_alloc",
|
||||
"pbuf_copy_partial_pbuf",
|
||||
"esp_mmu_map",
|
||||
"mmu_hal_",
|
||||
"s_do_mapping", # Memory mapping function, not WiFi
|
||||
"hash_map_", # Hash map data structure
|
||||
"umm_assimilate", # UMM malloc assimilation
|
||||
],
|
||||
"bluetooth": ["bt_", "ble_", "l2c_", "gatt_", "gap_", "hci_", "BT_init"],
|
||||
"wifi_bt_coex": ["coex"],
|
||||
# Bluetooth categories must come BEFORE wifi_stack to avoid misclassification
|
||||
# Many BLE symbols contain patterns like "ble_" that would otherwise match wifi patterns
|
||||
"bluetooth_rom": ["r_ble", "r_lld", "r_llc", "r_llm"],
|
||||
"bluedroid_bt": [
|
||||
"bluedroid",
|
||||
@@ -207,6 +206,61 @@ SYMBOL_PATTERNS = {
|
||||
"copy_extra_byte_in_db",
|
||||
"parse_read_local_supported_commands_response",
|
||||
],
|
||||
"bluetooth": [
|
||||
"bt_",
|
||||
"_ble_", # More specific than "ble_" to avoid matching "able_", "enable_", "disable_"
|
||||
"l2c_",
|
||||
"l2ble_", # L2CAP for BLE
|
||||
"gatt_",
|
||||
"gap_",
|
||||
"hci_",
|
||||
"btsnd_hcic_", # Bluetooth HCI command send functions
|
||||
"BT_init",
|
||||
"BT_tx_", # Bluetooth transmit functions
|
||||
"esp_ble_", # Catch esp_ble_* functions
|
||||
],
|
||||
"bluetooth_ll": [
|
||||
"llm_", # Link layer manager
|
||||
"llc_", # Link layer control
|
||||
"lld_", # Link layer driver
|
||||
"ld_acl_", # Link layer ACL (Asynchronous Connection-Oriented)
|
||||
"llcp_", # Link layer control protocol
|
||||
"lmp_", # Link manager protocol
|
||||
],
|
||||
"wifi_bt_coex": ["coex"],
|
||||
"wifi_stack": [
|
||||
"ieee80211",
|
||||
"hostap",
|
||||
"sta_",
|
||||
"wifi_ap_", # More specific than "ap_" to avoid matching "cap_", "map_"
|
||||
"wifi_scan_", # More specific than "scan_" to avoid matching "_scan_" in other contexts
|
||||
"wifi_",
|
||||
"wpa_",
|
||||
"wps_",
|
||||
"esp_wifi",
|
||||
"cnx_",
|
||||
"wpa3_",
|
||||
"sae_",
|
||||
"wDev_",
|
||||
"ic_mac_", # More specific than "mac_" to avoid matching emac_
|
||||
"esf_buf",
|
||||
"gWpaSm",
|
||||
"sm_WPA",
|
||||
"eapol_",
|
||||
"owe_",
|
||||
"wifiLowLevelInit",
|
||||
# Removed "s_do_mapping" - this is memory management, not WiFi
|
||||
"gScanStruct",
|
||||
"ppSearchTxframe",
|
||||
"ppMapWaitTxq",
|
||||
"ppFillAMPDUBar",
|
||||
"ppCheckTxConnTrafficIdle",
|
||||
"ppCalTkipMic",
|
||||
"phy_force_wifi",
|
||||
"phy_unforce_wifi",
|
||||
"write_wifi_chan",
|
||||
"wifi_track_pll",
|
||||
],
|
||||
"crypto_math": [
|
||||
"ecp_",
|
||||
"bignum_",
|
||||
@@ -231,13 +285,36 @@ SYMBOL_PATTERNS = {
|
||||
"p_256_init_curve",
|
||||
"shift_sub_rows",
|
||||
"rshift",
|
||||
"rijndaelEncrypt", # AES Rijndael encryption
|
||||
],
|
||||
# System and Arduino core functions must come before libc
|
||||
"esp_system": [
|
||||
"system_", # ESP system functions
|
||||
"postmortem_", # Postmortem reporting
|
||||
],
|
||||
"arduino_core": [
|
||||
"pinMode",
|
||||
"resetPins",
|
||||
"millis",
|
||||
"micros",
|
||||
"delay(", # More specific - Arduino delay function with parenthesis
|
||||
"delayMicroseconds",
|
||||
"digitalWrite",
|
||||
"digitalRead",
|
||||
],
|
||||
"sntp": ["sntp_", "sntp_recv"],
|
||||
"scheduler": [
|
||||
"run_scheduled_",
|
||||
"compute_scheduled_",
|
||||
"event_TaskQueue",
|
||||
],
|
||||
"hw_crypto": ["esp_aes", "esp_sha", "esp_rsa", "esp_bignum", "esp_mpi"],
|
||||
"libc": [
|
||||
"printf",
|
||||
"scanf",
|
||||
"malloc",
|
||||
"free",
|
||||
"_free", # More specific than "free" to match _free, __free_r, etc. but not arbitrary "free" substring
|
||||
"umm_free", # UMM malloc free function
|
||||
"memcpy",
|
||||
"memset",
|
||||
"strcpy",
|
||||
@@ -259,7 +336,7 @@ SYMBOL_PATTERNS = {
|
||||
"_setenv_r",
|
||||
"_tzset_unlocked_r",
|
||||
"__tzcalc_limits",
|
||||
"select",
|
||||
"_select", # More specific than "select" to avoid matching "dhcp_select", etc.
|
||||
"scalbnf",
|
||||
"strtof",
|
||||
"strtof_l",
|
||||
@@ -316,8 +393,24 @@ SYMBOL_PATTERNS = {
|
||||
"CSWTCH$",
|
||||
"dst$",
|
||||
"sulp",
|
||||
"_strtol_l", # String to long with locale
|
||||
"__cvt", # Convert
|
||||
"__utoa", # Unsigned to ASCII
|
||||
"__global_locale", # Global locale
|
||||
"_ctype_", # Character type
|
||||
"impure_data", # Impure data
|
||||
],
|
||||
"string_ops": [
|
||||
"strcmp",
|
||||
"strncmp",
|
||||
"strchr",
|
||||
"strstr",
|
||||
"strtok",
|
||||
"strdup",
|
||||
"strncasecmp_P", # String compare (case insensitive, from program memory)
|
||||
"strnlen_P", # String length (from program memory)
|
||||
"strncat_P", # String concatenate (from program memory)
|
||||
],
|
||||
"string_ops": ["strcmp", "strncmp", "strchr", "strstr", "strtok", "strdup"],
|
||||
"memory_alloc": ["malloc", "calloc", "realloc", "free", "_sbrk"],
|
||||
"file_io": [
|
||||
"fread",
|
||||
@@ -338,10 +431,26 @@ SYMBOL_PATTERNS = {
|
||||
"vsscanf",
|
||||
],
|
||||
"cpp_anonymous": ["_GLOBAL__N_", "n$"],
|
||||
"cpp_runtime": ["__cxx", "_ZN", "_ZL", "_ZSt", "__gxx_personality", "_Z16"],
|
||||
"exception_handling": ["__cxa_", "_Unwind_", "__gcc_personality", "uw_frame_state"],
|
||||
# Plain C patterns only - C++ symbols will be categorized via DEMANGLED_PATTERNS
|
||||
"nvs": ["nvs_"], # Plain C NVS functions
|
||||
"ota": ["ota_", "OTA", "esp_ota", "app_desc"],
|
||||
# cpp_runtime: Removed _ZN, _ZL to let DEMANGLED_PATTERNS categorize C++ symbols properly
|
||||
# Only keep patterns that are truly runtime-specific and not categorizable by namespace
|
||||
"cpp_runtime": ["__cxx", "_ZSt", "__gxx_personality", "_Z16"],
|
||||
"exception_handling": [
|
||||
"__cxa_",
|
||||
"_Unwind_",
|
||||
"__gcc_personality",
|
||||
"uw_frame_state",
|
||||
"search_object", # Search for exception handling object
|
||||
"get_cie_encoding", # Get CIE encoding
|
||||
"add_fdes", # Add frame description entries
|
||||
"fde_unencoded_compare", # Compare FDEs
|
||||
"fde_mixed_encoding_compare", # Compare mixed encoding FDEs
|
||||
"frame_downheap", # Frame heap operations
|
||||
"frame_heapsort", # Frame heap sorting
|
||||
],
|
||||
"static_init": ["_GLOBAL__sub_I_"],
|
||||
"mdns_lib": ["mdns"],
|
||||
"phy_radio": [
|
||||
"phy_",
|
||||
"rf_",
|
||||
@@ -394,10 +503,47 @@ SYMBOL_PATTERNS = {
|
||||
"txcal_debuge_mode",
|
||||
"ant_wifitx_cfg",
|
||||
"reg_init_begin",
|
||||
"tx_cap_init", # TX capacitance init
|
||||
"ram_set_txcap", # RAM TX capacitance setting
|
||||
"tx_atten_", # TX attenuation
|
||||
"txiq_", # TX I/Q calibration
|
||||
"ram_cal_", # RAM calibration
|
||||
"ram_rxiq_", # RAM RX I/Q
|
||||
"readvdd33", # Read VDD33
|
||||
"test_tout", # Test timeout
|
||||
"tsen_meas", # Temperature sensor measurement
|
||||
"bbpll_cal", # Baseband PLL calibration
|
||||
"set_cal_", # Set calibration
|
||||
"set_rfanagain_", # Set RF analog gain
|
||||
"set_txdc_", # Set TX DC
|
||||
"get_vdd33_", # Get VDD33
|
||||
"gen_rx_gain_table", # Generate RX gain table
|
||||
"ram_ana_inf_gating_en", # RAM analog interface gating enable
|
||||
"tx_cont_en", # TX continuous enable
|
||||
"tx_delay_cfg", # TX delay configuration
|
||||
"tx_gain_table_set", # TX gain table set
|
||||
"check_and_reset_hw_deadlock", # Hardware deadlock check
|
||||
"s_config", # System/hardware config
|
||||
"chan14_mic_cfg", # Channel 14 MIC config
|
||||
],
|
||||
"wifi_phy_pp": [
|
||||
"pp_",
|
||||
"ppT",
|
||||
"ppR",
|
||||
"ppP",
|
||||
"ppInstall",
|
||||
"ppCalTxAMPDULength",
|
||||
"ppCheckTx", # Packet processor TX check
|
||||
"ppCal", # Packet processor calibration
|
||||
"HdlAllBuffedEb", # Handle buffered EB
|
||||
],
|
||||
"wifi_phy_pp": ["pp_", "ppT", "ppR", "ppP", "ppInstall", "ppCalTxAMPDULength"],
|
||||
"wifi_lmac": ["lmac"],
|
||||
"wifi_device": ["wdev", "wDev_"],
|
||||
"wifi_device": [
|
||||
"wdev",
|
||||
"wDev_",
|
||||
"ic_set_sta", # Set station mode
|
||||
"ic_set_vif", # Set virtual interface
|
||||
],
|
||||
"power_mgmt": [
|
||||
"pm_",
|
||||
"sleep",
|
||||
@@ -406,15 +552,7 @@ SYMBOL_PATTERNS = {
|
||||
"deep_sleep",
|
||||
"power_down",
|
||||
"g_pm",
|
||||
],
|
||||
"memory_mgmt": [
|
||||
"mem_",
|
||||
"memory_",
|
||||
"tlsf_",
|
||||
"memp_",
|
||||
"pbuf_",
|
||||
"pbuf_alloc",
|
||||
"pbuf_copy_partial_pbuf",
|
||||
"pmc", # Power Management Controller
|
||||
],
|
||||
"hal_layer": ["hal_"],
|
||||
"clock_mgmt": [
|
||||
@@ -439,7 +577,6 @@ SYMBOL_PATTERNS = {
|
||||
"error_handling": ["panic", "abort", "assert", "error_", "fault"],
|
||||
"authentication": ["auth"],
|
||||
"ppp_protocol": ["ppp", "ipcp_", "lcp_", "chap_", "LcpEchoCheck"],
|
||||
"dhcp": ["dhcp", "handle_dhcp"],
|
||||
"ethernet_phy": [
|
||||
"emac_",
|
||||
"eth_phy_",
|
||||
@@ -618,7 +755,15 @@ SYMBOL_PATTERNS = {
|
||||
"ampdu_dispatch_upto",
|
||||
],
|
||||
"ieee802_11": ["ieee802_11_", "ieee802_11_parse_elems"],
|
||||
"rate_control": ["rssi_margin", "rcGetSched", "get_rate_fcc_index"],
|
||||
"rate_control": [
|
||||
"rssi_margin",
|
||||
"rcGetSched",
|
||||
"get_rate_fcc_index",
|
||||
"rcGetRate", # Get rate
|
||||
"rc_get_", # Rate control getters
|
||||
"rc_set_", # Rate control setters
|
||||
"rc_enable_", # Rate control enable functions
|
||||
],
|
||||
"nan": ["nan_dp_", "nan_dp_post_tx", "nan_dp_delete_peer"],
|
||||
"channel_mgmt": ["chm_init", "chm_set_current_channel"],
|
||||
"trace": ["trc_init", "trc_onAmpduOp"],
|
||||
@@ -799,31 +944,18 @@ SYMBOL_PATTERNS = {
|
||||
"supports_interlaced_inquiry_scan",
|
||||
"supports_reading_remote_extended_features",
|
||||
],
|
||||
"bluetooth_ll": [
|
||||
"lld_pdu_",
|
||||
"ld_acl_",
|
||||
"lld_stop_ind_handler",
|
||||
"lld_evt_winsize_change",
|
||||
"config_lld_evt_funcs_reset",
|
||||
"config_lld_funcs_reset",
|
||||
"config_llm_funcs_reset",
|
||||
"llm_set_long_adv_data",
|
||||
"lld_retry_tx_prog",
|
||||
"llc_link_sup_to_ind_handler",
|
||||
"config_llc_funcs_reset",
|
||||
"lld_evt_rxwin_compute",
|
||||
"config_btdm_funcs_reset",
|
||||
"config_ea_funcs_reset",
|
||||
"llc_defalut_state_tab_reset",
|
||||
"config_rwip_funcs_reset",
|
||||
"ke_lmp_rx_flooding_detect",
|
||||
],
|
||||
}
|
||||
|
||||
# Demangled patterns: patterns found in demangled C++ names
|
||||
DEMANGLED_PATTERNS = {
|
||||
"gpio_driver": ["GPIO"],
|
||||
"uart_driver": ["UART"],
|
||||
# mdns_lib must come before network_stack to avoid "udp" matching "_udpReadBuffer" in MDNSResponder
|
||||
"mdns_lib": [
|
||||
"MDNSResponder",
|
||||
"MDNSImplementation",
|
||||
"MDNS",
|
||||
],
|
||||
"network_stack": [
|
||||
"lwip",
|
||||
"tcp",
|
||||
@@ -836,6 +968,24 @@ DEMANGLED_PATTERNS = {
|
||||
"ethernet",
|
||||
"ppp",
|
||||
"slip",
|
||||
"UdpContext", # UDP context class
|
||||
"DhcpServer", # DHCP server class
|
||||
],
|
||||
"arduino_core": [
|
||||
"String::", # Arduino String class
|
||||
"Print::", # Arduino Print class
|
||||
"HardwareSerial::", # Serial class
|
||||
"IPAddress::", # IP address class
|
||||
"EspClass::", # ESP class
|
||||
"experimental::_SPI", # Experimental SPI
|
||||
],
|
||||
"ota": [
|
||||
"UpdaterClass",
|
||||
"Updater::",
|
||||
],
|
||||
"wifi": [
|
||||
"ESP8266WiFi",
|
||||
"WiFi::",
|
||||
],
|
||||
"wifi_stack": ["NetworkInterface"],
|
||||
"nimble_bt": [
|
||||
@@ -854,7 +1004,6 @@ DEMANGLED_PATTERNS = {
|
||||
"rtti": ["__type_info", "__class_type_info"],
|
||||
"web_server_lib": ["AsyncWebServer", "AsyncWebHandler", "WebServer"],
|
||||
"async_tcp": ["AsyncClient", "AsyncServer"],
|
||||
"mdns_lib": ["mdns"],
|
||||
"json_lib": [
|
||||
"ArduinoJson",
|
||||
"JsonDocument",
|
||||
|
||||
@@ -15,8 +15,13 @@ from esphome.const import (
|
||||
CONF_TYPE_ID,
|
||||
CONF_UPDATE_INTERVAL,
|
||||
)
|
||||
from esphome.core import ID
|
||||
from esphome.cpp_generator import MockObj, MockObjClass, TemplateArgsType
|
||||
from esphome.core import ID, Lambda
|
||||
from esphome.cpp_generator import (
|
||||
LambdaExpression,
|
||||
MockObj,
|
||||
MockObjClass,
|
||||
TemplateArgsType,
|
||||
)
|
||||
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
|
||||
from esphome.types import ConfigType
|
||||
from esphome.util import Registry
|
||||
@@ -87,6 +92,7 @@ def validate_potentially_or_condition(value):
|
||||
|
||||
DelayAction = cg.esphome_ns.class_("DelayAction", Action, cg.Component)
|
||||
LambdaAction = cg.esphome_ns.class_("LambdaAction", Action)
|
||||
StatelessLambdaAction = cg.esphome_ns.class_("StatelessLambdaAction", Action)
|
||||
IfAction = cg.esphome_ns.class_("IfAction", Action)
|
||||
WhileAction = cg.esphome_ns.class_("WhileAction", Action)
|
||||
RepeatAction = cg.esphome_ns.class_("RepeatAction", Action)
|
||||
@@ -97,9 +103,40 @@ ResumeComponentAction = cg.esphome_ns.class_("ResumeComponentAction", Action)
|
||||
Automation = cg.esphome_ns.class_("Automation")
|
||||
|
||||
LambdaCondition = cg.esphome_ns.class_("LambdaCondition", Condition)
|
||||
StatelessLambdaCondition = cg.esphome_ns.class_("StatelessLambdaCondition", Condition)
|
||||
ForCondition = cg.esphome_ns.class_("ForCondition", Condition, cg.Component)
|
||||
|
||||
|
||||
def new_lambda_pvariable(
|
||||
id_obj: ID,
|
||||
lambda_expr: LambdaExpression,
|
||||
stateless_class: MockObjClass,
|
||||
template_arg: cg.TemplateArguments | None = None,
|
||||
) -> MockObj:
|
||||
"""Create Pvariable for lambda, using stateless class if applicable.
|
||||
|
||||
Combines ID selection and Pvariable creation in one call. For stateless
|
||||
lambdas (empty capture), uses function pointer instead of std::function.
|
||||
|
||||
Args:
|
||||
id_obj: The ID object (action_id, condition_id, or filter_id)
|
||||
lambda_expr: The lambda expression object
|
||||
stateless_class: The stateless class to use for stateless lambdas
|
||||
template_arg: Optional template arguments (for actions/conditions)
|
||||
|
||||
Returns:
|
||||
The created Pvariable
|
||||
"""
|
||||
# For stateless lambdas, use function pointer instead of std::function
|
||||
if lambda_expr.capture == "":
|
||||
id_obj = id_obj.copy()
|
||||
id_obj.type = stateless_class
|
||||
|
||||
if template_arg is not None:
|
||||
return cg.new_Pvariable(id_obj, template_arg, lambda_expr)
|
||||
return cg.new_Pvariable(id_obj, lambda_expr)
|
||||
|
||||
|
||||
def validate_automation(extra_schema=None, extra_validators=None, single=False):
|
||||
if extra_schema is None:
|
||||
extra_schema = {}
|
||||
@@ -145,7 +182,7 @@ def validate_automation(extra_schema=None, extra_validators=None, single=False):
|
||||
value = cv.Schema([extra_validators])(value)
|
||||
if single:
|
||||
if len(value) != 1:
|
||||
raise cv.Invalid("Cannot have more than 1 automation for templates")
|
||||
raise cv.Invalid("This trigger allows only a single automation")
|
||||
return value[0]
|
||||
return value
|
||||
|
||||
@@ -240,7 +277,9 @@ async def lambda_condition_to_code(
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
lambda_ = await cg.process_lambda(config, args, return_type=bool)
|
||||
return cg.new_Pvariable(condition_id, template_arg, lambda_)
|
||||
return new_lambda_pvariable(
|
||||
condition_id, lambda_, StatelessLambdaCondition, template_arg
|
||||
)
|
||||
|
||||
|
||||
@register_condition(
|
||||
@@ -271,6 +310,30 @@ async def for_condition_to_code(
|
||||
return var
|
||||
|
||||
|
||||
@register_condition(
|
||||
"component.is_idle",
|
||||
LambdaCondition,
|
||||
maybe_simple_id(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(cg.Component),
|
||||
}
|
||||
),
|
||||
)
|
||||
async def component_is_idle_condition_to_code(
|
||||
config: ConfigType,
|
||||
condition_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
comp = await cg.get_variable(config[CONF_ID])
|
||||
lambda_ = await cg.process_lambda(
|
||||
Lambda(f"return {comp}->is_idle();"), args, return_type=bool
|
||||
)
|
||||
return new_lambda_pvariable(
|
||||
condition_id, lambda_, StatelessLambdaCondition, template_arg
|
||||
)
|
||||
|
||||
|
||||
@register_action(
|
||||
"delay", DelayAction, cv.templatable(cv.positive_time_period_milliseconds)
|
||||
)
|
||||
@@ -406,7 +469,7 @@ async def lambda_action_to_code(
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
lambda_ = await cg.process_lambda(config, args, return_type=cg.void)
|
||||
return cg.new_Pvariable(action_id, template_arg, lambda_)
|
||||
return new_lambda_pvariable(action_id, lambda_, StatelessLambdaAction, template_arg)
|
||||
|
||||
|
||||
@register_action(
|
||||
|
||||
@@ -62,6 +62,7 @@ from esphome.cpp_types import ( # noqa: F401
|
||||
EntityBase,
|
||||
EntityCategory,
|
||||
ESPTime,
|
||||
FixedVector,
|
||||
GPIOPin,
|
||||
InternalGPIOPin,
|
||||
JsonObject,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "absolute_humidity.h"
|
||||
// test
|
||||
|
||||
namespace esphome {
|
||||
namespace absolute_humidity {
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ enum SaturationVaporPressureEquation {
|
||||
};
|
||||
|
||||
/// This class implements calculation of absolute humidity from temperature and relative humidity.
|
||||
// Test change for clang-tidy split logic
|
||||
class AbsoluteHumidityComponent : public sensor::Sensor, public Component {
|
||||
public:
|
||||
AbsoluteHumidityComponent() = default;
|
||||
|
||||
@@ -9,7 +9,7 @@ static const char *const TAG = "adalight_light_effect";
|
||||
static const uint32_t ADALIGHT_ACK_INTERVAL = 1000;
|
||||
static const uint32_t ADALIGHT_RECEIVE_TIMEOUT = 1000;
|
||||
|
||||
AdalightLightEffect::AdalightLightEffect(const std::string &name) : AddressableLightEffect(name) {}
|
||||
AdalightLightEffect::AdalightLightEffect(const char *name) : AddressableLightEffect(name) {}
|
||||
|
||||
void AdalightLightEffect::start() {
|
||||
AddressableLightEffect::start();
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace adalight {
|
||||
|
||||
class AdalightLightEffect : public light::AddressableLightEffect, public uart::UARTDevice {
|
||||
public:
|
||||
AdalightLightEffect(const std::string &name);
|
||||
AdalightLightEffect(const char *name);
|
||||
|
||||
void start() override;
|
||||
void stop() override;
|
||||
|
||||
@@ -172,12 +172,6 @@ def alarm_control_panel_schema(
|
||||
return _ALARM_CONTROL_PANEL_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
# Remove before 2025.11.0
|
||||
ALARM_CONTROL_PANEL_SCHEMA = alarm_control_panel_schema(AlarmControlPanel)
|
||||
ALARM_CONTROL_PANEL_SCHEMA.add_extra(
|
||||
cv.deprecated_schema_constant("alarm_control_panel")
|
||||
)
|
||||
|
||||
ALARM_CONTROL_PANEL_ACTION_SCHEMA = maybe_simple_id(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(AlarmControlPanel),
|
||||
|
||||
@@ -71,10 +71,12 @@ SERVICE_ARG_NATIVE_TYPES = {
|
||||
"int": cg.int32,
|
||||
"float": float,
|
||||
"string": cg.std_string,
|
||||
"bool[]": cg.std_vector.template(bool),
|
||||
"int[]": cg.std_vector.template(cg.int32),
|
||||
"float[]": cg.std_vector.template(float),
|
||||
"string[]": cg.std_vector.template(cg.std_string),
|
||||
"bool[]": cg.FixedVector.template(bool).operator("const").operator("ref"),
|
||||
"int[]": cg.FixedVector.template(cg.int32).operator("const").operator("ref"),
|
||||
"float[]": cg.FixedVector.template(float).operator("const").operator("ref"),
|
||||
"string[]": cg.FixedVector.template(cg.std_string)
|
||||
.operator("const")
|
||||
.operator("ref"),
|
||||
}
|
||||
CONF_ENCRYPTION = "encryption"
|
||||
CONF_BATCH_DELAY = "batch_delay"
|
||||
@@ -258,6 +260,10 @@ async def to_code(config):
|
||||
if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]:
|
||||
cg.add_define("USE_API_SERVICES")
|
||||
|
||||
# Set USE_API_CUSTOM_SERVICES if external components need dynamic service registration
|
||||
if config[CONF_CUSTOM_SERVICES]:
|
||||
cg.add_define("USE_API_CUSTOM_SERVICES")
|
||||
|
||||
if config[CONF_HOMEASSISTANT_SERVICES]:
|
||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||
|
||||
@@ -265,6 +271,8 @@ async def to_code(config):
|
||||
cg.add_define("USE_API_HOMEASSISTANT_STATES")
|
||||
|
||||
if actions := config.get(CONF_ACTIONS, []):
|
||||
# Collect all triggers first, then register all at once with initializer_list
|
||||
triggers: list[cg.Pvariable] = []
|
||||
for conf in actions:
|
||||
template_args = []
|
||||
func_args = []
|
||||
@@ -278,8 +286,10 @@ async def to_code(config):
|
||||
trigger = cg.new_Pvariable(
|
||||
conf[CONF_TRIGGER_ID], templ, conf[CONF_ACTION], service_arg_names
|
||||
)
|
||||
cg.add(var.register_user_service(trigger))
|
||||
triggers.append(trigger)
|
||||
await automation.build_automation(trigger, func_args, conf)
|
||||
# Register all services at once - single allocation, no reallocations
|
||||
cg.add(var.initialize_user_services(triggers))
|
||||
|
||||
if CONF_ON_CLIENT_CONNECTED in config:
|
||||
cg.add_define("USE_API_CLIENT_CONNECTED_TRIGGER")
|
||||
|
||||
@@ -425,7 +425,7 @@ message ListEntitiesFanResponse {
|
||||
bool disabled_by_default = 9;
|
||||
string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON"];
|
||||
EntityCategory entity_category = 11;
|
||||
repeated string supported_preset_modes = 12 [(container_pointer) = "std::set"];
|
||||
repeated string supported_preset_modes = 12 [(container_pointer_no_template) = "std::vector<const char *>"];
|
||||
uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"];
|
||||
}
|
||||
// Deprecated in API version 1.6 - only used in deprecated fields
|
||||
@@ -989,7 +989,7 @@ message ListEntitiesClimateResponse {
|
||||
|
||||
bool supports_current_temperature = 5; // Deprecated: use feature_flags
|
||||
bool supports_two_point_target_temperature = 6; // Deprecated: use feature_flags
|
||||
repeated ClimateMode supported_modes = 7 [(container_pointer) = "std::set<climate::ClimateMode>"];
|
||||
repeated ClimateMode supported_modes = 7 [(container_pointer_no_template) = "climate::ClimateModeMask"];
|
||||
float visual_min_temperature = 8;
|
||||
float visual_max_temperature = 9;
|
||||
float visual_target_temperature_step = 10;
|
||||
@@ -998,11 +998,11 @@ message ListEntitiesClimateResponse {
|
||||
// Deprecated in API version 1.5
|
||||
bool legacy_supports_away = 11 [deprecated=true];
|
||||
bool supports_action = 12; // Deprecated: use feature_flags
|
||||
repeated ClimateFanMode supported_fan_modes = 13 [(container_pointer) = "std::set<climate::ClimateFanMode>"];
|
||||
repeated ClimateSwingMode supported_swing_modes = 14 [(container_pointer) = "std::set<climate::ClimateSwingMode>"];
|
||||
repeated string supported_custom_fan_modes = 15 [(container_pointer) = "std::set"];
|
||||
repeated ClimatePreset supported_presets = 16 [(container_pointer) = "std::set<climate::ClimatePreset>"];
|
||||
repeated string supported_custom_presets = 17 [(container_pointer) = "std::set"];
|
||||
repeated ClimateFanMode supported_fan_modes = 13 [(container_pointer_no_template) = "climate::ClimateFanModeMask"];
|
||||
repeated ClimateSwingMode supported_swing_modes = 14 [(container_pointer_no_template) = "climate::ClimateSwingModeMask"];
|
||||
repeated string supported_custom_fan_modes = 15 [(container_pointer_no_template) = "std::vector<const char *>"];
|
||||
repeated ClimatePreset supported_presets = 16 [(container_pointer_no_template) = "climate::ClimatePresetMask"];
|
||||
repeated string supported_custom_presets = 17 [(container_pointer_no_template) = "std::vector<const char *>"];
|
||||
bool disabled_by_default = 18;
|
||||
string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON"];
|
||||
EntityCategory entity_category = 20;
|
||||
@@ -1143,7 +1143,7 @@ message ListEntitiesSelectResponse {
|
||||
reserved 4; // Deprecated: was string unique_id
|
||||
|
||||
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
|
||||
repeated string options = 6 [(container_pointer) = "std::vector"];
|
||||
repeated string options = 6 [(container_pointer_no_template) = "FixedVector<const char *>"];
|
||||
bool disabled_by_default = 7;
|
||||
EntityCategory entity_category = 8;
|
||||
uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"];
|
||||
|
||||
@@ -423,7 +423,7 @@ uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *con
|
||||
msg.supports_speed = traits.supports_speed();
|
||||
msg.supports_direction = traits.supports_direction();
|
||||
msg.supported_speed_count = traits.supported_speed_count();
|
||||
msg.supported_preset_modes = &traits.supported_preset_modes_for_api_();
|
||||
msg.supported_preset_modes = &traits.supported_preset_modes();
|
||||
return fill_and_encode_entity_info(fan, msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
|
||||
}
|
||||
void APIConnection::fan_command(const FanCommandRequest &msg) {
|
||||
@@ -486,7 +486,7 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c
|
||||
if (light->supports_effects()) {
|
||||
msg.effects.emplace_back("None");
|
||||
for (auto *effect : light->get_effects()) {
|
||||
msg.effects.push_back(effect->get_name());
|
||||
msg.effects.emplace_back(effect->get_name());
|
||||
}
|
||||
}
|
||||
return fill_and_encode_entity_info(light, msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size,
|
||||
@@ -637,14 +637,14 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection
|
||||
}
|
||||
if (traits.get_supports_fan_modes() && climate->fan_mode.has_value())
|
||||
resp.fan_mode = static_cast<enums::ClimateFanMode>(climate->fan_mode.value());
|
||||
if (!traits.get_supported_custom_fan_modes().empty() && climate->custom_fan_mode.has_value()) {
|
||||
resp.set_custom_fan_mode(StringRef(climate->custom_fan_mode.value()));
|
||||
if (!traits.get_supported_custom_fan_modes().empty() && climate->has_custom_fan_mode()) {
|
||||
resp.set_custom_fan_mode(StringRef(climate->get_custom_fan_mode()));
|
||||
}
|
||||
if (traits.get_supports_presets() && climate->preset.has_value()) {
|
||||
resp.preset = static_cast<enums::ClimatePreset>(climate->preset.value());
|
||||
}
|
||||
if (!traits.get_supported_custom_presets().empty() && climate->custom_preset.has_value()) {
|
||||
resp.set_custom_preset(StringRef(climate->custom_preset.value()));
|
||||
if (!traits.get_supported_custom_presets().empty() && climate->has_custom_preset()) {
|
||||
resp.set_custom_preset(StringRef(climate->get_custom_preset()));
|
||||
}
|
||||
if (traits.get_supports_swing_modes())
|
||||
resp.swing_mode = static_cast<enums::ClimateSwingMode>(climate->swing_mode);
|
||||
@@ -669,18 +669,18 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection
|
||||
msg.supports_action = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION);
|
||||
// Current feature flags and other supported parameters
|
||||
msg.feature_flags = traits.get_feature_flags();
|
||||
msg.supported_modes = &traits.get_supported_modes_for_api_();
|
||||
msg.supported_modes = &traits.get_supported_modes();
|
||||
msg.visual_min_temperature = traits.get_visual_min_temperature();
|
||||
msg.visual_max_temperature = traits.get_visual_max_temperature();
|
||||
msg.visual_target_temperature_step = traits.get_visual_target_temperature_step();
|
||||
msg.visual_current_temperature_step = traits.get_visual_current_temperature_step();
|
||||
msg.visual_min_humidity = traits.get_visual_min_humidity();
|
||||
msg.visual_max_humidity = traits.get_visual_max_humidity();
|
||||
msg.supported_fan_modes = &traits.get_supported_fan_modes_for_api_();
|
||||
msg.supported_custom_fan_modes = &traits.get_supported_custom_fan_modes_for_api_();
|
||||
msg.supported_presets = &traits.get_supported_presets_for_api_();
|
||||
msg.supported_custom_presets = &traits.get_supported_custom_presets_for_api_();
|
||||
msg.supported_swing_modes = &traits.get_supported_swing_modes_for_api_();
|
||||
msg.supported_fan_modes = &traits.get_supported_fan_modes();
|
||||
msg.supported_custom_fan_modes = &traits.get_supported_custom_fan_modes();
|
||||
msg.supported_presets = &traits.get_supported_presets();
|
||||
msg.supported_custom_presets = &traits.get_supported_custom_presets();
|
||||
msg.supported_swing_modes = &traits.get_supported_swing_modes();
|
||||
return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size,
|
||||
is_single);
|
||||
}
|
||||
@@ -1572,7 +1572,13 @@ bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryption
|
||||
resp.success = false;
|
||||
|
||||
psk_t psk{};
|
||||
if (base64_decode(msg.key, psk.data(), msg.key.size()) != psk.size()) {
|
||||
if (msg.key.empty()) {
|
||||
if (this->parent_->clear_noise_psk(true)) {
|
||||
resp.success = true;
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Failed to clear encryption key");
|
||||
}
|
||||
} else if (base64_decode(msg.key, psk.data(), msg.key.size()) != psk.size()) {
|
||||
ESP_LOGW(TAG, "Invalid encryption key length");
|
||||
} else if (!this->parent_->save_noise_psk(psk, true)) {
|
||||
ESP_LOGW(TAG, "Failed to save encryption key");
|
||||
|
||||
@@ -434,8 +434,7 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, st
|
||||
return APIError::OK;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> *raw_buffer = buffer.get_buffer();
|
||||
uint8_t *buffer_data = raw_buffer->data(); // Cache buffer pointer
|
||||
uint8_t *buffer_data = buffer.get_buffer()->data();
|
||||
|
||||
this->reusable_iovs_.clear();
|
||||
this->reusable_iovs_.reserve(packets.size());
|
||||
|
||||
@@ -230,8 +230,7 @@ APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer
|
||||
return APIError::OK;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> *raw_buffer = buffer.get_buffer();
|
||||
uint8_t *buffer_data = raw_buffer->data(); // Cache buffer pointer
|
||||
uint8_t *buffer_data = buffer.get_buffer()->data();
|
||||
|
||||
this->reusable_iovs_.clear();
|
||||
this->reusable_iovs_.reserve(packets.size());
|
||||
|
||||
@@ -355,8 +355,8 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const {
|
||||
buffer.encode_string(10, this->icon_ref_);
|
||||
#endif
|
||||
buffer.encode_uint32(11, static_cast<uint32_t>(this->entity_category));
|
||||
for (const auto &it : *this->supported_preset_modes) {
|
||||
buffer.encode_string(12, it, true);
|
||||
for (const char *it : *this->supported_preset_modes) {
|
||||
buffer.encode_string(12, it, strlen(it), true);
|
||||
}
|
||||
#ifdef USE_DEVICES
|
||||
buffer.encode_uint32(13, this->device_id);
|
||||
@@ -376,8 +376,8 @@ void ListEntitiesFanResponse::calculate_size(ProtoSize &size) const {
|
||||
#endif
|
||||
size.add_uint32(1, static_cast<uint32_t>(this->entity_category));
|
||||
if (!this->supported_preset_modes->empty()) {
|
||||
for (const auto &it : *this->supported_preset_modes) {
|
||||
size.add_length_force(1, it.size());
|
||||
for (const char *it : *this->supported_preset_modes) {
|
||||
size.add_length_force(1, strlen(it));
|
||||
}
|
||||
}
|
||||
#ifdef USE_DEVICES
|
||||
@@ -1179,14 +1179,14 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const {
|
||||
for (const auto &it : *this->supported_swing_modes) {
|
||||
buffer.encode_uint32(14, static_cast<uint32_t>(it), true);
|
||||
}
|
||||
for (const auto &it : *this->supported_custom_fan_modes) {
|
||||
buffer.encode_string(15, it, true);
|
||||
for (const char *it : *this->supported_custom_fan_modes) {
|
||||
buffer.encode_string(15, it, strlen(it), true);
|
||||
}
|
||||
for (const auto &it : *this->supported_presets) {
|
||||
buffer.encode_uint32(16, static_cast<uint32_t>(it), true);
|
||||
}
|
||||
for (const auto &it : *this->supported_custom_presets) {
|
||||
buffer.encode_string(17, it, true);
|
||||
for (const char *it : *this->supported_custom_presets) {
|
||||
buffer.encode_string(17, it, strlen(it), true);
|
||||
}
|
||||
buffer.encode_bool(18, this->disabled_by_default);
|
||||
#ifdef USE_ENTITY_ICON
|
||||
@@ -1229,8 +1229,8 @@ void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const {
|
||||
}
|
||||
}
|
||||
if (!this->supported_custom_fan_modes->empty()) {
|
||||
for (const auto &it : *this->supported_custom_fan_modes) {
|
||||
size.add_length_force(1, it.size());
|
||||
for (const char *it : *this->supported_custom_fan_modes) {
|
||||
size.add_length_force(1, strlen(it));
|
||||
}
|
||||
}
|
||||
if (!this->supported_presets->empty()) {
|
||||
@@ -1239,8 +1239,8 @@ void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const {
|
||||
}
|
||||
}
|
||||
if (!this->supported_custom_presets->empty()) {
|
||||
for (const auto &it : *this->supported_custom_presets) {
|
||||
size.add_length_force(2, it.size());
|
||||
for (const char *it : *this->supported_custom_presets) {
|
||||
size.add_length_force(2, strlen(it));
|
||||
}
|
||||
}
|
||||
size.add_bool(2, this->disabled_by_default);
|
||||
@@ -1475,8 +1475,8 @@ void ListEntitiesSelectResponse::encode(ProtoWriteBuffer buffer) const {
|
||||
#ifdef USE_ENTITY_ICON
|
||||
buffer.encode_string(5, this->icon_ref_);
|
||||
#endif
|
||||
for (const auto &it : *this->options) {
|
||||
buffer.encode_string(6, it, true);
|
||||
for (const char *it : *this->options) {
|
||||
buffer.encode_string(6, it, strlen(it), true);
|
||||
}
|
||||
buffer.encode_bool(7, this->disabled_by_default);
|
||||
buffer.encode_uint32(8, static_cast<uint32_t>(this->entity_category));
|
||||
@@ -1492,8 +1492,8 @@ void ListEntitiesSelectResponse::calculate_size(ProtoSize &size) const {
|
||||
size.add_length(1, this->icon_ref_.size());
|
||||
#endif
|
||||
if (!this->options->empty()) {
|
||||
for (const auto &it : *this->options) {
|
||||
size.add_length_force(1, it.size());
|
||||
for (const char *it : *this->options) {
|
||||
size.add_length_force(1, strlen(it));
|
||||
}
|
||||
}
|
||||
size.add_bool(1, this->disabled_by_default);
|
||||
|
||||
@@ -725,7 +725,7 @@ class ListEntitiesFanResponse final : public InfoResponseProtoMessage {
|
||||
bool supports_speed{false};
|
||||
bool supports_direction{false};
|
||||
int32_t supported_speed_count{0};
|
||||
const std::set<std::string> *supported_preset_modes{};
|
||||
const std::vector<const char *> *supported_preset_modes{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@@ -1377,16 +1377,16 @@ class ListEntitiesClimateResponse final : public InfoResponseProtoMessage {
|
||||
#endif
|
||||
bool supports_current_temperature{false};
|
||||
bool supports_two_point_target_temperature{false};
|
||||
const std::set<climate::ClimateMode> *supported_modes{};
|
||||
const climate::ClimateModeMask *supported_modes{};
|
||||
float visual_min_temperature{0.0f};
|
||||
float visual_max_temperature{0.0f};
|
||||
float visual_target_temperature_step{0.0f};
|
||||
bool supports_action{false};
|
||||
const std::set<climate::ClimateFanMode> *supported_fan_modes{};
|
||||
const std::set<climate::ClimateSwingMode> *supported_swing_modes{};
|
||||
const std::set<std::string> *supported_custom_fan_modes{};
|
||||
const std::set<climate::ClimatePreset> *supported_presets{};
|
||||
const std::set<std::string> *supported_custom_presets{};
|
||||
const climate::ClimateFanModeMask *supported_fan_modes{};
|
||||
const climate::ClimateSwingModeMask *supported_swing_modes{};
|
||||
const std::vector<const char *> *supported_custom_fan_modes{};
|
||||
const climate::ClimatePresetMask *supported_presets{};
|
||||
const std::vector<const char *> *supported_custom_presets{};
|
||||
float visual_current_temperature_step{0.0f};
|
||||
bool supports_current_humidity{false};
|
||||
bool supports_target_humidity{false};
|
||||
@@ -1534,7 +1534,7 @@ class ListEntitiesSelectResponse final : public InfoResponseProtoMessage {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "list_entities_select_response"; }
|
||||
#endif
|
||||
const std::vector<std::string> *options{};
|
||||
const FixedVector<const char *> *options{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
|
||||
@@ -88,6 +88,12 @@ static void dump_field(std::string &out, const char *field_name, StringRef value
|
||||
out.append("\n");
|
||||
}
|
||||
|
||||
static void dump_field(std::string &out, const char *field_name, const char *value, int indent = 2) {
|
||||
append_field_prefix(out, field_name, indent);
|
||||
out.append("'").append(value).append("'");
|
||||
out.append("\n");
|
||||
}
|
||||
|
||||
template<typename T> static void dump_field(std::string &out, const char *field_name, T value, int indent = 2) {
|
||||
append_field_prefix(out, field_name, indent);
|
||||
out.append(proto_enum_to_string<T>(value));
|
||||
|
||||
@@ -468,6 +468,31 @@ uint16_t APIServer::get_port() const { return this->port_; }
|
||||
void APIServer::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg,
|
||||
const LogString *fail_log_msg, const psk_t &active_psk, bool make_active) {
|
||||
if (!this->noise_pref_.save(&new_psk)) {
|
||||
ESP_LOGW(TAG, "%s", LOG_STR_ARG(fail_log_msg));
|
||||
return false;
|
||||
}
|
||||
// ensure it's written immediately
|
||||
if (!global_preferences->sync()) {
|
||||
ESP_LOGW(TAG, "Failed to sync preferences");
|
||||
return false;
|
||||
}
|
||||
ESP_LOGD(TAG, "%s", LOG_STR_ARG(save_log_msg));
|
||||
if (make_active) {
|
||||
this->set_timeout(100, [this, active_psk]() {
|
||||
ESP_LOGW(TAG, "Disconnecting all clients to reset PSK");
|
||||
this->set_noise_psk(active_psk);
|
||||
for (auto &c : this->clients_) {
|
||||
DisconnectRequest req;
|
||||
c->send_message(req, DisconnectRequest::MESSAGE_TYPE);
|
||||
}
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
|
||||
#ifdef USE_API_NOISE_PSK_FROM_YAML
|
||||
// When PSK is set from YAML, this function should never be called
|
||||
@@ -482,27 +507,21 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
|
||||
}
|
||||
|
||||
SavedNoisePsk new_saved_psk{psk};
|
||||
if (!this->noise_pref_.save(&new_saved_psk)) {
|
||||
ESP_LOGW(TAG, "Failed to save Noise PSK");
|
||||
return false;
|
||||
}
|
||||
// ensure it's written immediately
|
||||
if (!global_preferences->sync()) {
|
||||
ESP_LOGW(TAG, "Failed to sync preferences");
|
||||
return false;
|
||||
}
|
||||
ESP_LOGD(TAG, "Noise PSK saved");
|
||||
if (make_active) {
|
||||
this->set_timeout(100, [this, psk]() {
|
||||
ESP_LOGW(TAG, "Disconnecting all clients to reset PSK");
|
||||
this->set_noise_psk(psk);
|
||||
for (auto &c : this->clients_) {
|
||||
DisconnectRequest req;
|
||||
c->send_message(req, DisconnectRequest::MESSAGE_TYPE);
|
||||
}
|
||||
});
|
||||
}
|
||||
return true;
|
||||
return this->update_noise_psk_(new_saved_psk, LOG_STR("Noise PSK saved"), LOG_STR("Failed to save Noise PSK"), psk,
|
||||
make_active);
|
||||
#endif
|
||||
}
|
||||
bool APIServer::clear_noise_psk(bool make_active) {
|
||||
#ifdef USE_API_NOISE_PSK_FROM_YAML
|
||||
// When PSK is set from YAML, this function should never be called
|
||||
// but if it is, reject the change
|
||||
ESP_LOGW(TAG, "Key set in YAML");
|
||||
return false;
|
||||
#else
|
||||
SavedNoisePsk empty_psk{};
|
||||
psk_t empty{};
|
||||
return this->update_noise_psk_(empty_psk, LOG_STR("Noise PSK cleared"), LOG_STR("Failed to clear Noise PSK"), empty,
|
||||
make_active);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -53,6 +53,7 @@ class APIServer : public Component, public Controller {
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
bool save_noise_psk(psk_t psk, bool make_active = true);
|
||||
bool clear_noise_psk(bool make_active = true);
|
||||
void set_noise_psk(psk_t psk) { noise_ctx_->set_psk(psk); }
|
||||
std::shared_ptr<APINoiseContext> get_noise_ctx() { return noise_ctx_; }
|
||||
#endif // USE_API_NOISE
|
||||
@@ -124,8 +125,14 @@ class APIServer : public Component, public Controller {
|
||||
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
#endif // USE_API_HOMEASSISTANT_SERVICES
|
||||
#ifdef USE_API_SERVICES
|
||||
void initialize_user_services(std::initializer_list<UserServiceDescriptor *> services) {
|
||||
this->user_services_.assign(services);
|
||||
}
|
||||
#ifdef USE_API_CUSTOM_SERVICES
|
||||
// Only compile push_back method when custom_services: true (external components)
|
||||
void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }
|
||||
#endif
|
||||
#endif
|
||||
#ifdef USE_HOMEASSISTANT_TIME
|
||||
void request_time();
|
||||
#endif
|
||||
@@ -174,6 +181,10 @@ class APIServer : public Component, public Controller {
|
||||
|
||||
protected:
|
||||
void schedule_reboot_timeout_();
|
||||
#ifdef USE_API_NOISE
|
||||
bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg,
|
||||
const psk_t &active_psk, bool make_active);
|
||||
#endif // USE_API_NOISE
|
||||
// Pointers and pointer-like types first (4 bytes each)
|
||||
std::unique_ptr<socket::Socket> socket_ = nullptr;
|
||||
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
|
||||
|
||||
@@ -53,8 +53,14 @@ class CustomAPIDevice {
|
||||
template<typename T, typename... Ts>
|
||||
void register_service(void (T::*callback)(Ts...), const std::string &name,
|
||||
const std::array<std::string, sizeof...(Ts)> &arg_names) {
|
||||
#ifdef USE_API_CUSTOM_SERVICES
|
||||
auto *service = new CustomAPIDeviceService<T, Ts...>(name, arg_names, (T *) this, callback); // NOLINT
|
||||
global_api_server->register_user_service(service);
|
||||
#else
|
||||
static_assert(
|
||||
sizeof(T) == 0,
|
||||
"register_service() requires 'custom_services: true' in the 'api:' section of your YAML configuration");
|
||||
#endif
|
||||
}
|
||||
#else
|
||||
template<typename T, typename... Ts>
|
||||
@@ -86,8 +92,14 @@ class CustomAPIDevice {
|
||||
*/
|
||||
#ifdef USE_API_SERVICES
|
||||
template<typename T> void register_service(void (T::*callback)(), const std::string &name) {
|
||||
#ifdef USE_API_CUSTOM_SERVICES
|
||||
auto *service = new CustomAPIDeviceService<T>(name, {}, (T *) this, callback); // NOLINT
|
||||
global_api_server->register_user_service(service);
|
||||
#else
|
||||
static_assert(
|
||||
sizeof(T) == 0,
|
||||
"register_service() requires 'custom_services: true' in the 'api:' section of your YAML configuration");
|
||||
#endif
|
||||
}
|
||||
#else
|
||||
template<typename T> void register_service(void (T::*callback)(), const std::string &name) {
|
||||
|
||||
@@ -11,23 +11,58 @@ template<> int32_t get_execute_arg_value<int32_t>(const ExecuteServiceArgument &
|
||||
}
|
||||
template<> float get_execute_arg_value<float>(const ExecuteServiceArgument &arg) { return arg.float_; }
|
||||
template<> std::string get_execute_arg_value<std::string>(const ExecuteServiceArgument &arg) { return arg.string_; }
|
||||
|
||||
// Legacy std::vector versions for external components using custom_api_device.h - optimized with reserve
|
||||
template<> std::vector<bool> get_execute_arg_value<std::vector<bool>>(const ExecuteServiceArgument &arg) {
|
||||
return std::vector<bool>(arg.bool_array.begin(), arg.bool_array.end());
|
||||
std::vector<bool> result;
|
||||
result.reserve(arg.bool_array.size());
|
||||
result.insert(result.end(), arg.bool_array.begin(), arg.bool_array.end());
|
||||
return result;
|
||||
}
|
||||
template<> std::vector<int32_t> get_execute_arg_value<std::vector<int32_t>>(const ExecuteServiceArgument &arg) {
|
||||
return std::vector<int32_t>(arg.int_array.begin(), arg.int_array.end());
|
||||
std::vector<int32_t> result;
|
||||
result.reserve(arg.int_array.size());
|
||||
result.insert(result.end(), arg.int_array.begin(), arg.int_array.end());
|
||||
return result;
|
||||
}
|
||||
template<> std::vector<float> get_execute_arg_value<std::vector<float>>(const ExecuteServiceArgument &arg) {
|
||||
return std::vector<float>(arg.float_array.begin(), arg.float_array.end());
|
||||
std::vector<float> result;
|
||||
result.reserve(arg.float_array.size());
|
||||
result.insert(result.end(), arg.float_array.begin(), arg.float_array.end());
|
||||
return result;
|
||||
}
|
||||
template<> std::vector<std::string> get_execute_arg_value<std::vector<std::string>>(const ExecuteServiceArgument &arg) {
|
||||
return std::vector<std::string>(arg.string_array.begin(), arg.string_array.end());
|
||||
std::vector<std::string> result;
|
||||
result.reserve(arg.string_array.size());
|
||||
result.insert(result.end(), arg.string_array.begin(), arg.string_array.end());
|
||||
return result;
|
||||
}
|
||||
|
||||
// New FixedVector const reference versions for YAML-generated services - zero-copy
|
||||
template<>
|
||||
const FixedVector<bool> &get_execute_arg_value<const FixedVector<bool> &>(const ExecuteServiceArgument &arg) {
|
||||
return arg.bool_array;
|
||||
}
|
||||
template<>
|
||||
const FixedVector<int32_t> &get_execute_arg_value<const FixedVector<int32_t> &>(const ExecuteServiceArgument &arg) {
|
||||
return arg.int_array;
|
||||
}
|
||||
template<>
|
||||
const FixedVector<float> &get_execute_arg_value<const FixedVector<float> &>(const ExecuteServiceArgument &arg) {
|
||||
return arg.float_array;
|
||||
}
|
||||
template<>
|
||||
const FixedVector<std::string> &get_execute_arg_value<const FixedVector<std::string> &>(
|
||||
const ExecuteServiceArgument &arg) {
|
||||
return arg.string_array;
|
||||
}
|
||||
|
||||
template<> enums::ServiceArgType to_service_arg_type<bool>() { return enums::SERVICE_ARG_TYPE_BOOL; }
|
||||
template<> enums::ServiceArgType to_service_arg_type<int32_t>() { return enums::SERVICE_ARG_TYPE_INT; }
|
||||
template<> enums::ServiceArgType to_service_arg_type<float>() { return enums::SERVICE_ARG_TYPE_FLOAT; }
|
||||
template<> enums::ServiceArgType to_service_arg_type<std::string>() { return enums::SERVICE_ARG_TYPE_STRING; }
|
||||
|
||||
// Legacy std::vector versions for external components using custom_api_device.h
|
||||
template<> enums::ServiceArgType to_service_arg_type<std::vector<bool>>() { return enums::SERVICE_ARG_TYPE_BOOL_ARRAY; }
|
||||
template<> enums::ServiceArgType to_service_arg_type<std::vector<int32_t>>() {
|
||||
return enums::SERVICE_ARG_TYPE_INT_ARRAY;
|
||||
@@ -39,4 +74,18 @@ template<> enums::ServiceArgType to_service_arg_type<std::vector<std::string>>()
|
||||
return enums::SERVICE_ARG_TYPE_STRING_ARRAY;
|
||||
}
|
||||
|
||||
// New FixedVector const reference versions for YAML-generated services
|
||||
template<> enums::ServiceArgType to_service_arg_type<const FixedVector<bool> &>() {
|
||||
return enums::SERVICE_ARG_TYPE_BOOL_ARRAY;
|
||||
}
|
||||
template<> enums::ServiceArgType to_service_arg_type<const FixedVector<int32_t> &>() {
|
||||
return enums::SERVICE_ARG_TYPE_INT_ARRAY;
|
||||
}
|
||||
template<> enums::ServiceArgType to_service_arg_type<const FixedVector<float> &>() {
|
||||
return enums::SERVICE_ARG_TYPE_FLOAT_ARRAY;
|
||||
}
|
||||
template<> enums::ServiceArgType to_service_arg_type<const FixedVector<std::string> &>() {
|
||||
return enums::SERVICE_ARG_TYPE_STRING_ARRAY;
|
||||
}
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
@@ -99,9 +99,7 @@ enum BedjetCommand : uint8_t {
|
||||
|
||||
static const uint8_t BEDJET_FAN_SPEED_COUNT = 20;
|
||||
|
||||
static const char *const BEDJET_FAN_STEP_NAMES[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_;
|
||||
static const std::string BEDJET_FAN_STEP_NAME_STRINGS[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_;
|
||||
static const std::set<std::string> BEDJET_FAN_STEP_NAMES_SET BEDJET_FAN_STEP_NAMES_;
|
||||
static constexpr const char *const BEDJET_FAN_STEP_NAMES[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_;
|
||||
|
||||
} // namespace bedjet
|
||||
} // namespace esphome
|
||||
|
||||
@@ -8,15 +8,15 @@ namespace bedjet {
|
||||
|
||||
using namespace esphome::climate;
|
||||
|
||||
static const std::string *bedjet_fan_step_to_fan_mode(const uint8_t fan_step) {
|
||||
static const char *bedjet_fan_step_to_fan_mode(const uint8_t fan_step) {
|
||||
if (fan_step < BEDJET_FAN_SPEED_COUNT)
|
||||
return &BEDJET_FAN_STEP_NAME_STRINGS[fan_step];
|
||||
return BEDJET_FAN_STEP_NAMES[fan_step];
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static uint8_t bedjet_fan_speed_to_step(const std::string &fan_step_percent) {
|
||||
static uint8_t bedjet_fan_speed_to_step(const char *fan_step_percent) {
|
||||
for (int i = 0; i < BEDJET_FAN_SPEED_COUNT; i++) {
|
||||
if (fan_step_percent == BEDJET_FAN_STEP_NAME_STRINGS[i]) {
|
||||
if (strcmp(BEDJET_FAN_STEP_NAMES[i], fan_step_percent) == 0) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,7 @@ void BedJetClimate::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_fan_mode_to_string(mode)));
|
||||
}
|
||||
for (const auto &mode : traits.get_supported_custom_fan_modes()) {
|
||||
ESP_LOGCONFIG(TAG, " - %s (c)", mode.c_str());
|
||||
ESP_LOGCONFIG(TAG, " - %s (c)", mode);
|
||||
}
|
||||
|
||||
ESP_LOGCONFIG(TAG, " Supported presets:");
|
||||
@@ -56,7 +56,7 @@ void BedJetClimate::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_preset_to_string(preset)));
|
||||
}
|
||||
for (const auto &preset : traits.get_supported_custom_presets()) {
|
||||
ESP_LOGCONFIG(TAG, " - %s (c)", preset.c_str());
|
||||
ESP_LOGCONFIG(TAG, " - %s (c)", preset);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ void BedJetClimate::reset_state_() {
|
||||
this->target_temperature = NAN;
|
||||
this->current_temperature = NAN;
|
||||
this->preset.reset();
|
||||
this->custom_preset.reset();
|
||||
this->clear_custom_preset_();
|
||||
this->publish_state();
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ void BedJetClimate::control(const ClimateCall &call) {
|
||||
if (button_result) {
|
||||
this->mode = mode;
|
||||
// We're using (custom) preset for Turbo, EXT HT, & M1-3 presets, so changing climate mode will clear those
|
||||
this->custom_preset.reset();
|
||||
this->clear_custom_preset_();
|
||||
this->preset.reset();
|
||||
}
|
||||
}
|
||||
@@ -144,8 +144,7 @@ void BedJetClimate::control(const ClimateCall &call) {
|
||||
|
||||
if (result) {
|
||||
this->mode = CLIMATE_MODE_HEAT;
|
||||
this->preset = CLIMATE_PRESET_BOOST;
|
||||
this->custom_preset.reset();
|
||||
this->set_preset_(CLIMATE_PRESET_BOOST);
|
||||
}
|
||||
} else if (preset == CLIMATE_PRESET_NONE && this->preset.has_value()) {
|
||||
if (this->mode == CLIMATE_MODE_HEAT && this->preset == CLIMATE_PRESET_BOOST) {
|
||||
@@ -153,7 +152,7 @@ void BedJetClimate::control(const ClimateCall &call) {
|
||||
result = this->parent_->send_button(heat_button(this->heating_mode_));
|
||||
if (result) {
|
||||
this->preset.reset();
|
||||
this->custom_preset.reset();
|
||||
this->clear_custom_preset_();
|
||||
}
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Ignoring preset '%s' call; with current mode '%s' and preset '%s'",
|
||||
@@ -164,28 +163,27 @@ void BedJetClimate::control(const ClimateCall &call) {
|
||||
ESP_LOGW(TAG, "Unsupported preset: %d", preset);
|
||||
return;
|
||||
}
|
||||
} else if (call.get_custom_preset().has_value()) {
|
||||
std::string preset = *call.get_custom_preset();
|
||||
} else if (call.has_custom_preset()) {
|
||||
const char *preset = call.get_custom_preset();
|
||||
bool result;
|
||||
|
||||
if (preset == "M1") {
|
||||
if (strcmp(preset, "M1") == 0) {
|
||||
result = this->parent_->button_memory1();
|
||||
} else if (preset == "M2") {
|
||||
} else if (strcmp(preset, "M2") == 0) {
|
||||
result = this->parent_->button_memory2();
|
||||
} else if (preset == "M3") {
|
||||
} else if (strcmp(preset, "M3") == 0) {
|
||||
result = this->parent_->button_memory3();
|
||||
} else if (preset == "LTD HT") {
|
||||
} else if (strcmp(preset, "LTD HT") == 0) {
|
||||
result = this->parent_->button_heat();
|
||||
} else if (preset == "EXT HT") {
|
||||
} else if (strcmp(preset, "EXT HT") == 0) {
|
||||
result = this->parent_->button_ext_heat();
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Unsupported preset: %s", preset.c_str());
|
||||
ESP_LOGW(TAG, "Unsupported preset: %s", preset);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result) {
|
||||
this->custom_preset = preset;
|
||||
this->preset.reset();
|
||||
this->set_custom_preset_(preset);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,19 +205,16 @@ void BedJetClimate::control(const ClimateCall &call) {
|
||||
}
|
||||
|
||||
if (result) {
|
||||
this->fan_mode = fan_mode;
|
||||
this->custom_fan_mode.reset();
|
||||
this->set_fan_mode_(fan_mode);
|
||||
}
|
||||
} else if (call.get_custom_fan_mode().has_value()) {
|
||||
auto fan_mode = *call.get_custom_fan_mode();
|
||||
} else if (call.has_custom_fan_mode()) {
|
||||
const char *fan_mode = call.get_custom_fan_mode();
|
||||
auto fan_index = bedjet_fan_speed_to_step(fan_mode);
|
||||
if (fan_index <= 19) {
|
||||
ESP_LOGV(TAG, "[%s] Converted fan mode %s to bedjet fan step %d", this->get_name().c_str(), fan_mode.c_str(),
|
||||
fan_index);
|
||||
ESP_LOGV(TAG, "[%s] Converted fan mode %s to bedjet fan step %d", this->get_name().c_str(), fan_mode, fan_index);
|
||||
bool result = this->parent_->set_fan_index(fan_index);
|
||||
if (result) {
|
||||
this->custom_fan_mode = fan_mode;
|
||||
this->fan_mode.reset();
|
||||
this->set_custom_fan_mode_(fan_mode);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -245,7 +240,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) {
|
||||
|
||||
const auto *fan_mode_name = bedjet_fan_step_to_fan_mode(data->fan_step);
|
||||
if (fan_mode_name != nullptr) {
|
||||
this->custom_fan_mode = *fan_mode_name;
|
||||
this->set_custom_fan_mode_(fan_mode_name);
|
||||
}
|
||||
|
||||
// TODO: Get biorhythm data to determine which preset (M1-3) is running, if any.
|
||||
@@ -255,7 +250,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) {
|
||||
this->mode = CLIMATE_MODE_OFF;
|
||||
this->action = CLIMATE_ACTION_IDLE;
|
||||
this->fan_mode = CLIMATE_FAN_OFF;
|
||||
this->custom_preset.reset();
|
||||
this->clear_custom_preset_();
|
||||
this->preset.reset();
|
||||
break;
|
||||
|
||||
@@ -266,7 +261,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) {
|
||||
if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
|
||||
this->set_custom_preset_("LTD HT");
|
||||
} else {
|
||||
this->custom_preset.reset();
|
||||
this->clear_custom_preset_();
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -275,7 +270,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) {
|
||||
this->action = CLIMATE_ACTION_HEATING;
|
||||
this->preset.reset();
|
||||
if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
|
||||
this->custom_preset.reset();
|
||||
this->clear_custom_preset_();
|
||||
} else {
|
||||
this->set_custom_preset_("EXT HT");
|
||||
}
|
||||
@@ -284,20 +279,19 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) {
|
||||
case MODE_COOL:
|
||||
this->mode = CLIMATE_MODE_FAN_ONLY;
|
||||
this->action = CLIMATE_ACTION_COOLING;
|
||||
this->custom_preset.reset();
|
||||
this->clear_custom_preset_();
|
||||
this->preset.reset();
|
||||
break;
|
||||
|
||||
case MODE_DRY:
|
||||
this->mode = CLIMATE_MODE_DRY;
|
||||
this->action = CLIMATE_ACTION_DRYING;
|
||||
this->custom_preset.reset();
|
||||
this->clear_custom_preset_();
|
||||
this->preset.reset();
|
||||
break;
|
||||
|
||||
case MODE_TURBO:
|
||||
this->preset = CLIMATE_PRESET_BOOST;
|
||||
this->custom_preset.reset();
|
||||
this->set_preset_(CLIMATE_PRESET_BOOST);
|
||||
this->mode = CLIMATE_MODE_HEAT;
|
||||
this->action = CLIMATE_ACTION_HEATING;
|
||||
break;
|
||||
|
||||
@@ -43,28 +43,20 @@ class BedJetClimate : public climate::Climate, public BedJetClient, public Polli
|
||||
});
|
||||
|
||||
// It would be better if we had a slider for the fan modes.
|
||||
traits.set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES_SET);
|
||||
traits.set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES);
|
||||
traits.set_supported_presets({
|
||||
// If we support NONE, then have to decide what happens if the user switches to it (turn off?)
|
||||
// climate::CLIMATE_PRESET_NONE,
|
||||
// Climate doesn't have a "TURBO" mode, but we can use the BOOST preset instead.
|
||||
climate::CLIMATE_PRESET_BOOST,
|
||||
});
|
||||
// String literals are stored in rodata and valid for program lifetime
|
||||
traits.set_supported_custom_presets({
|
||||
// We could fetch biodata from bedjet and set these names that way.
|
||||
// But then we have to invert the lookup in order to send the right preset.
|
||||
// For now, we can leave them as M1-3 to match the remote buttons.
|
||||
// EXT HT added to match remote button.
|
||||
"EXT HT",
|
||||
this->heating_mode_ == HEAT_MODE_EXTENDED ? "LTD HT" : "EXT HT",
|
||||
"M1",
|
||||
"M2",
|
||||
"M3",
|
||||
});
|
||||
if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
|
||||
traits.add_supported_custom_preset("LTD HT");
|
||||
} else {
|
||||
traits.add_supported_custom_preset("EXT HT");
|
||||
}
|
||||
traits.set_visual_min_temperature(19.0);
|
||||
traits.set_visual_max_temperature(43.0);
|
||||
traits.set_visual_temperature_step(1.0);
|
||||
|
||||
@@ -155,6 +155,7 @@ DelayedOffFilter = binary_sensor_ns.class_("DelayedOffFilter", Filter, cg.Compon
|
||||
InvertFilter = binary_sensor_ns.class_("InvertFilter", Filter)
|
||||
AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter, cg.Component)
|
||||
LambdaFilter = binary_sensor_ns.class_("LambdaFilter", Filter)
|
||||
StatelessLambdaFilter = binary_sensor_ns.class_("StatelessLambdaFilter", Filter)
|
||||
SettleFilter = binary_sensor_ns.class_("SettleFilter", Filter, cg.Component)
|
||||
|
||||
_LOGGER = getLogger(__name__)
|
||||
@@ -264,20 +265,31 @@ async def delayed_off_filter_to_code(config, filter_id):
|
||||
),
|
||||
)
|
||||
async def autorepeat_filter_to_code(config, filter_id):
|
||||
timings = []
|
||||
if len(config) > 0:
|
||||
timings.extend(
|
||||
(conf[CONF_DELAY], conf[CONF_TIME_OFF], conf[CONF_TIME_ON])
|
||||
for conf in config
|
||||
)
|
||||
else:
|
||||
timings.append(
|
||||
(
|
||||
cv.time_period_str_unit(DEFAULT_DELAY).total_milliseconds,
|
||||
cv.time_period_str_unit(DEFAULT_TIME_OFF).total_milliseconds,
|
||||
cv.time_period_str_unit(DEFAULT_TIME_ON).total_milliseconds,
|
||||
timings = [
|
||||
cg.StructInitializer(
|
||||
cg.MockObj("AutorepeatFilterTiming", "esphome::binary_sensor::"),
|
||||
("delay", conf[CONF_DELAY]),
|
||||
("time_off", conf[CONF_TIME_OFF]),
|
||||
("time_on", conf[CONF_TIME_ON]),
|
||||
)
|
||||
)
|
||||
for conf in config
|
||||
]
|
||||
else:
|
||||
timings = [
|
||||
cg.StructInitializer(
|
||||
cg.MockObj("AutorepeatFilterTiming", "esphome::binary_sensor::"),
|
||||
("delay", cv.time_period_str_unit(DEFAULT_DELAY).total_milliseconds),
|
||||
(
|
||||
"time_off",
|
||||
cv.time_period_str_unit(DEFAULT_TIME_OFF).total_milliseconds,
|
||||
),
|
||||
(
|
||||
"time_on",
|
||||
cv.time_period_str_unit(DEFAULT_TIME_ON).total_milliseconds,
|
||||
),
|
||||
)
|
||||
]
|
||||
var = cg.new_Pvariable(filter_id, timings)
|
||||
await cg.register_component(var, {})
|
||||
return var
|
||||
@@ -288,7 +300,7 @@ async def lambda_filter_to_code(config, filter_id):
|
||||
lambda_ = await cg.process_lambda(
|
||||
config, [(bool, "x")], return_type=cg.optional.template(bool)
|
||||
)
|
||||
return cg.new_Pvariable(filter_id, lambda_)
|
||||
return automation.new_lambda_pvariable(filter_id, lambda_, StatelessLambdaFilter)
|
||||
|
||||
|
||||
@register_filter(
|
||||
@@ -536,11 +548,6 @@ def binary_sensor_schema(
|
||||
return _BINARY_SENSOR_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
# Remove before 2025.11.0
|
||||
BINARY_SENSOR_SCHEMA = binary_sensor_schema()
|
||||
BINARY_SENSOR_SCHEMA.add_extra(cv.deprecated_schema_constant("binary_sensor"))
|
||||
|
||||
|
||||
async def setup_binary_sensor_core_(var, config):
|
||||
await setup_entity(var, config, "binary_sensor")
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
#include <cinttypes>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -92,8 +92,8 @@ class DoubleClickTrigger : public Trigger<> {
|
||||
|
||||
class MultiClickTrigger : public Trigger<>, public Component {
|
||||
public:
|
||||
explicit MultiClickTrigger(BinarySensor *parent, std::vector<MultiClickTriggerEvent> timing)
|
||||
: parent_(parent), timing_(std::move(timing)) {}
|
||||
explicit MultiClickTrigger(BinarySensor *parent, std::initializer_list<MultiClickTriggerEvent> timing)
|
||||
: parent_(parent), timing_(timing) {}
|
||||
|
||||
void setup() override {
|
||||
this->last_state_ = this->parent_->get_state_default(false);
|
||||
@@ -115,7 +115,7 @@ class MultiClickTrigger : public Trigger<>, public Component {
|
||||
void trigger_();
|
||||
|
||||
BinarySensor *parent_;
|
||||
std::vector<MultiClickTriggerEvent> timing_;
|
||||
FixedVector<MultiClickTriggerEvent> timing_;
|
||||
uint32_t invalid_cooldown_{1000};
|
||||
optional<size_t> at_index_{};
|
||||
bool last_state_{false};
|
||||
|
||||
@@ -51,7 +51,7 @@ void BinarySensor::add_filter(Filter *filter) {
|
||||
last_filter->next_ = filter;
|
||||
}
|
||||
}
|
||||
void BinarySensor::add_filters(const std::vector<Filter *> &filters) {
|
||||
void BinarySensor::add_filters(std::initializer_list<Filter *> filters) {
|
||||
for (Filter *filter : filters) {
|
||||
this->add_filter(filter);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/binary_sensor/filter.h"
|
||||
|
||||
#include <vector>
|
||||
#include <initializer_list>
|
||||
|
||||
namespace esphome {
|
||||
|
||||
@@ -48,7 +48,7 @@ class BinarySensor : public StatefulEntityBase<bool>, public EntityBase_DeviceCl
|
||||
void publish_initial_state(bool new_state);
|
||||
|
||||
void add_filter(Filter *filter);
|
||||
void add_filters(const std::vector<Filter *> &filters);
|
||||
void add_filters(std::initializer_list<Filter *> filters);
|
||||
|
||||
// ========== INTERNAL METHODS ==========
|
||||
// (In most use cases you won't need these)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#include "filter.h"
|
||||
|
||||
#include "binary_sensor.h"
|
||||
#include <utility>
|
||||
|
||||
namespace esphome {
|
||||
|
||||
@@ -68,7 +67,7 @@ float DelayedOffFilter::get_setup_priority() const { return setup_priority::HARD
|
||||
|
||||
optional<bool> InvertFilter::new_value(bool value) { return !value; }
|
||||
|
||||
AutorepeatFilter::AutorepeatFilter(std::vector<AutorepeatFilterTiming> timings) : timings_(std::move(timings)) {}
|
||||
AutorepeatFilter::AutorepeatFilter(std::initializer_list<AutorepeatFilterTiming> timings) : timings_(timings) {}
|
||||
|
||||
optional<bool> AutorepeatFilter::new_value(bool value) {
|
||||
if (value) {
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace esphome {
|
||||
|
||||
namespace binary_sensor {
|
||||
@@ -82,11 +80,6 @@ class InvertFilter : public Filter {
|
||||
};
|
||||
|
||||
struct AutorepeatFilterTiming {
|
||||
AutorepeatFilterTiming(uint32_t delay, uint32_t off, uint32_t on) {
|
||||
this->delay = delay;
|
||||
this->time_off = off;
|
||||
this->time_on = on;
|
||||
}
|
||||
uint32_t delay;
|
||||
uint32_t time_off;
|
||||
uint32_t time_on;
|
||||
@@ -94,7 +87,7 @@ struct AutorepeatFilterTiming {
|
||||
|
||||
class AutorepeatFilter : public Filter, public Component {
|
||||
public:
|
||||
explicit AutorepeatFilter(std::vector<AutorepeatFilterTiming> timings);
|
||||
explicit AutorepeatFilter(std::initializer_list<AutorepeatFilterTiming> timings);
|
||||
|
||||
optional<bool> new_value(bool value) override;
|
||||
|
||||
@@ -104,7 +97,7 @@ class AutorepeatFilter : public Filter, public Component {
|
||||
void next_timing_();
|
||||
void next_value_(bool val);
|
||||
|
||||
std::vector<AutorepeatFilterTiming> timings_;
|
||||
FixedVector<AutorepeatFilterTiming> timings_;
|
||||
uint8_t active_timing_{0};
|
||||
};
|
||||
|
||||
@@ -118,6 +111,21 @@ class LambdaFilter : public Filter {
|
||||
std::function<optional<bool>(bool)> f_;
|
||||
};
|
||||
|
||||
/** Optimized lambda filter for stateless lambdas (no capture).
|
||||
*
|
||||
* Uses function pointer instead of std::function to reduce memory overhead.
|
||||
* Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function).
|
||||
*/
|
||||
class StatelessLambdaFilter : public Filter {
|
||||
public:
|
||||
explicit StatelessLambdaFilter(optional<bool> (*f)(bool)) : f_(f) {}
|
||||
|
||||
optional<bool> new_value(bool value) override { return this->f_(value); }
|
||||
|
||||
protected:
|
||||
optional<bool> (*f_)(bool);
|
||||
};
|
||||
|
||||
class SettleFilter : public Filter, public Component {
|
||||
public:
|
||||
optional<bool> new_value(bool value) override;
|
||||
|
||||
@@ -96,8 +96,11 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
|
||||
BLEClientWriteAction(BLEClient *ble_client) {
|
||||
ble_client->register_ble_node(this);
|
||||
ble_client_ = ble_client;
|
||||
this->construct_simple_value_();
|
||||
}
|
||||
|
||||
~BLEClientWriteAction() { this->destroy_simple_value_(); }
|
||||
|
||||
void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); }
|
||||
void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
|
||||
void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
|
||||
@@ -106,14 +109,18 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
|
||||
void set_char_uuid32(uint32_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
|
||||
void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
|
||||
|
||||
void set_value_template(std::function<std::vector<uint8_t>(Ts...)> func) {
|
||||
this->value_template_ = std::move(func);
|
||||
has_simple_value_ = false;
|
||||
void set_value_template(std::vector<uint8_t> (*func)(Ts...)) {
|
||||
this->destroy_simple_value_();
|
||||
this->value_.template_func = func;
|
||||
this->has_simple_value_ = false;
|
||||
}
|
||||
|
||||
void set_value_simple(const std::vector<uint8_t> &value) {
|
||||
this->value_simple_ = value;
|
||||
has_simple_value_ = true;
|
||||
if (!this->has_simple_value_) {
|
||||
this->construct_simple_value_();
|
||||
}
|
||||
this->value_.simple = value;
|
||||
this->has_simple_value_ = true;
|
||||
}
|
||||
|
||||
void play(Ts... x) override {}
|
||||
@@ -121,7 +128,7 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
|
||||
void play_complex(Ts... x) override {
|
||||
this->num_running_++;
|
||||
this->var_ = std::make_tuple(x...);
|
||||
auto value = this->has_simple_value_ ? this->value_simple_ : this->value_template_(x...);
|
||||
auto value = this->has_simple_value_ ? this->value_.simple : this->value_.template_func(x...);
|
||||
// on write failure, continue the automation chain rather than stopping so that e.g. disconnect can work.
|
||||
if (!write(value))
|
||||
this->play_next_(x...);
|
||||
@@ -194,10 +201,22 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
|
||||
}
|
||||
|
||||
private:
|
||||
void construct_simple_value_() { new (&this->value_.simple) std::vector<uint8_t>(); }
|
||||
|
||||
void destroy_simple_value_() {
|
||||
if (this->has_simple_value_) {
|
||||
this->value_.simple.~vector();
|
||||
}
|
||||
}
|
||||
|
||||
BLEClient *ble_client_;
|
||||
bool has_simple_value_ = true;
|
||||
std::vector<uint8_t> value_simple_;
|
||||
std::function<std::vector<uint8_t>(Ts...)> value_template_{};
|
||||
union Value {
|
||||
std::vector<uint8_t> simple;
|
||||
std::vector<uint8_t> (*template_func)(Ts...);
|
||||
Value() {} // trivial constructor
|
||||
~Value() {} // trivial destructor - we manage lifetime via discriminator
|
||||
} value_;
|
||||
espbt::ESPBTUUID service_uuid_;
|
||||
espbt::ESPBTUUID char_uuid_;
|
||||
std::tuple<Ts...> var_{};
|
||||
@@ -213,9 +232,9 @@ template<typename... Ts> class BLEClientPasskeyReplyAction : public Action<Ts...
|
||||
void play(Ts... x) override {
|
||||
uint32_t passkey;
|
||||
if (has_simple_value_) {
|
||||
passkey = this->value_simple_;
|
||||
passkey = this->value_.simple;
|
||||
} else {
|
||||
passkey = this->value_template_(x...);
|
||||
passkey = this->value_.template_func(x...);
|
||||
}
|
||||
if (passkey > 999999)
|
||||
return;
|
||||
@@ -224,21 +243,23 @@ template<typename... Ts> class BLEClientPasskeyReplyAction : public Action<Ts...
|
||||
esp_ble_passkey_reply(remote_bda, true, passkey);
|
||||
}
|
||||
|
||||
void set_value_template(std::function<uint32_t(Ts...)> func) {
|
||||
this->value_template_ = std::move(func);
|
||||
has_simple_value_ = false;
|
||||
void set_value_template(uint32_t (*func)(Ts...)) {
|
||||
this->value_.template_func = func;
|
||||
this->has_simple_value_ = false;
|
||||
}
|
||||
|
||||
void set_value_simple(const uint32_t &value) {
|
||||
this->value_simple_ = value;
|
||||
has_simple_value_ = true;
|
||||
this->value_.simple = value;
|
||||
this->has_simple_value_ = true;
|
||||
}
|
||||
|
||||
private:
|
||||
BLEClient *parent_{nullptr};
|
||||
bool has_simple_value_ = true;
|
||||
uint32_t value_simple_{0};
|
||||
std::function<uint32_t(Ts...)> value_template_{};
|
||||
union {
|
||||
uint32_t simple;
|
||||
uint32_t (*template_func)(Ts...);
|
||||
} value_{.simple = 0};
|
||||
};
|
||||
|
||||
template<typename... Ts> class BLEClientNumericComparisonReplyAction : public Action<Ts...> {
|
||||
@@ -249,27 +270,29 @@ template<typename... Ts> class BLEClientNumericComparisonReplyAction : public Ac
|
||||
esp_bd_addr_t remote_bda;
|
||||
memcpy(remote_bda, parent_->get_remote_bda(), sizeof(esp_bd_addr_t));
|
||||
if (has_simple_value_) {
|
||||
esp_ble_confirm_reply(remote_bda, this->value_simple_);
|
||||
esp_ble_confirm_reply(remote_bda, this->value_.simple);
|
||||
} else {
|
||||
esp_ble_confirm_reply(remote_bda, this->value_template_(x...));
|
||||
esp_ble_confirm_reply(remote_bda, this->value_.template_func(x...));
|
||||
}
|
||||
}
|
||||
|
||||
void set_value_template(std::function<bool(Ts...)> func) {
|
||||
this->value_template_ = std::move(func);
|
||||
has_simple_value_ = false;
|
||||
void set_value_template(bool (*func)(Ts...)) {
|
||||
this->value_.template_func = func;
|
||||
this->has_simple_value_ = false;
|
||||
}
|
||||
|
||||
void set_value_simple(const bool &value) {
|
||||
this->value_simple_ = value;
|
||||
has_simple_value_ = true;
|
||||
this->value_.simple = value;
|
||||
this->has_simple_value_ = true;
|
||||
}
|
||||
|
||||
private:
|
||||
BLEClient *parent_{nullptr};
|
||||
bool has_simple_value_ = true;
|
||||
bool value_simple_{false};
|
||||
std::function<bool(Ts...)> value_template_{};
|
||||
union {
|
||||
bool simple;
|
||||
bool (*template_func)(Ts...);
|
||||
} value_{.simple = false};
|
||||
};
|
||||
|
||||
template<typename... Ts> class BLEClientRemoveBondAction : public Action<Ts...> {
|
||||
|
||||
@@ -77,6 +77,9 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
|
||||
}
|
||||
} else {
|
||||
this->node_state = espbt::ClientState::ESTABLISHED;
|
||||
// For non-notify characteristics, trigger an immediate read after service discovery
|
||||
// to avoid peripherals disconnecting due to inactivity
|
||||
this->update();
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -117,9 +120,9 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
|
||||
}
|
||||
|
||||
float BLESensor::parse_data_(uint8_t *value, uint16_t value_len) {
|
||||
if (this->data_to_value_func_.has_value()) {
|
||||
if (this->has_data_to_value_) {
|
||||
std::vector<uint8_t> data(value, value + value_len);
|
||||
return (*this->data_to_value_func_)(data);
|
||||
return this->data_to_value_func_(data);
|
||||
} else {
|
||||
return value[0];
|
||||
}
|
||||
|
||||
@@ -15,8 +15,6 @@ namespace ble_client {
|
||||
|
||||
namespace espbt = esphome::esp32_ble_tracker;
|
||||
|
||||
using data_to_value_t = std::function<float(std::vector<uint8_t>)>;
|
||||
|
||||
class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClientNode {
|
||||
public:
|
||||
void loop() override;
|
||||
@@ -33,13 +31,17 @@ class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClie
|
||||
void set_descr_uuid16(uint16_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); }
|
||||
void set_descr_uuid32(uint32_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
|
||||
void set_descr_uuid128(uint8_t *uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
|
||||
void set_data_to_value(data_to_value_t &&lambda) { this->data_to_value_func_ = lambda; }
|
||||
void set_data_to_value(float (*lambda)(const std::vector<uint8_t> &)) {
|
||||
this->data_to_value_func_ = lambda;
|
||||
this->has_data_to_value_ = true;
|
||||
}
|
||||
void set_enable_notify(bool notify) { this->notify_ = notify; }
|
||||
uint16_t handle;
|
||||
|
||||
protected:
|
||||
float parse_data_(uint8_t *value, uint16_t value_len);
|
||||
optional<data_to_value_t> data_to_value_func_{};
|
||||
bool has_data_to_value_{false};
|
||||
float (*data_to_value_func_)(const std::vector<uint8_t> &){};
|
||||
bool notify_;
|
||||
espbt::ESPBTUUID service_uuid_;
|
||||
espbt::ESPBTUUID char_uuid_;
|
||||
|
||||
@@ -79,6 +79,9 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
}
|
||||
} else {
|
||||
this->node_state = espbt::ClientState::ESTABLISHED;
|
||||
// For non-notify characteristics, trigger an immediate read after service discovery
|
||||
// to avoid peripherals disconnecting due to inactivity
|
||||
this->update();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -84,11 +84,6 @@ def button_schema(
|
||||
return _BUTTON_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
# Remove before 2025.11.0
|
||||
BUTTON_SCHEMA = button_schema(Button)
|
||||
BUTTON_SCHEMA.add_extra(cv.deprecated_schema_constant("button"))
|
||||
|
||||
|
||||
async def setup_button_core_(var, config):
|
||||
await setup_entity(var, config, "button")
|
||||
|
||||
|
||||
@@ -270,11 +270,6 @@ def climate_schema(
|
||||
return _CLIMATE_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
# Remove before 2025.11.0
|
||||
CLIMATE_SCHEMA = climate_schema(Climate)
|
||||
CLIMATE_SCHEMA.add_extra(cv.deprecated_schema_constant("climate"))
|
||||
|
||||
|
||||
async def setup_climate_core_(var, config):
|
||||
await setup_entity(var, config, "climate")
|
||||
|
||||
|
||||
@@ -50,21 +50,21 @@ void ClimateCall::perform() {
|
||||
const LogString *mode_s = climate_mode_to_string(*this->mode_);
|
||||
ESP_LOGD(TAG, " Mode: %s", LOG_STR_ARG(mode_s));
|
||||
}
|
||||
if (this->custom_fan_mode_.has_value()) {
|
||||
if (this->custom_fan_mode_ != nullptr) {
|
||||
this->fan_mode_.reset();
|
||||
ESP_LOGD(TAG, " Custom Fan: %s", this->custom_fan_mode_.value().c_str());
|
||||
ESP_LOGD(TAG, " Custom Fan: %s", this->custom_fan_mode_);
|
||||
}
|
||||
if (this->fan_mode_.has_value()) {
|
||||
this->custom_fan_mode_.reset();
|
||||
this->custom_fan_mode_ = nullptr;
|
||||
const LogString *fan_mode_s = climate_fan_mode_to_string(*this->fan_mode_);
|
||||
ESP_LOGD(TAG, " Fan: %s", LOG_STR_ARG(fan_mode_s));
|
||||
}
|
||||
if (this->custom_preset_.has_value()) {
|
||||
if (this->custom_preset_ != nullptr) {
|
||||
this->preset_.reset();
|
||||
ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset_.value().c_str());
|
||||
ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset_);
|
||||
}
|
||||
if (this->preset_.has_value()) {
|
||||
this->custom_preset_.reset();
|
||||
this->custom_preset_ = nullptr;
|
||||
const LogString *preset_s = climate_preset_to_string(*this->preset_);
|
||||
ESP_LOGD(TAG, " Preset: %s", LOG_STR_ARG(preset_s));
|
||||
}
|
||||
@@ -96,11 +96,10 @@ void ClimateCall::validate_() {
|
||||
this->mode_.reset();
|
||||
}
|
||||
}
|
||||
if (this->custom_fan_mode_.has_value()) {
|
||||
auto custom_fan_mode = *this->custom_fan_mode_;
|
||||
if (!traits.supports_custom_fan_mode(custom_fan_mode)) {
|
||||
ESP_LOGW(TAG, " Fan Mode %s not supported", custom_fan_mode.c_str());
|
||||
this->custom_fan_mode_.reset();
|
||||
if (this->custom_fan_mode_ != nullptr) {
|
||||
if (!traits.supports_custom_fan_mode(this->custom_fan_mode_)) {
|
||||
ESP_LOGW(TAG, " Fan Mode %s not supported", this->custom_fan_mode_);
|
||||
this->custom_fan_mode_ = nullptr;
|
||||
}
|
||||
} else if (this->fan_mode_.has_value()) {
|
||||
auto fan_mode = *this->fan_mode_;
|
||||
@@ -109,11 +108,10 @@ void ClimateCall::validate_() {
|
||||
this->fan_mode_.reset();
|
||||
}
|
||||
}
|
||||
if (this->custom_preset_.has_value()) {
|
||||
auto custom_preset = *this->custom_preset_;
|
||||
if (!traits.supports_custom_preset(custom_preset)) {
|
||||
ESP_LOGW(TAG, " Preset %s not supported", custom_preset.c_str());
|
||||
this->custom_preset_.reset();
|
||||
if (this->custom_preset_ != nullptr) {
|
||||
if (!traits.supports_custom_preset(this->custom_preset_)) {
|
||||
ESP_LOGW(TAG, " Preset %s not supported", this->custom_preset_);
|
||||
this->custom_preset_ = nullptr;
|
||||
}
|
||||
} else if (this->preset_.has_value()) {
|
||||
auto preset = *this->preset_;
|
||||
@@ -186,26 +184,29 @@ ClimateCall &ClimateCall::set_mode(const std::string &mode) {
|
||||
|
||||
ClimateCall &ClimateCall::set_fan_mode(ClimateFanMode fan_mode) {
|
||||
this->fan_mode_ = fan_mode;
|
||||
this->custom_fan_mode_.reset();
|
||||
this->custom_fan_mode_ = nullptr;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) {
|
||||
ClimateCall &ClimateCall::set_fan_mode(const char *custom_fan_mode) {
|
||||
// Check if it's a standard enum mode first
|
||||
for (const auto &mode_entry : CLIMATE_FAN_MODES_BY_STR) {
|
||||
if (str_equals_case_insensitive(fan_mode, mode_entry.str)) {
|
||||
this->set_fan_mode(static_cast<ClimateFanMode>(mode_entry.value));
|
||||
return *this;
|
||||
if (str_equals_case_insensitive(custom_fan_mode, mode_entry.str)) {
|
||||
return this->set_fan_mode(static_cast<ClimateFanMode>(mode_entry.value));
|
||||
}
|
||||
}
|
||||
if (this->parent_->get_traits().supports_custom_fan_mode(fan_mode)) {
|
||||
this->custom_fan_mode_ = fan_mode;
|
||||
// Find the matching pointer from parent climate device
|
||||
if (const char *mode_ptr = this->parent_->find_custom_fan_mode_(custom_fan_mode)) {
|
||||
this->custom_fan_mode_ = mode_ptr;
|
||||
this->fan_mode_.reset();
|
||||
} else {
|
||||
ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), fan_mode.c_str());
|
||||
return *this;
|
||||
}
|
||||
ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), custom_fan_mode);
|
||||
return *this;
|
||||
}
|
||||
|
||||
ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) { return this->set_fan_mode(fan_mode.c_str()); }
|
||||
|
||||
ClimateCall &ClimateCall::set_fan_mode(optional<std::string> fan_mode) {
|
||||
if (fan_mode.has_value()) {
|
||||
this->set_fan_mode(fan_mode.value());
|
||||
@@ -215,26 +216,29 @@ ClimateCall &ClimateCall::set_fan_mode(optional<std::string> fan_mode) {
|
||||
|
||||
ClimateCall &ClimateCall::set_preset(ClimatePreset preset) {
|
||||
this->preset_ = preset;
|
||||
this->custom_preset_.reset();
|
||||
this->custom_preset_ = nullptr;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ClimateCall &ClimateCall::set_preset(const std::string &preset) {
|
||||
ClimateCall &ClimateCall::set_preset(const char *custom_preset) {
|
||||
// Check if it's a standard enum preset first
|
||||
for (const auto &preset_entry : CLIMATE_PRESETS_BY_STR) {
|
||||
if (str_equals_case_insensitive(preset, preset_entry.str)) {
|
||||
this->set_preset(static_cast<ClimatePreset>(preset_entry.value));
|
||||
return *this;
|
||||
if (str_equals_case_insensitive(custom_preset, preset_entry.str)) {
|
||||
return this->set_preset(static_cast<ClimatePreset>(preset_entry.value));
|
||||
}
|
||||
}
|
||||
if (this->parent_->get_traits().supports_custom_preset(preset)) {
|
||||
this->custom_preset_ = preset;
|
||||
// Find the matching pointer from parent climate device
|
||||
if (const char *preset_ptr = this->parent_->find_custom_preset_(custom_preset)) {
|
||||
this->custom_preset_ = preset_ptr;
|
||||
this->preset_.reset();
|
||||
} else {
|
||||
ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), preset.c_str());
|
||||
return *this;
|
||||
}
|
||||
ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), custom_preset);
|
||||
return *this;
|
||||
}
|
||||
|
||||
ClimateCall &ClimateCall::set_preset(const std::string &preset) { return this->set_preset(preset.c_str()); }
|
||||
|
||||
ClimateCall &ClimateCall::set_preset(optional<std::string> preset) {
|
||||
if (preset.has_value()) {
|
||||
this->set_preset(preset.value());
|
||||
@@ -287,8 +291,6 @@ const optional<ClimateMode> &ClimateCall::get_mode() const { return this->mode_;
|
||||
const optional<ClimateFanMode> &ClimateCall::get_fan_mode() const { return this->fan_mode_; }
|
||||
const optional<ClimateSwingMode> &ClimateCall::get_swing_mode() const { return this->swing_mode_; }
|
||||
const optional<ClimatePreset> &ClimateCall::get_preset() const { return this->preset_; }
|
||||
const optional<std::string> &ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; }
|
||||
const optional<std::string> &ClimateCall::get_custom_preset() const { return this->custom_preset_; }
|
||||
|
||||
ClimateCall &ClimateCall::set_target_temperature_high(optional<float> target_temperature_high) {
|
||||
this->target_temperature_high_ = target_temperature_high;
|
||||
@@ -317,13 +319,13 @@ ClimateCall &ClimateCall::set_mode(optional<ClimateMode> mode) {
|
||||
|
||||
ClimateCall &ClimateCall::set_fan_mode(optional<ClimateFanMode> fan_mode) {
|
||||
this->fan_mode_ = fan_mode;
|
||||
this->custom_fan_mode_.reset();
|
||||
this->custom_fan_mode_ = nullptr;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ClimateCall &ClimateCall::set_preset(optional<ClimatePreset> preset) {
|
||||
this->preset_ = preset;
|
||||
this->custom_preset_.reset();
|
||||
this->custom_preset_ = nullptr;
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -382,30 +384,34 @@ void Climate::save_state_() {
|
||||
state.uses_custom_fan_mode = false;
|
||||
state.fan_mode = this->fan_mode.value();
|
||||
}
|
||||
if (!traits.get_supported_custom_fan_modes().empty() && custom_fan_mode.has_value()) {
|
||||
if (!traits.get_supported_custom_fan_modes().empty() && this->has_custom_fan_mode()) {
|
||||
state.uses_custom_fan_mode = true;
|
||||
const auto &supported = traits.get_supported_custom_fan_modes();
|
||||
std::vector<std::string> vec{supported.begin(), supported.end()};
|
||||
for (size_t i = 0; i < vec.size(); i++) {
|
||||
if (vec[i] == custom_fan_mode) {
|
||||
// std::vector maintains insertion order
|
||||
size_t i = 0;
|
||||
for (const char *mode : supported) {
|
||||
if (strcmp(mode, this->custom_fan_mode_) == 0) {
|
||||
state.custom_fan_mode = i;
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
if (traits.get_supports_presets() && preset.has_value()) {
|
||||
state.uses_custom_preset = false;
|
||||
state.preset = this->preset.value();
|
||||
}
|
||||
if (!traits.get_supported_custom_presets().empty() && custom_preset.has_value()) {
|
||||
if (!traits.get_supported_custom_presets().empty() && this->has_custom_preset()) {
|
||||
state.uses_custom_preset = true;
|
||||
const auto &supported = traits.get_supported_custom_presets();
|
||||
std::vector<std::string> vec{supported.begin(), supported.end()};
|
||||
for (size_t i = 0; i < vec.size(); i++) {
|
||||
if (vec[i] == custom_preset) {
|
||||
// std::vector maintains insertion order
|
||||
size_t i = 0;
|
||||
for (const char *preset : supported) {
|
||||
if (strcmp(preset, this->custom_preset_) == 0) {
|
||||
state.custom_preset = i;
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
if (traits.get_supports_swing_modes()) {
|
||||
@@ -426,14 +432,14 @@ void Climate::publish_state() {
|
||||
if (traits.get_supports_fan_modes() && this->fan_mode.has_value()) {
|
||||
ESP_LOGD(TAG, " Fan Mode: %s", LOG_STR_ARG(climate_fan_mode_to_string(this->fan_mode.value())));
|
||||
}
|
||||
if (!traits.get_supported_custom_fan_modes().empty() && this->custom_fan_mode.has_value()) {
|
||||
ESP_LOGD(TAG, " Custom Fan Mode: %s", this->custom_fan_mode.value().c_str());
|
||||
if (!traits.get_supported_custom_fan_modes().empty() && this->has_custom_fan_mode()) {
|
||||
ESP_LOGD(TAG, " Custom Fan Mode: %s", this->custom_fan_mode_);
|
||||
}
|
||||
if (traits.get_supports_presets() && this->preset.has_value()) {
|
||||
ESP_LOGD(TAG, " Preset: %s", LOG_STR_ARG(climate_preset_to_string(this->preset.value())));
|
||||
}
|
||||
if (!traits.get_supported_custom_presets().empty() && this->custom_preset.has_value()) {
|
||||
ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset.value().c_str());
|
||||
if (!traits.get_supported_custom_presets().empty() && this->has_custom_preset()) {
|
||||
ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset_);
|
||||
}
|
||||
if (traits.get_supports_swing_modes()) {
|
||||
ESP_LOGD(TAG, " Swing Mode: %s", LOG_STR_ARG(climate_swing_mode_to_string(this->swing_mode)));
|
||||
@@ -520,13 +526,23 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) {
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
|
||||
call.set_target_humidity(this->target_humidity);
|
||||
}
|
||||
if (traits.get_supports_fan_modes() || !traits.get_supported_custom_fan_modes().empty()) {
|
||||
if (this->uses_custom_fan_mode) {
|
||||
if (this->custom_fan_mode < traits.get_supported_custom_fan_modes().size()) {
|
||||
call.fan_mode_.reset();
|
||||
call.custom_fan_mode_ = traits.get_supported_custom_fan_modes()[this->custom_fan_mode];
|
||||
}
|
||||
} else if (traits.supports_fan_mode(this->fan_mode)) {
|
||||
call.set_fan_mode(this->fan_mode);
|
||||
}
|
||||
if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) {
|
||||
if (this->uses_custom_preset) {
|
||||
if (this->custom_preset < traits.get_supported_custom_presets().size()) {
|
||||
call.preset_.reset();
|
||||
call.custom_preset_ = traits.get_supported_custom_presets()[this->custom_preset];
|
||||
}
|
||||
} else if (traits.supports_preset(this->preset)) {
|
||||
call.set_preset(this->preset);
|
||||
}
|
||||
if (traits.get_supports_swing_modes()) {
|
||||
if (traits.supports_swing_mode(this->swing_mode)) {
|
||||
call.set_swing_mode(this->swing_mode);
|
||||
}
|
||||
return call;
|
||||
@@ -545,56 +561,131 @@ void ClimateDeviceRestoreState::apply(Climate *climate) {
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
|
||||
climate->target_humidity = this->target_humidity;
|
||||
}
|
||||
if (traits.get_supports_fan_modes() && !this->uses_custom_fan_mode) {
|
||||
if (this->uses_custom_fan_mode) {
|
||||
if (this->custom_fan_mode < traits.get_supported_custom_fan_modes().size()) {
|
||||
climate->fan_mode.reset();
|
||||
climate->custom_fan_mode_ = traits.get_supported_custom_fan_modes()[this->custom_fan_mode];
|
||||
}
|
||||
} else if (traits.supports_fan_mode(this->fan_mode)) {
|
||||
climate->fan_mode = this->fan_mode;
|
||||
climate->clear_custom_fan_mode_();
|
||||
}
|
||||
if (!traits.get_supported_custom_fan_modes().empty() && this->uses_custom_fan_mode) {
|
||||
// std::set has consistent order (lexicographic for strings), so this is ok
|
||||
const auto &modes = traits.get_supported_custom_fan_modes();
|
||||
std::vector<std::string> modes_vec{modes.begin(), modes.end()};
|
||||
if (custom_fan_mode < modes_vec.size()) {
|
||||
climate->custom_fan_mode = modes_vec[this->custom_fan_mode];
|
||||
if (this->uses_custom_preset) {
|
||||
if (this->custom_preset < traits.get_supported_custom_presets().size()) {
|
||||
climate->preset.reset();
|
||||
climate->custom_preset_ = traits.get_supported_custom_presets()[this->custom_preset];
|
||||
}
|
||||
}
|
||||
if (traits.get_supports_presets() && !this->uses_custom_preset) {
|
||||
} else if (traits.supports_preset(this->preset)) {
|
||||
climate->preset = this->preset;
|
||||
climate->clear_custom_preset_();
|
||||
}
|
||||
if (!traits.get_supported_custom_presets().empty() && uses_custom_preset) {
|
||||
// std::set has consistent order (lexicographic for strings), so this is ok
|
||||
const auto &presets = traits.get_supported_custom_presets();
|
||||
std::vector<std::string> presets_vec{presets.begin(), presets.end()};
|
||||
if (custom_preset < presets_vec.size()) {
|
||||
climate->custom_preset = presets_vec[this->custom_preset];
|
||||
}
|
||||
}
|
||||
if (traits.get_supports_swing_modes()) {
|
||||
if (traits.supports_swing_mode(this->swing_mode)) {
|
||||
climate->swing_mode = this->swing_mode;
|
||||
}
|
||||
climate->publish_state();
|
||||
}
|
||||
|
||||
template<typename T1, typename T2> bool set_alternative(optional<T1> &dst, optional<T2> &alt, const T1 &src) {
|
||||
bool is_changed = alt.has_value();
|
||||
alt.reset();
|
||||
if (is_changed || dst != src) {
|
||||
dst = src;
|
||||
is_changed = true;
|
||||
/** Template helper for setting primary modes (fan_mode, preset) with mutual exclusion.
|
||||
*
|
||||
* Climate devices have mutually exclusive mode pairs:
|
||||
* - fan_mode (enum) vs custom_fan_mode_ (const char*)
|
||||
* - preset (enum) vs custom_preset_ (const char*)
|
||||
*
|
||||
* Only one mode in each pair can be active at a time. This helper ensures setting a primary
|
||||
* mode automatically clears its corresponding custom mode.
|
||||
*
|
||||
* Example state transitions:
|
||||
* Before: custom_fan_mode_="Turbo", fan_mode=nullopt
|
||||
* Call: set_fan_mode_(CLIMATE_FAN_HIGH)
|
||||
* After: custom_fan_mode_=nullptr, fan_mode=CLIMATE_FAN_HIGH
|
||||
*
|
||||
* @param primary The primary mode optional (fan_mode or preset)
|
||||
* @param custom_ptr Reference to the custom mode pointer (custom_fan_mode_ or custom_preset_)
|
||||
* @param value The new primary mode value to set
|
||||
* @return true if state changed, false if already set to this value
|
||||
*/
|
||||
template<typename T> bool set_primary_mode(optional<T> &primary, const char *&custom_ptr, T value) {
|
||||
// Clear the custom mode (mutual exclusion)
|
||||
bool changed = custom_ptr != nullptr;
|
||||
custom_ptr = nullptr;
|
||||
// Set the primary mode
|
||||
if (changed || !primary.has_value() || primary.value() != value) {
|
||||
primary = value;
|
||||
return true;
|
||||
}
|
||||
return is_changed;
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Template helper for setting custom modes (custom_fan_mode_, custom_preset_) with mutual exclusion.
|
||||
*
|
||||
* This helper ensures setting a custom mode automatically clears its corresponding primary mode.
|
||||
* It also validates that the custom mode exists in the device's supported modes (lifetime safety).
|
||||
*
|
||||
* Example state transitions:
|
||||
* Before: fan_mode=CLIMATE_FAN_HIGH, custom_fan_mode_=nullptr
|
||||
* Call: set_custom_fan_mode_("Turbo")
|
||||
* After: fan_mode=nullopt, custom_fan_mode_="Turbo" (pointer from traits)
|
||||
*
|
||||
* Lifetime Safety:
|
||||
* - found_ptr must come from traits.find_custom_*_mode_()
|
||||
* - Only pointers found in traits are stored, ensuring they remain valid
|
||||
* - Prevents dangling pointers from temporary strings
|
||||
*
|
||||
* @param custom_ptr Reference to the custom mode pointer to set
|
||||
* @param primary The primary mode optional to clear
|
||||
* @param found_ptr The validated pointer from traits (nullptr if not found)
|
||||
* @param has_custom Whether a custom mode is currently active
|
||||
* @return true if state changed, false otherwise
|
||||
*/
|
||||
template<typename T>
|
||||
bool set_custom_mode(const char *&custom_ptr, optional<T> &primary, const char *found_ptr, bool has_custom) {
|
||||
if (found_ptr != nullptr) {
|
||||
// Clear the primary mode (mutual exclusion)
|
||||
bool changed = primary.has_value();
|
||||
primary.reset();
|
||||
// Set the custom mode (pointer is validated by caller from traits)
|
||||
if (changed || custom_ptr != found_ptr) {
|
||||
custom_ptr = found_ptr;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// Mode not found in supported modes, clear it if currently set
|
||||
if (has_custom) {
|
||||
custom_ptr = nullptr;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Climate::set_fan_mode_(ClimateFanMode mode) {
|
||||
return set_alternative(this->fan_mode, this->custom_fan_mode, mode);
|
||||
return set_primary_mode(this->fan_mode, this->custom_fan_mode_, mode);
|
||||
}
|
||||
|
||||
bool Climate::set_custom_fan_mode_(const std::string &mode) {
|
||||
return set_alternative(this->custom_fan_mode, this->fan_mode, mode);
|
||||
bool Climate::set_custom_fan_mode_(const char *mode) {
|
||||
auto traits = this->get_traits();
|
||||
return set_custom_mode<ClimateFanMode>(this->custom_fan_mode_, this->fan_mode, traits.find_custom_fan_mode_(mode),
|
||||
this->has_custom_fan_mode());
|
||||
}
|
||||
|
||||
bool Climate::set_preset_(ClimatePreset preset) { return set_alternative(this->preset, this->custom_preset, preset); }
|
||||
void Climate::clear_custom_fan_mode_() { this->custom_fan_mode_ = nullptr; }
|
||||
|
||||
bool Climate::set_custom_preset_(const std::string &preset) {
|
||||
return set_alternative(this->custom_preset, this->preset, preset);
|
||||
bool Climate::set_preset_(ClimatePreset preset) { return set_primary_mode(this->preset, this->custom_preset_, preset); }
|
||||
|
||||
bool Climate::set_custom_preset_(const char *preset) {
|
||||
auto traits = this->get_traits();
|
||||
return set_custom_mode<ClimatePreset>(this->custom_preset_, this->preset, traits.find_custom_preset_(preset),
|
||||
this->has_custom_preset());
|
||||
}
|
||||
|
||||
void Climate::clear_custom_preset_() { this->custom_preset_ = nullptr; }
|
||||
|
||||
const char *Climate::find_custom_fan_mode_(const char *custom_fan_mode) {
|
||||
return this->get_traits().find_custom_fan_mode_(custom_fan_mode);
|
||||
}
|
||||
|
||||
const char *Climate::find_custom_preset_(const char *custom_preset) {
|
||||
return this->get_traits().find_custom_preset_(custom_preset);
|
||||
}
|
||||
|
||||
void Climate::dump_traits_(const char *tag) {
|
||||
@@ -646,8 +737,8 @@ void Climate::dump_traits_(const char *tag) {
|
||||
}
|
||||
if (!traits.get_supported_custom_fan_modes().empty()) {
|
||||
ESP_LOGCONFIG(tag, " Supported custom fan modes:");
|
||||
for (const std::string &s : traits.get_supported_custom_fan_modes())
|
||||
ESP_LOGCONFIG(tag, " - %s", s.c_str());
|
||||
for (const char *s : traits.get_supported_custom_fan_modes())
|
||||
ESP_LOGCONFIG(tag, " - %s", s);
|
||||
}
|
||||
if (!traits.get_supported_presets().empty()) {
|
||||
ESP_LOGCONFIG(tag, " Supported presets:");
|
||||
@@ -656,8 +747,8 @@ void Climate::dump_traits_(const char *tag) {
|
||||
}
|
||||
if (!traits.get_supported_custom_presets().empty()) {
|
||||
ESP_LOGCONFIG(tag, " Supported custom presets:");
|
||||
for (const std::string &s : traits.get_supported_custom_presets())
|
||||
ESP_LOGCONFIG(tag, " - %s", s.c_str());
|
||||
for (const char *s : traits.get_supported_custom_presets())
|
||||
ESP_LOGCONFIG(tag, " - %s", s);
|
||||
}
|
||||
if (!traits.get_supported_swing_modes().empty()) {
|
||||
ESP_LOGCONFIG(tag, " Supported swing modes:");
|
||||
|
||||
@@ -33,6 +33,7 @@ class Climate;
|
||||
class ClimateCall {
|
||||
public:
|
||||
explicit ClimateCall(Climate *parent) : parent_(parent) {}
|
||||
friend struct ClimateDeviceRestoreState;
|
||||
|
||||
/// Set the mode of the climate device.
|
||||
ClimateCall &set_mode(ClimateMode mode);
|
||||
@@ -76,6 +77,8 @@ class ClimateCall {
|
||||
ClimateCall &set_fan_mode(const std::string &fan_mode);
|
||||
/// Set the fan mode of the climate device based on a string.
|
||||
ClimateCall &set_fan_mode(optional<std::string> fan_mode);
|
||||
/// Set the custom fan mode of the climate device.
|
||||
ClimateCall &set_fan_mode(const char *custom_fan_mode);
|
||||
/// Set the swing mode of the climate device.
|
||||
ClimateCall &set_swing_mode(ClimateSwingMode swing_mode);
|
||||
/// Set the swing mode of the climate device.
|
||||
@@ -90,6 +93,8 @@ class ClimateCall {
|
||||
ClimateCall &set_preset(const std::string &preset);
|
||||
/// Set the preset of the climate device based on a string.
|
||||
ClimateCall &set_preset(optional<std::string> preset);
|
||||
/// Set the custom preset of the climate device.
|
||||
ClimateCall &set_preset(const char *custom_preset);
|
||||
|
||||
void perform();
|
||||
|
||||
@@ -102,8 +107,10 @@ class ClimateCall {
|
||||
const optional<ClimateFanMode> &get_fan_mode() const;
|
||||
const optional<ClimateSwingMode> &get_swing_mode() const;
|
||||
const optional<ClimatePreset> &get_preset() const;
|
||||
const optional<std::string> &get_custom_fan_mode() const;
|
||||
const optional<std::string> &get_custom_preset() const;
|
||||
const char *get_custom_fan_mode() const { return this->custom_fan_mode_; }
|
||||
const char *get_custom_preset() const { return this->custom_preset_; }
|
||||
bool has_custom_fan_mode() const { return this->custom_fan_mode_ != nullptr; }
|
||||
bool has_custom_preset() const { return this->custom_preset_ != nullptr; }
|
||||
|
||||
protected:
|
||||
void validate_();
|
||||
@@ -117,8 +124,10 @@ class ClimateCall {
|
||||
optional<ClimateFanMode> fan_mode_;
|
||||
optional<ClimateSwingMode> swing_mode_;
|
||||
optional<ClimatePreset> preset_;
|
||||
optional<std::string> custom_fan_mode_;
|
||||
optional<std::string> custom_preset_;
|
||||
|
||||
private:
|
||||
const char *custom_fan_mode_{nullptr};
|
||||
const char *custom_preset_{nullptr};
|
||||
};
|
||||
|
||||
/// Struct used to save the state of the climate device in restore memory.
|
||||
@@ -211,6 +220,12 @@ class Climate : public EntityBase {
|
||||
void set_visual_min_humidity_override(float visual_min_humidity_override);
|
||||
void set_visual_max_humidity_override(float visual_max_humidity_override);
|
||||
|
||||
/// Check if a custom fan mode is currently active.
|
||||
bool has_custom_fan_mode() const { return this->custom_fan_mode_ != nullptr; }
|
||||
|
||||
/// Check if a custom preset is currently active.
|
||||
bool has_custom_preset() const { return this->custom_preset_ != nullptr; }
|
||||
|
||||
/// The current temperature of the climate device, as reported from the integration.
|
||||
float current_temperature{NAN};
|
||||
|
||||
@@ -237,12 +252,6 @@ class Climate : public EntityBase {
|
||||
/// The active preset of the climate device.
|
||||
optional<ClimatePreset> preset;
|
||||
|
||||
/// The active custom fan mode of the climate device.
|
||||
optional<std::string> custom_fan_mode;
|
||||
|
||||
/// The active custom preset mode of the climate device.
|
||||
optional<std::string> custom_preset;
|
||||
|
||||
/// The active mode of the climate device.
|
||||
ClimateMode mode{CLIMATE_MODE_OFF};
|
||||
|
||||
@@ -252,20 +261,37 @@ class Climate : public EntityBase {
|
||||
/// The active swing mode of the climate device.
|
||||
ClimateSwingMode swing_mode{CLIMATE_SWING_OFF};
|
||||
|
||||
/// Get the active custom fan mode (read-only access).
|
||||
const char *get_custom_fan_mode() const { return this->custom_fan_mode_; }
|
||||
|
||||
/// Get the active custom preset (read-only access).
|
||||
const char *get_custom_preset() const { return this->custom_preset_; }
|
||||
|
||||
protected:
|
||||
friend ClimateCall;
|
||||
friend struct ClimateDeviceRestoreState;
|
||||
|
||||
/// Set fan mode. Reset custom fan mode. Return true if fan mode has been changed.
|
||||
bool set_fan_mode_(ClimateFanMode mode);
|
||||
|
||||
/// Set custom fan mode. Reset primary fan mode. Return true if fan mode has been changed.
|
||||
bool set_custom_fan_mode_(const std::string &mode);
|
||||
bool set_custom_fan_mode_(const char *mode);
|
||||
/// Clear custom fan mode.
|
||||
void clear_custom_fan_mode_();
|
||||
|
||||
/// Set preset. Reset custom preset. Return true if preset has been changed.
|
||||
bool set_preset_(ClimatePreset preset);
|
||||
|
||||
/// Set custom preset. Reset primary preset. Return true if preset has been changed.
|
||||
bool set_custom_preset_(const std::string &preset);
|
||||
bool set_custom_preset_(const char *preset);
|
||||
/// Clear custom preset.
|
||||
void clear_custom_preset_();
|
||||
|
||||
/// Find and return the matching custom fan mode pointer from traits, or nullptr if not found.
|
||||
const char *find_custom_fan_mode_(const char *custom_fan_mode);
|
||||
|
||||
/// Find and return the matching custom preset pointer from traits, or nullptr if not found.
|
||||
const char *find_custom_preset_(const char *custom_preset);
|
||||
|
||||
/** Get the default traits of this climate device.
|
||||
*
|
||||
@@ -302,6 +328,21 @@ class Climate : public EntityBase {
|
||||
optional<float> visual_current_temperature_step_override_{};
|
||||
optional<float> visual_min_humidity_override_{};
|
||||
optional<float> visual_max_humidity_override_{};
|
||||
|
||||
private:
|
||||
/** The active custom fan mode (private - enforces use of safe setters).
|
||||
*
|
||||
* Points to an entry in traits.supported_custom_fan_modes_ or nullptr.
|
||||
* Use get_custom_fan_mode() to read, set_custom_fan_mode_() to modify.
|
||||
*/
|
||||
const char *custom_fan_mode_{nullptr};
|
||||
|
||||
/** The active custom preset (private - enforces use of safe setters).
|
||||
*
|
||||
* Points to an entry in traits.supported_custom_presets_ or nullptr.
|
||||
* Use get_custom_preset() to read, set_custom_preset_() to modify.
|
||||
*/
|
||||
const char *custom_preset_{nullptr};
|
||||
};
|
||||
|
||||
} // namespace climate
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace esphome {
|
||||
namespace climate {
|
||||
|
||||
/// Enum for all modes a climate device can be in.
|
||||
/// NOTE: If adding values, update ClimateModeMask in climate_traits.h to use the new last value
|
||||
enum ClimateMode : uint8_t {
|
||||
/// The climate device is off
|
||||
CLIMATE_MODE_OFF = 0,
|
||||
@@ -24,7 +25,7 @@ enum ClimateMode : uint8_t {
|
||||
* For example, the target temperature can be adjusted based on a schedule, or learned behavior.
|
||||
* The target temperature can't be adjusted when in this mode.
|
||||
*/
|
||||
CLIMATE_MODE_AUTO = 6
|
||||
CLIMATE_MODE_AUTO = 6 // Update ClimateModeMask in climate_traits.h if adding values after this
|
||||
};
|
||||
|
||||
/// Enum for the current action of the climate device. Values match those of ClimateMode.
|
||||
@@ -43,6 +44,7 @@ enum ClimateAction : uint8_t {
|
||||
CLIMATE_ACTION_FAN = 6,
|
||||
};
|
||||
|
||||
/// NOTE: If adding values, update ClimateFanModeMask in climate_traits.h to use the new last value
|
||||
enum ClimateFanMode : uint8_t {
|
||||
/// The fan mode is set to On
|
||||
CLIMATE_FAN_ON = 0,
|
||||
@@ -63,10 +65,11 @@ enum ClimateFanMode : uint8_t {
|
||||
/// The fan mode is set to Diffuse
|
||||
CLIMATE_FAN_DIFFUSE = 8,
|
||||
/// The fan mode is set to Quiet
|
||||
CLIMATE_FAN_QUIET = 9,
|
||||
CLIMATE_FAN_QUIET = 9, // Update ClimateFanModeMask in climate_traits.h if adding values after this
|
||||
};
|
||||
|
||||
/// Enum for all modes a climate swing can be in
|
||||
/// NOTE: If adding values, update ClimateSwingModeMask in climate_traits.h to use the new last value
|
||||
enum ClimateSwingMode : uint8_t {
|
||||
/// The swing mode is set to Off
|
||||
CLIMATE_SWING_OFF = 0,
|
||||
@@ -75,10 +78,11 @@ enum ClimateSwingMode : uint8_t {
|
||||
/// The fan mode is set to Vertical
|
||||
CLIMATE_SWING_VERTICAL = 2,
|
||||
/// The fan mode is set to Horizontal
|
||||
CLIMATE_SWING_HORIZONTAL = 3,
|
||||
CLIMATE_SWING_HORIZONTAL = 3, // Update ClimateSwingModeMask in climate_traits.h if adding values after this
|
||||
};
|
||||
|
||||
/// Enum for all preset modes
|
||||
/// NOTE: If adding values, update ClimatePresetMask in climate_traits.h to use the new last value
|
||||
enum ClimatePreset : uint8_t {
|
||||
/// No preset is active
|
||||
CLIMATE_PRESET_NONE = 0,
|
||||
@@ -95,7 +99,7 @@ enum ClimatePreset : uint8_t {
|
||||
/// Device is prepared for sleep
|
||||
CLIMATE_PRESET_SLEEP = 6,
|
||||
/// Device is reacting to activity (e.g., movement sensors)
|
||||
CLIMATE_PRESET_ACTIVITY = 7,
|
||||
CLIMATE_PRESET_ACTIVITY = 7, // Update ClimatePresetMask in climate_traits.h if adding values after this
|
||||
};
|
||||
|
||||
enum ClimateFeature : uint32_t {
|
||||
|
||||
@@ -1,19 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include <set>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
#include "climate_mode.h"
|
||||
#include "esphome/core/finite_set_mask.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
|
||||
#ifdef USE_API
|
||||
namespace api {
|
||||
class APIConnection;
|
||||
} // namespace api
|
||||
#endif
|
||||
|
||||
namespace climate {
|
||||
|
||||
// Type aliases for climate enum bitmasks
|
||||
// These replace std::set<EnumType> to eliminate red-black tree overhead
|
||||
// For contiguous enums starting at 0, DefaultBitPolicy provides 1:1 mapping (enum value = bit position)
|
||||
// Bitmask size is automatically calculated from the last enum value
|
||||
using ClimateModeMask = FiniteSetMask<ClimateMode, DefaultBitPolicy<ClimateMode, CLIMATE_MODE_AUTO + 1>>;
|
||||
using ClimateFanModeMask = FiniteSetMask<ClimateFanMode, DefaultBitPolicy<ClimateFanMode, CLIMATE_FAN_QUIET + 1>>;
|
||||
using ClimateSwingModeMask =
|
||||
FiniteSetMask<ClimateSwingMode, DefaultBitPolicy<ClimateSwingMode, CLIMATE_SWING_HORIZONTAL + 1>>;
|
||||
using ClimatePresetMask = FiniteSetMask<ClimatePreset, DefaultBitPolicy<ClimatePreset, CLIMATE_PRESET_ACTIVITY + 1>>;
|
||||
|
||||
// Lightweight linear search for small vectors (1-20 items) of const char* pointers
|
||||
// Avoids std::find template overhead
|
||||
inline bool vector_contains(const std::vector<const char *> &vec, const char *value) {
|
||||
for (const char *item : vec) {
|
||||
if (strcmp(item, value) == 0)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Find and return matching pointer from vector, or nullptr if not found
|
||||
inline const char *vector_find(const std::vector<const char *> &vec, const char *value) {
|
||||
for (const char *item : vec) {
|
||||
if (strcmp(item, value) == 0)
|
||||
return item;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/** This class contains all static data for climate devices.
|
||||
*
|
||||
* All climate devices must support these features:
|
||||
@@ -41,7 +65,11 @@ namespace climate {
|
||||
* - temperature step - the step with which to increase/decrease target temperature.
|
||||
* This also affects with how many decimal places the temperature is shown
|
||||
*/
|
||||
class Climate; // Forward declaration
|
||||
|
||||
class ClimateTraits {
|
||||
friend class Climate; // Allow Climate to access protected find methods
|
||||
|
||||
public:
|
||||
/// Get/set feature flags (see ClimateFeatures enum in climate_mode.h)
|
||||
uint32_t get_feature_flags() const { return this->feature_flags_; }
|
||||
@@ -107,48 +135,74 @@ class ClimateTraits {
|
||||
}
|
||||
}
|
||||
|
||||
void set_supported_modes(std::set<ClimateMode> modes) { this->supported_modes_ = std::move(modes); }
|
||||
void set_supported_modes(ClimateModeMask modes) { this->supported_modes_ = modes; }
|
||||
void add_supported_mode(ClimateMode mode) { this->supported_modes_.insert(mode); }
|
||||
bool supports_mode(ClimateMode mode) const { return this->supported_modes_.count(mode); }
|
||||
const std::set<ClimateMode> &get_supported_modes() const { return this->supported_modes_; }
|
||||
const ClimateModeMask &get_supported_modes() const { return this->supported_modes_; }
|
||||
|
||||
void set_supported_fan_modes(std::set<ClimateFanMode> modes) { this->supported_fan_modes_ = std::move(modes); }
|
||||
void set_supported_fan_modes(ClimateFanModeMask modes) { this->supported_fan_modes_ = modes; }
|
||||
void add_supported_fan_mode(ClimateFanMode mode) { this->supported_fan_modes_.insert(mode); }
|
||||
void add_supported_custom_fan_mode(const std::string &mode) { this->supported_custom_fan_modes_.insert(mode); }
|
||||
bool supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.count(fan_mode); }
|
||||
bool get_supports_fan_modes() const {
|
||||
return !this->supported_fan_modes_.empty() || !this->supported_custom_fan_modes_.empty();
|
||||
}
|
||||
const std::set<ClimateFanMode> &get_supported_fan_modes() const { return this->supported_fan_modes_; }
|
||||
const ClimateFanModeMask &get_supported_fan_modes() const { return this->supported_fan_modes_; }
|
||||
|
||||
void set_supported_custom_fan_modes(std::set<std::string> supported_custom_fan_modes) {
|
||||
this->supported_custom_fan_modes_ = std::move(supported_custom_fan_modes);
|
||||
void set_supported_custom_fan_modes(std::initializer_list<const char *> modes) {
|
||||
this->supported_custom_fan_modes_ = modes;
|
||||
}
|
||||
void set_supported_custom_fan_modes(const std::vector<const char *> &modes) {
|
||||
this->supported_custom_fan_modes_ = modes;
|
||||
}
|
||||
template<size_t N> void set_supported_custom_fan_modes(const char *const (&modes)[N]) {
|
||||
this->supported_custom_fan_modes_.assign(modes, modes + N);
|
||||
}
|
||||
|
||||
// Deleted overloads to catch incorrect std::string usage at compile time with clear error messages
|
||||
void set_supported_custom_fan_modes(const std::vector<std::string> &modes) = delete;
|
||||
void set_supported_custom_fan_modes(std::initializer_list<std::string> modes) = delete;
|
||||
|
||||
const std::vector<const char *> &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; }
|
||||
bool supports_custom_fan_mode(const char *custom_fan_mode) const {
|
||||
return vector_contains(this->supported_custom_fan_modes_, custom_fan_mode);
|
||||
}
|
||||
const std::set<std::string> &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; }
|
||||
bool supports_custom_fan_mode(const std::string &custom_fan_mode) const {
|
||||
return this->supported_custom_fan_modes_.count(custom_fan_mode);
|
||||
return this->supports_custom_fan_mode(custom_fan_mode.c_str());
|
||||
}
|
||||
|
||||
void set_supported_presets(std::set<ClimatePreset> presets) { this->supported_presets_ = std::move(presets); }
|
||||
void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; }
|
||||
void add_supported_preset(ClimatePreset preset) { this->supported_presets_.insert(preset); }
|
||||
void add_supported_custom_preset(const std::string &preset) { this->supported_custom_presets_.insert(preset); }
|
||||
bool supports_preset(ClimatePreset preset) const { return this->supported_presets_.count(preset); }
|
||||
bool get_supports_presets() const { return !this->supported_presets_.empty(); }
|
||||
const std::set<climate::ClimatePreset> &get_supported_presets() const { return this->supported_presets_; }
|
||||
const ClimatePresetMask &get_supported_presets() const { return this->supported_presets_; }
|
||||
|
||||
void set_supported_custom_presets(std::set<std::string> supported_custom_presets) {
|
||||
this->supported_custom_presets_ = std::move(supported_custom_presets);
|
||||
void set_supported_custom_presets(std::initializer_list<const char *> presets) {
|
||||
this->supported_custom_presets_ = presets;
|
||||
}
|
||||
void set_supported_custom_presets(const std::vector<const char *> &presets) {
|
||||
this->supported_custom_presets_ = presets;
|
||||
}
|
||||
template<size_t N> void set_supported_custom_presets(const char *const (&presets)[N]) {
|
||||
this->supported_custom_presets_.assign(presets, presets + N);
|
||||
}
|
||||
|
||||
// Deleted overloads to catch incorrect std::string usage at compile time with clear error messages
|
||||
void set_supported_custom_presets(const std::vector<std::string> &presets) = delete;
|
||||
void set_supported_custom_presets(std::initializer_list<std::string> presets) = delete;
|
||||
|
||||
const std::vector<const char *> &get_supported_custom_presets() const { return this->supported_custom_presets_; }
|
||||
bool supports_custom_preset(const char *custom_preset) const {
|
||||
return vector_contains(this->supported_custom_presets_, custom_preset);
|
||||
}
|
||||
const std::set<std::string> &get_supported_custom_presets() const { return this->supported_custom_presets_; }
|
||||
bool supports_custom_preset(const std::string &custom_preset) const {
|
||||
return this->supported_custom_presets_.count(custom_preset);
|
||||
return this->supports_custom_preset(custom_preset.c_str());
|
||||
}
|
||||
|
||||
void set_supported_swing_modes(std::set<ClimateSwingMode> modes) { this->supported_swing_modes_ = std::move(modes); }
|
||||
void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; }
|
||||
void add_supported_swing_mode(ClimateSwingMode mode) { this->supported_swing_modes_.insert(mode); }
|
||||
bool supports_swing_mode(ClimateSwingMode swing_mode) const { return this->supported_swing_modes_.count(swing_mode); }
|
||||
bool get_supports_swing_modes() const { return !this->supported_swing_modes_.empty(); }
|
||||
const std::set<ClimateSwingMode> &get_supported_swing_modes() const { return this->supported_swing_modes_; }
|
||||
const ClimateSwingModeMask &get_supported_swing_modes() const { return this->supported_swing_modes_; }
|
||||
|
||||
float get_visual_min_temperature() const { return this->visual_min_temperature_; }
|
||||
void set_visual_min_temperature(float visual_min_temperature) {
|
||||
@@ -179,23 +233,6 @@ class ClimateTraits {
|
||||
void set_visual_max_humidity(float visual_max_humidity) { this->visual_max_humidity_ = visual_max_humidity; }
|
||||
|
||||
protected:
|
||||
#ifdef USE_API
|
||||
// The API connection is a friend class to access internal methods
|
||||
friend class api::APIConnection;
|
||||
// These methods return references to internal data structures.
|
||||
// They are used by the API to avoid copying data when encoding messages.
|
||||
// Warning: Do not use these methods outside of the API connection code.
|
||||
// They return references to internal data that can be invalidated.
|
||||
const std::set<ClimateMode> &get_supported_modes_for_api_() const { return this->supported_modes_; }
|
||||
const std::set<ClimateFanMode> &get_supported_fan_modes_for_api_() const { return this->supported_fan_modes_; }
|
||||
const std::set<std::string> &get_supported_custom_fan_modes_for_api_() const {
|
||||
return this->supported_custom_fan_modes_;
|
||||
}
|
||||
const std::set<climate::ClimatePreset> &get_supported_presets_for_api_() const { return this->supported_presets_; }
|
||||
const std::set<std::string> &get_supported_custom_presets_for_api_() const { return this->supported_custom_presets_; }
|
||||
const std::set<ClimateSwingMode> &get_supported_swing_modes_for_api_() const { return this->supported_swing_modes_; }
|
||||
#endif
|
||||
|
||||
void set_mode_support_(climate::ClimateMode mode, bool supported) {
|
||||
if (supported) {
|
||||
this->supported_modes_.insert(mode);
|
||||
@@ -218,6 +255,18 @@ class ClimateTraits {
|
||||
}
|
||||
}
|
||||
|
||||
/// Find and return the matching custom fan mode pointer from supported modes, or nullptr if not found
|
||||
/// This is protected as it's an implementation detail - use Climate::find_custom_fan_mode_() instead
|
||||
const char *find_custom_fan_mode_(const char *custom_fan_mode) const {
|
||||
return vector_find(this->supported_custom_fan_modes_, custom_fan_mode);
|
||||
}
|
||||
|
||||
/// Find and return the matching custom preset pointer from supported presets, or nullptr if not found
|
||||
/// This is protected as it's an implementation detail - use Climate::find_custom_preset_() instead
|
||||
const char *find_custom_preset_(const char *custom_preset) const {
|
||||
return vector_find(this->supported_custom_presets_, custom_preset);
|
||||
}
|
||||
|
||||
uint32_t feature_flags_{0};
|
||||
float visual_min_temperature_{10};
|
||||
float visual_max_temperature_{30};
|
||||
@@ -226,12 +275,21 @@ class ClimateTraits {
|
||||
float visual_min_humidity_{30};
|
||||
float visual_max_humidity_{99};
|
||||
|
||||
std::set<climate::ClimateMode> supported_modes_ = {climate::CLIMATE_MODE_OFF};
|
||||
std::set<climate::ClimateFanMode> supported_fan_modes_;
|
||||
std::set<climate::ClimateSwingMode> supported_swing_modes_;
|
||||
std::set<climate::ClimatePreset> supported_presets_;
|
||||
std::set<std::string> supported_custom_fan_modes_;
|
||||
std::set<std::string> supported_custom_presets_;
|
||||
climate::ClimateModeMask supported_modes_{climate::CLIMATE_MODE_OFF};
|
||||
climate::ClimateFanModeMask supported_fan_modes_;
|
||||
climate::ClimateSwingModeMask supported_swing_modes_;
|
||||
climate::ClimatePresetMask supported_presets_;
|
||||
|
||||
/** Custom mode storage using const char* pointers to eliminate std::string overhead.
|
||||
*
|
||||
* Pointers must remain valid for the ClimateTraits lifetime. Safe patterns:
|
||||
* - String literals: set_supported_custom_fan_modes({"Turbo", "Silent"})
|
||||
* - Static const data: static const char* MODE = "Eco";
|
||||
*
|
||||
* Climate class setters validate pointers are from these vectors before storing.
|
||||
*/
|
||||
std::vector<const char *> supported_custom_fan_modes_;
|
||||
std::vector<const char *> supported_custom_presets_;
|
||||
};
|
||||
|
||||
} // namespace climate
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import logging
|
||||
|
||||
from esphome import core
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import climate, remote_base, sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT
|
||||
from esphome.const import CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -52,26 +51,6 @@ def climate_ir_with_receiver_schema(
|
||||
)
|
||||
|
||||
|
||||
# Remove before 2025.11.0
|
||||
def deprecated_schema_constant(config):
|
||||
type: str = "unknown"
|
||||
if (id := config.get(CONF_ID)) is not None and isinstance(id, core.ID):
|
||||
type = str(id.type).split("::", maxsplit=1)[0]
|
||||
_LOGGER.warning(
|
||||
"Using `climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA` is deprecated and will be removed in ESPHome 2025.11.0. "
|
||||
"Please use `climate_ir.climate_ir_with_receiver_schema(...)` instead. "
|
||||
"If you are seeing this, report an issue to the external_component author and ask them to update it. "
|
||||
"https://developers.esphome.io/blog/2025/05/14/_schema-deprecations/. "
|
||||
"Component using this schema: %s",
|
||||
type,
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
CLIMATE_IR_WITH_RECEIVER_SCHEMA = climate_ir_with_receiver_schema(ClimateIR)
|
||||
CLIMATE_IR_WITH_RECEIVER_SCHEMA.add_extra(deprecated_schema_constant)
|
||||
|
||||
|
||||
async def register_climate_ir(var, config):
|
||||
await cg.register_component(var, config)
|
||||
await remote_base.register_transmittable(var, config)
|
||||
|
||||
@@ -24,16 +24,18 @@ class ClimateIR : public Component,
|
||||
public remote_base::RemoteTransmittable {
|
||||
public:
|
||||
ClimateIR(float minimum_temperature, float maximum_temperature, float temperature_step = 1.0f,
|
||||
bool supports_dry = false, bool supports_fan_only = false, std::set<climate::ClimateFanMode> fan_modes = {},
|
||||
std::set<climate::ClimateSwingMode> swing_modes = {}, std::set<climate::ClimatePreset> presets = {}) {
|
||||
bool supports_dry = false, bool supports_fan_only = false,
|
||||
climate::ClimateFanModeMask fan_modes = climate::ClimateFanModeMask(),
|
||||
climate::ClimateSwingModeMask swing_modes = climate::ClimateSwingModeMask(),
|
||||
climate::ClimatePresetMask presets = climate::ClimatePresetMask()) {
|
||||
this->minimum_temperature_ = minimum_temperature;
|
||||
this->maximum_temperature_ = maximum_temperature;
|
||||
this->temperature_step_ = temperature_step;
|
||||
this->supports_dry_ = supports_dry;
|
||||
this->supports_fan_only_ = supports_fan_only;
|
||||
this->fan_modes_ = std::move(fan_modes);
|
||||
this->swing_modes_ = std::move(swing_modes);
|
||||
this->presets_ = std::move(presets);
|
||||
this->fan_modes_ = fan_modes;
|
||||
this->swing_modes_ = swing_modes;
|
||||
this->presets_ = presets;
|
||||
}
|
||||
|
||||
void setup() override;
|
||||
@@ -60,9 +62,9 @@ class ClimateIR : public Component,
|
||||
bool supports_heat_{true};
|
||||
bool supports_dry_{false};
|
||||
bool supports_fan_only_{false};
|
||||
std::set<climate::ClimateFanMode> fan_modes_ = {};
|
||||
std::set<climate::ClimateSwingMode> swing_modes_ = {};
|
||||
std::set<climate::ClimatePreset> presets_ = {};
|
||||
climate::ClimateFanModeMask fan_modes_{};
|
||||
climate::ClimateSwingModeMask swing_modes_{};
|
||||
climate::ClimatePresetMask presets_{};
|
||||
|
||||
sensor::Sensor *sensor_{nullptr};
|
||||
};
|
||||
|
||||
@@ -151,11 +151,6 @@ def cover_schema(
|
||||
return _COVER_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
# Remove before 2025.11.0
|
||||
COVER_SCHEMA = cover_schema(Cover)
|
||||
COVER_SCHEMA.add_extra(cv.deprecated_schema_constant("cover"))
|
||||
|
||||
|
||||
async def setup_cover_core_(var, config):
|
||||
await setup_entity(var, config, "cover")
|
||||
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
|
||||
#define BOOTLOADER_VERSION_REGISTER NRF_TIMER2->CC[0]
|
||||
|
||||
namespace esphome {
|
||||
namespace debug {
|
||||
namespace esphome::debug {
|
||||
|
||||
static const char *const TAG = "debug";
|
||||
constexpr std::uintptr_t MBR_PARAM_PAGE_ADDR = 0xFFC;
|
||||
@@ -281,14 +280,18 @@ void DebugComponent::get_device_info_(std::string &device_info) {
|
||||
NRF_FICR->INFO.VARIANT & 0xFF, package(NRF_FICR->INFO.PACKAGE));
|
||||
ESP_LOGD(TAG, "RAM: %ukB, Flash: %ukB, production test: %sdone", NRF_FICR->INFO.RAM, NRF_FICR->INFO.FLASH,
|
||||
(NRF_FICR->PRODTEST[0] == 0xBB42319F ? "" : "not "));
|
||||
bool n_reset_enabled = NRF_UICR->PSELRESET[0] == NRF_UICR->PSELRESET[1] &&
|
||||
(NRF_UICR->PSELRESET[0] & UICR_PSELRESET_CONNECT_Msk) == UICR_PSELRESET_CONNECT_Connected
|
||||
<< UICR_PSELRESET_CONNECT_Pos;
|
||||
ESP_LOGD(
|
||||
TAG, "GPIO as NFC pins: %s, GPIO as nRESET pin: %s",
|
||||
YESNO((NRF_UICR->NFCPINS & UICR_NFCPINS_PROTECT_Msk) == (UICR_NFCPINS_PROTECT_NFC << UICR_NFCPINS_PROTECT_Pos)),
|
||||
YESNO(((NRF_UICR->PSELRESET[0] & UICR_PSELRESET_CONNECT_Msk) !=
|
||||
(UICR_PSELRESET_CONNECT_Connected << UICR_PSELRESET_CONNECT_Pos)) ||
|
||||
((NRF_UICR->PSELRESET[1] & UICR_PSELRESET_CONNECT_Msk) !=
|
||||
(UICR_PSELRESET_CONNECT_Connected << UICR_PSELRESET_CONNECT_Pos))));
|
||||
|
||||
YESNO(n_reset_enabled));
|
||||
if (n_reset_enabled) {
|
||||
uint8_t port = (NRF_UICR->PSELRESET[0] & UICR_PSELRESET_PORT_Msk) >> UICR_PSELRESET_PORT_Pos;
|
||||
uint8_t pin = (NRF_UICR->PSELRESET[0] & UICR_PSELRESET_PIN_Msk) >> UICR_PSELRESET_PIN_Pos;
|
||||
ESP_LOGD(TAG, "nRESET port P%u.%02u", port, pin);
|
||||
}
|
||||
#ifdef USE_BOOTLOADER_MCUBOOT
|
||||
ESP_LOGD(TAG, "bootloader: mcuboot");
|
||||
#else
|
||||
@@ -322,10 +325,22 @@ void DebugComponent::get_device_info_(std::string &device_info) {
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
auto uicr = [](volatile uint32_t *data, uint8_t size) {
|
||||
std::string res;
|
||||
char buf[sizeof(uint32_t) * 2 + 1];
|
||||
for (size_t i = 0; i < size; i++) {
|
||||
if (i > 0) {
|
||||
res += ' ';
|
||||
}
|
||||
res += format_hex_pretty<uint32_t>(data[i], '\0', false);
|
||||
}
|
||||
return res;
|
||||
};
|
||||
ESP_LOGD(TAG, "NRFFW %s", uicr(NRF_UICR->NRFFW, 13).c_str());
|
||||
ESP_LOGD(TAG, "NRFHW %s", uicr(NRF_UICR->NRFHW, 12).c_str());
|
||||
}
|
||||
|
||||
void DebugComponent::update_platform_() {}
|
||||
|
||||
} // namespace debug
|
||||
} // namespace esphome
|
||||
} // namespace esphome::debug
|
||||
#endif
|
||||
|
||||
@@ -28,16 +28,16 @@ class DemoClimate : public climate::Climate, public Component {
|
||||
this->mode = climate::CLIMATE_MODE_AUTO;
|
||||
this->action = climate::CLIMATE_ACTION_COOLING;
|
||||
this->fan_mode = climate::CLIMATE_FAN_HIGH;
|
||||
this->custom_preset = {"My Preset"};
|
||||
this->set_custom_preset_("My Preset");
|
||||
break;
|
||||
case DemoClimateType::TYPE_3:
|
||||
this->current_temperature = 21.5;
|
||||
this->target_temperature_low = 21.0;
|
||||
this->target_temperature_high = 22.5;
|
||||
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
|
||||
this->custom_fan_mode = {"Auto Low"};
|
||||
this->set_custom_fan_mode_("Auto Low");
|
||||
this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL;
|
||||
this->preset = climate::CLIMATE_PRESET_AWAY;
|
||||
this->set_preset_(climate::CLIMATE_PRESET_AWAY);
|
||||
break;
|
||||
}
|
||||
this->publish_state();
|
||||
@@ -58,23 +58,19 @@ class DemoClimate : public climate::Climate, public Component {
|
||||
this->target_temperature_high = *call.get_target_temperature_high();
|
||||
}
|
||||
if (call.get_fan_mode().has_value()) {
|
||||
this->fan_mode = *call.get_fan_mode();
|
||||
this->custom_fan_mode.reset();
|
||||
this->set_fan_mode_(*call.get_fan_mode());
|
||||
}
|
||||
if (call.get_swing_mode().has_value()) {
|
||||
this->swing_mode = *call.get_swing_mode();
|
||||
}
|
||||
if (call.get_custom_fan_mode().has_value()) {
|
||||
this->custom_fan_mode = *call.get_custom_fan_mode();
|
||||
this->fan_mode.reset();
|
||||
if (call.has_custom_fan_mode()) {
|
||||
this->set_custom_fan_mode_(call.get_custom_fan_mode());
|
||||
}
|
||||
if (call.get_preset().has_value()) {
|
||||
this->preset = *call.get_preset();
|
||||
this->custom_preset.reset();
|
||||
this->set_preset_(*call.get_preset());
|
||||
}
|
||||
if (call.get_custom_preset().has_value()) {
|
||||
this->custom_preset = *call.get_custom_preset();
|
||||
this->preset.reset();
|
||||
if (call.has_custom_preset()) {
|
||||
this->set_custom_preset_(call.get_custom_preset());
|
||||
}
|
||||
this->publish_state();
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#include "e131_addressable_light_effect.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace esphome {
|
||||
namespace e131 {
|
||||
|
||||
@@ -76,14 +78,14 @@ void E131Component::loop() {
|
||||
}
|
||||
|
||||
void E131Component::add_effect(E131AddressableLightEffect *light_effect) {
|
||||
if (light_effects_.count(light_effect)) {
|
||||
if (std::find(light_effects_.begin(), light_effects_.end(), light_effect) != light_effects_.end()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Registering '%s' for universes %d-%d.", light_effect->get_name().c_str(),
|
||||
light_effect->get_first_universe(), light_effect->get_last_universe());
|
||||
ESP_LOGD(TAG, "Registering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(),
|
||||
light_effect->get_last_universe());
|
||||
|
||||
light_effects_.insert(light_effect);
|
||||
light_effects_.push_back(light_effect);
|
||||
|
||||
for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) {
|
||||
join_(universe);
|
||||
@@ -91,14 +93,17 @@ void E131Component::add_effect(E131AddressableLightEffect *light_effect) {
|
||||
}
|
||||
|
||||
void E131Component::remove_effect(E131AddressableLightEffect *light_effect) {
|
||||
if (!light_effects_.count(light_effect)) {
|
||||
auto it = std::find(light_effects_.begin(), light_effects_.end(), light_effect);
|
||||
if (it == light_effects_.end()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Unregistering '%s' for universes %d-%d.", light_effect->get_name().c_str(),
|
||||
light_effect->get_first_universe(), light_effect->get_last_universe());
|
||||
ESP_LOGD(TAG, "Unregistering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(),
|
||||
light_effect->get_last_universe());
|
||||
|
||||
light_effects_.erase(light_effect);
|
||||
// Swap with last element and pop for O(1) removal (order doesn't matter)
|
||||
*it = light_effects_.back();
|
||||
light_effects_.pop_back();
|
||||
|
||||
for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) {
|
||||
leave_(universe);
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#include <cinttypes>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome {
|
||||
@@ -47,9 +46,8 @@ class E131Component : public esphome::Component {
|
||||
|
||||
E131ListenMethod listen_method_{E131_MULTICAST};
|
||||
std::unique_ptr<socket::Socket> socket_;
|
||||
std::set<E131AddressableLightEffect *> light_effects_;
|
||||
std::vector<E131AddressableLightEffect *> light_effects_;
|
||||
std::map<int, int> universe_consumers_;
|
||||
std::map<int, E131Packet> universe_packets_;
|
||||
};
|
||||
|
||||
} // namespace e131
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace e131 {
|
||||
static const char *const TAG = "e131_addressable_light_effect";
|
||||
static const int MAX_DATA_SIZE = (sizeof(E131Packet::values) - 1);
|
||||
|
||||
E131AddressableLightEffect::E131AddressableLightEffect(const std::string &name) : AddressableLightEffect(name) {}
|
||||
E131AddressableLightEffect::E131AddressableLightEffect(const char *name) : AddressableLightEffect(name) {}
|
||||
|
||||
int E131AddressableLightEffect::get_data_per_universe() const { return get_lights_per_universe() * channels_; }
|
||||
|
||||
@@ -58,8 +58,8 @@ bool E131AddressableLightEffect::process_(int universe, const E131Packet &packet
|
||||
std::min(it->size(), std::min(output_offset + get_lights_per_universe(), output_offset + packet.count - 1));
|
||||
auto *input_data = packet.values + 1;
|
||||
|
||||
ESP_LOGV(TAG, "Applying data for '%s' on %d universe, for %" PRId32 "-%d.", get_name().c_str(), universe,
|
||||
output_offset, output_end);
|
||||
ESP_LOGV(TAG, "Applying data for '%s' on %d universe, for %" PRId32 "-%d.", get_name(), universe, output_offset,
|
||||
output_end);
|
||||
|
||||
switch (channels_) {
|
||||
case E131_MONO:
|
||||
|
||||
@@ -13,7 +13,7 @@ enum E131LightChannels { E131_MONO = 1, E131_RGB = 3, E131_RGBW = 4 };
|
||||
|
||||
class E131AddressableLightEffect : public light::AddressableLightEffect {
|
||||
public:
|
||||
E131AddressableLightEffect(const std::string &name);
|
||||
E131AddressableLightEffect(const char *name);
|
||||
|
||||
void start() override;
|
||||
void stop() override;
|
||||
|
||||
@@ -304,9 +304,13 @@ def _format_framework_arduino_version(ver: cv.Version) -> str:
|
||||
def _format_framework_espidf_version(ver: cv.Version, release: str) -> str:
|
||||
# format the given espidf (https://github.com/pioarduino/esp-idf/releases) version to
|
||||
# a PIO platformio/framework-espidf value
|
||||
if ver == cv.Version(5, 4, 3) or ver >= cv.Version(5, 5, 1):
|
||||
ext = "tar.xz"
|
||||
else:
|
||||
ext = "zip"
|
||||
if release:
|
||||
return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}.{release}/esp-idf-v{str(ver)}.zip"
|
||||
return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.zip"
|
||||
return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}.{release}/esp-idf-v{str(ver)}.{ext}"
|
||||
return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.{ext}"
|
||||
|
||||
|
||||
def _is_framework_url(source: str) -> str:
|
||||
@@ -355,6 +359,7 @@ ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
|
||||
ESP_IDF_PLATFORM_VERSION_LOOKUP = {
|
||||
cv.Version(5, 5, 1): cv.Version(55, 3, 31, "1"),
|
||||
cv.Version(5, 5, 0): cv.Version(55, 3, 31, "1"),
|
||||
cv.Version(5, 4, 3): cv.Version(55, 3, 32),
|
||||
cv.Version(5, 4, 2): cv.Version(54, 3, 21, "2"),
|
||||
cv.Version(5, 4, 1): cv.Version(54, 3, 21, "2"),
|
||||
cv.Version(5, 4, 0): cv.Version(54, 3, 21, "2"),
|
||||
@@ -550,6 +555,32 @@ CONF_ENABLE_LWIP_BRIDGE_INTERFACE = "enable_lwip_bridge_interface"
|
||||
CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING = "enable_lwip_tcpip_core_locking"
|
||||
CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY = "enable_lwip_check_thread_safety"
|
||||
CONF_DISABLE_LIBC_LOCKS_IN_IRAM = "disable_libc_locks_in_iram"
|
||||
CONF_DISABLE_VFS_SUPPORT_TERMIOS = "disable_vfs_support_termios"
|
||||
CONF_DISABLE_VFS_SUPPORT_SELECT = "disable_vfs_support_select"
|
||||
CONF_DISABLE_VFS_SUPPORT_DIR = "disable_vfs_support_dir"
|
||||
|
||||
# VFS requirement tracking
|
||||
# Components that need VFS features can call require_vfs_select() or require_vfs_dir()
|
||||
KEY_VFS_SELECT_REQUIRED = "vfs_select_required"
|
||||
KEY_VFS_DIR_REQUIRED = "vfs_dir_required"
|
||||
|
||||
|
||||
def require_vfs_select() -> None:
|
||||
"""Mark that VFS select support is required by a component.
|
||||
|
||||
Call this from components that use esp_vfs_eventfd or other VFS select features.
|
||||
This prevents CONFIG_VFS_SUPPORT_SELECT from being disabled.
|
||||
"""
|
||||
CORE.data[KEY_VFS_SELECT_REQUIRED] = True
|
||||
|
||||
|
||||
def require_vfs_dir() -> None:
|
||||
"""Mark that VFS directory support is required by a component.
|
||||
|
||||
Call this from components that use directory functions (opendir, readdir, mkdir, etc.).
|
||||
This prevents CONFIG_VFS_SUPPORT_DIR from being disabled.
|
||||
"""
|
||||
CORE.data[KEY_VFS_DIR_REQUIRED] = True
|
||||
|
||||
|
||||
def _validate_idf_component(config: ConfigType) -> ConfigType:
|
||||
@@ -615,6 +646,13 @@ FRAMEWORK_SCHEMA = cv.All(
|
||||
cv.Optional(
|
||||
CONF_DISABLE_LIBC_LOCKS_IN_IRAM, default=True
|
||||
): cv.boolean,
|
||||
cv.Optional(
|
||||
CONF_DISABLE_VFS_SUPPORT_TERMIOS, default=True
|
||||
): cv.boolean,
|
||||
cv.Optional(
|
||||
CONF_DISABLE_VFS_SUPPORT_SELECT, default=True
|
||||
): cv.boolean,
|
||||
cv.Optional(CONF_DISABLE_VFS_SUPPORT_DIR, default=True): cv.boolean,
|
||||
cv.Optional(CONF_EXECUTE_FROM_PSRAM): cv.boolean,
|
||||
}
|
||||
),
|
||||
@@ -844,6 +882,11 @@ async def to_code(config):
|
||||
for clean_var in ("IDF_PATH", "IDF_TOOLS_PATH"):
|
||||
os.environ.pop(clean_var, None)
|
||||
|
||||
# Set the location of the IDF component manager cache
|
||||
os.environ["IDF_COMPONENT_CACHE_PATH"] = str(
|
||||
CORE.relative_internal_path(".espressif")
|
||||
)
|
||||
|
||||
add_extra_script(
|
||||
"post",
|
||||
"post_build.py",
|
||||
@@ -962,6 +1005,43 @@ async def to_code(config):
|
||||
if advanced.get(CONF_DISABLE_LIBC_LOCKS_IN_IRAM, True):
|
||||
add_idf_sdkconfig_option("CONFIG_LIBC_LOCKS_PLACE_IN_IRAM", False)
|
||||
|
||||
# Disable VFS support for termios (terminal I/O functions)
|
||||
# ESPHome doesn't use termios functions on ESP32 (only used in host UART driver).
|
||||
# Saves approximately 1.8KB of flash when disabled (default).
|
||||
add_idf_sdkconfig_option(
|
||||
"CONFIG_VFS_SUPPORT_TERMIOS",
|
||||
not advanced.get(CONF_DISABLE_VFS_SUPPORT_TERMIOS, True),
|
||||
)
|
||||
|
||||
# Disable VFS support for select() with file descriptors
|
||||
# ESPHome only uses select() with sockets via lwip_select(), which still works.
|
||||
# VFS select is only needed for UART/eventfd file descriptors.
|
||||
# Components that need it (e.g., openthread) call require_vfs_select().
|
||||
# Saves approximately 2.7KB of flash when disabled (default).
|
||||
if CORE.data.get(KEY_VFS_SELECT_REQUIRED, False):
|
||||
# Component requires VFS select - force enable regardless of user setting
|
||||
add_idf_sdkconfig_option("CONFIG_VFS_SUPPORT_SELECT", True)
|
||||
else:
|
||||
# No component needs it - allow user to control (default: disabled)
|
||||
add_idf_sdkconfig_option(
|
||||
"CONFIG_VFS_SUPPORT_SELECT",
|
||||
not advanced.get(CONF_DISABLE_VFS_SUPPORT_SELECT, True),
|
||||
)
|
||||
|
||||
# Disable VFS support for directory functions (opendir, readdir, mkdir, etc.)
|
||||
# ESPHome doesn't use directory functions on ESP32.
|
||||
# Components that need it (e.g., storage components) call require_vfs_dir().
|
||||
# Saves approximately 0.5KB+ of flash when disabled (default).
|
||||
if CORE.data.get(KEY_VFS_DIR_REQUIRED, False):
|
||||
# Component requires VFS directory support - force enable regardless of user setting
|
||||
add_idf_sdkconfig_option("CONFIG_VFS_SUPPORT_DIR", True)
|
||||
else:
|
||||
# No component needs it - allow user to control (default: disabled)
|
||||
add_idf_sdkconfig_option(
|
||||
"CONFIG_VFS_SUPPORT_DIR",
|
||||
not advanced.get(CONF_DISABLE_VFS_SUPPORT_DIR, True),
|
||||
)
|
||||
|
||||
cg.add_platformio_option("board_build.partitions", "partitions.csv")
|
||||
if CONF_PARTITIONS in config:
|
||||
add_extra_build_file(
|
||||
|
||||
@@ -96,7 +96,11 @@ void loop_task(void *pv_params) {
|
||||
|
||||
extern "C" void app_main() {
|
||||
esp32::setup_preferences();
|
||||
#if CONFIG_FREERTOS_UNICORE
|
||||
xTaskCreate(loop_task, "loopTask", 8192, nullptr, 1, &loop_task_handle);
|
||||
#else
|
||||
xTaskCreatePinnedToCore(loop_task, "loopTask", 8192, nullptr, 1, &loop_task_handle, 1);
|
||||
#endif
|
||||
}
|
||||
#endif // USE_ESP_IDF
|
||||
|
||||
|
||||
@@ -40,13 +40,13 @@ class ESP32InternalGPIOPin : public InternalGPIOPin {
|
||||
// - 3 bytes for members below
|
||||
// - 1 byte padding for alignment
|
||||
// - 4 bytes for vtable pointer
|
||||
uint8_t pin_; // GPIO pin number (0-255, actual max ~54 on ESP32)
|
||||
gpio::Flags flags_; // GPIO flags (1 byte)
|
||||
uint8_t pin_; // GPIO pin number (0-255, actual max ~54 on ESP32)
|
||||
gpio::Flags flags_{}; // GPIO flags (1 byte)
|
||||
struct PinFlags {
|
||||
uint8_t inverted : 1; // Invert pin logic (1 bit)
|
||||
uint8_t drive_strength : 2; // Drive strength 0-3 (2 bits)
|
||||
uint8_t reserved : 5; // Reserved for future use (5 bits)
|
||||
} pin_flags_; // Total: 1 byte
|
||||
} pin_flags_{}; // Total: 1 byte
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
static bool isr_service_installed;
|
||||
};
|
||||
|
||||
@@ -223,7 +223,10 @@ async def esp32_pin_to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
num = config[CONF_NUMBER]
|
||||
cg.add(var.set_pin(getattr(gpio_num_t, f"GPIO_NUM_{num}")))
|
||||
cg.add(var.set_inverted(config[CONF_INVERTED]))
|
||||
# Only set if true to avoid bloating setup() function
|
||||
# (inverted bit in pin_flags_ bitfield is zero-initialized to false)
|
||||
if config[CONF_INVERTED]:
|
||||
cg.add(var.set_inverted(True))
|
||||
if CONF_DRIVE_STRENGTH in config:
|
||||
cg.add(var.set_drive_strength(config[CONF_DRIVE_STRENGTH]))
|
||||
cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE])))
|
||||
|
||||
@@ -7,6 +7,7 @@ from typing import Any
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import socket
|
||||
from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
@@ -481,6 +482,14 @@ async def to_code(config):
|
||||
cg.add(var.set_name(name))
|
||||
await cg.register_component(var, config)
|
||||
|
||||
# BLE uses 1 UDP socket for event notification to wake up main loop from select()
|
||||
# This enables low-latency (~12μs) BLE event processing instead of waiting for
|
||||
# select() timeout (0-16ms). The socket is created in ble_setup_() and used to
|
||||
# wake lwip_select() when BLE events arrive from the BLE thread.
|
||||
# Note: Called during config generation, socket is created at runtime. In practice,
|
||||
# always used since esp32_ble only runs on ESP32 which always has USE_SOCKET_SELECT_SUPPORT.
|
||||
socket.consume_sockets(1, "esp32_ble")(config)
|
||||
|
||||
# Define max connections for use in C++ code (e.g., ble_server.h)
|
||||
max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS)
|
||||
cg.add_define("USE_ESP32_BLE_MAX_CONNECTIONS", max_connections)
|
||||
|
||||
@@ -27,10 +27,34 @@ extern "C" {
|
||||
#include <esp32-hal-bt.h>
|
||||
#endif
|
||||
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
#include <lwip/sockets.h>
|
||||
#endif
|
||||
|
||||
namespace esphome::esp32_ble {
|
||||
|
||||
static const char *const TAG = "esp32_ble";
|
||||
|
||||
// GAP event groups for deduplication across gap_event_handler and dispatch_gap_event_
|
||||
#define GAP_SCAN_COMPLETE_EVENTS \
|
||||
case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: \
|
||||
case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: \
|
||||
case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT
|
||||
|
||||
#define GAP_ADV_COMPLETE_EVENTS \
|
||||
case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT: \
|
||||
case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT: \
|
||||
case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: \
|
||||
case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: \
|
||||
case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT
|
||||
|
||||
#define GAP_SECURITY_EVENTS \
|
||||
case ESP_GAP_BLE_AUTH_CMPL_EVT: \
|
||||
case ESP_GAP_BLE_SEC_REQ_EVT: \
|
||||
case ESP_GAP_BLE_PASSKEY_NOTIF_EVT: \
|
||||
case ESP_GAP_BLE_PASSKEY_REQ_EVT: \
|
||||
case ESP_GAP_BLE_NC_REQ_EVT
|
||||
|
||||
void ESP32BLE::setup() {
|
||||
global_ble = this;
|
||||
if (!ble_pre_setup_()) {
|
||||
@@ -273,10 +297,21 @@ bool ESP32BLE::ble_setup_() {
|
||||
// BLE takes some time to be fully set up, 200ms should be more than enough
|
||||
delay(200); // NOLINT
|
||||
|
||||
// Set up notification socket to wake main loop for BLE events
|
||||
// This enables low-latency (~12μs) event processing instead of waiting for select() timeout
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
this->setup_event_notification_();
|
||||
#endif
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ESP32BLE::ble_dismantle_() {
|
||||
// Clean up notification socket first before dismantling BLE stack
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
this->cleanup_event_notification_();
|
||||
#endif
|
||||
|
||||
esp_err_t err = esp_bluedroid_disable();
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_bluedroid_disable failed: %d", err);
|
||||
@@ -374,6 +409,12 @@ void ESP32BLE::loop() {
|
||||
break;
|
||||
}
|
||||
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
// Drain any notification socket events first
|
||||
// This clears the socket so it doesn't stay "ready" in subsequent select() calls
|
||||
this->drain_event_notifications_();
|
||||
#endif
|
||||
|
||||
BLEEvent *ble_event = this->ble_events_.pop();
|
||||
while (ble_event != nullptr) {
|
||||
switch (ble_event->type_) {
|
||||
@@ -414,60 +455,48 @@ void ESP32BLE::loop() {
|
||||
break;
|
||||
|
||||
// Scan complete events
|
||||
case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT:
|
||||
case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT:
|
||||
case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT:
|
||||
// All three scan complete events have the same structure with just status
|
||||
// The scan_complete struct matches ESP-IDF's layout exactly, so this reinterpret_cast is safe
|
||||
// This is verified at compile-time by static_assert checks in ble_event.h
|
||||
// The struct already contains our copy of the status (copied in BLEEvent constructor)
|
||||
ESP_LOGV(TAG, "gap_event_handler - %d", gap_event);
|
||||
#ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT
|
||||
for (auto *gap_handler : this->gap_event_handlers_) {
|
||||
gap_handler->gap_event_handler(
|
||||
gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.scan_complete));
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
|
||||
GAP_SCAN_COMPLETE_EVENTS:
|
||||
// Advertising complete events
|
||||
case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT:
|
||||
case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT:
|
||||
case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT:
|
||||
case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
|
||||
case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:
|
||||
// All advertising complete events have the same structure with just status
|
||||
ESP_LOGV(TAG, "gap_event_handler - %d", gap_event);
|
||||
#ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT
|
||||
for (auto *gap_handler : this->gap_event_handlers_) {
|
||||
gap_handler->gap_event_handler(
|
||||
gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.adv_complete));
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
|
||||
GAP_ADV_COMPLETE_EVENTS:
|
||||
// RSSI complete event
|
||||
case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT:
|
||||
ESP_LOGV(TAG, "gap_event_handler - %d", gap_event);
|
||||
#ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT
|
||||
for (auto *gap_handler : this->gap_event_handlers_) {
|
||||
gap_handler->gap_event_handler(
|
||||
gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.read_rssi_complete));
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
|
||||
// Security events
|
||||
case ESP_GAP_BLE_AUTH_CMPL_EVT:
|
||||
case ESP_GAP_BLE_SEC_REQ_EVT:
|
||||
case ESP_GAP_BLE_PASSKEY_NOTIF_EVT:
|
||||
case ESP_GAP_BLE_PASSKEY_REQ_EVT:
|
||||
case ESP_GAP_BLE_NC_REQ_EVT:
|
||||
GAP_SECURITY_EVENTS:
|
||||
ESP_LOGV(TAG, "gap_event_handler - %d", gap_event);
|
||||
#ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT
|
||||
for (auto *gap_handler : this->gap_event_handlers_) {
|
||||
gap_handler->gap_event_handler(
|
||||
gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.security));
|
||||
{
|
||||
esp_ble_gap_cb_param_t *param;
|
||||
// clang-format off
|
||||
switch (gap_event) {
|
||||
// All three scan complete events have the same structure with just status
|
||||
// The scan_complete struct matches ESP-IDF's layout exactly, so this reinterpret_cast is safe
|
||||
// This is verified at compile-time by static_assert checks in ble_event.h
|
||||
// The struct already contains our copy of the status (copied in BLEEvent constructor)
|
||||
GAP_SCAN_COMPLETE_EVENTS:
|
||||
param = reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.scan_complete);
|
||||
break;
|
||||
|
||||
// All advertising complete events have the same structure with just status
|
||||
GAP_ADV_COMPLETE_EVENTS:
|
||||
param = reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.adv_complete);
|
||||
break;
|
||||
|
||||
case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT:
|
||||
param = reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.read_rssi_complete);
|
||||
break;
|
||||
|
||||
GAP_SECURITY_EVENTS:
|
||||
param = reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.security);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// clang-format on
|
||||
// Dispatch to all registered handlers
|
||||
for (auto *gap_handler : this->gap_event_handlers_) {
|
||||
gap_handler->gap_event_handler(gap_event, param);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
@@ -547,26 +576,24 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa
|
||||
// Queue GAP events that components need to handle
|
||||
// Scanning events - used by esp32_ble_tracker
|
||||
case ESP_GAP_BLE_SCAN_RESULT_EVT:
|
||||
case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT:
|
||||
case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT:
|
||||
case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT:
|
||||
GAP_SCAN_COMPLETE_EVENTS:
|
||||
// Advertising events - used by esp32_ble_beacon and esp32_ble server
|
||||
case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT:
|
||||
case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT:
|
||||
case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT:
|
||||
case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
|
||||
case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:
|
||||
GAP_ADV_COMPLETE_EVENTS:
|
||||
// Connection events - used by ble_client
|
||||
case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT:
|
||||
// Security events - used by ble_client and bluetooth_proxy
|
||||
case ESP_GAP_BLE_AUTH_CMPL_EVT:
|
||||
case ESP_GAP_BLE_SEC_REQ_EVT:
|
||||
case ESP_GAP_BLE_PASSKEY_NOTIF_EVT:
|
||||
case ESP_GAP_BLE_PASSKEY_REQ_EVT:
|
||||
case ESP_GAP_BLE_NC_REQ_EVT:
|
||||
enqueue_ble_event(event, param);
|
||||
return;
|
||||
|
||||
// Security events - used by ble_client and bluetooth_proxy
|
||||
// These are rare but interactive (pairing/bonding), so notify immediately
|
||||
GAP_SECURITY_EVENTS:
|
||||
enqueue_ble_event(event, param);
|
||||
// Wake up main loop to process security event immediately
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
global_ble->notify_main_loop_();
|
||||
#endif
|
||||
return;
|
||||
|
||||
// Ignore these GAP events as they are not relevant for our use case
|
||||
case ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT:
|
||||
case ESP_GAP_BLE_SET_PKT_LENGTH_COMPLETE_EVT:
|
||||
@@ -584,6 +611,10 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa
|
||||
void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
|
||||
esp_ble_gatts_cb_param_t *param) {
|
||||
enqueue_ble_event(event, gatts_if, param);
|
||||
// Wake up main loop to process GATT event immediately
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
global_ble->notify_main_loop_();
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -591,6 +622,10 @@ void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gat
|
||||
void ESP32BLE::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) {
|
||||
enqueue_ble_event(event, gattc_if, param);
|
||||
// Wake up main loop to process GATT event immediately
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
global_ble->notify_main_loop_();
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -630,6 +665,89 @@ void ESP32BLE::dump_config() {
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
void ESP32BLE::setup_event_notification_() {
|
||||
// Create UDP socket for event notifications
|
||||
this->notify_fd_ = lwip_socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
|
||||
if (this->notify_fd_ < 0) {
|
||||
ESP_LOGW(TAG, "Event socket create failed: %d", errno);
|
||||
return;
|
||||
}
|
||||
|
||||
// Bind to loopback with auto-assigned port
|
||||
struct sockaddr_in addr = {};
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_addr.s_addr = lwip_htonl(INADDR_LOOPBACK);
|
||||
addr.sin_port = 0; // Auto-assign port
|
||||
|
||||
if (lwip_bind(this->notify_fd_, (struct sockaddr *) &addr, sizeof(addr)) < 0) {
|
||||
ESP_LOGW(TAG, "Event socket bind failed: %d", errno);
|
||||
lwip_close(this->notify_fd_);
|
||||
this->notify_fd_ = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the assigned address and connect to it
|
||||
// Connecting a UDP socket allows using send() instead of sendto() for better performance
|
||||
struct sockaddr_in notify_addr;
|
||||
socklen_t len = sizeof(notify_addr);
|
||||
if (lwip_getsockname(this->notify_fd_, (struct sockaddr *) ¬ify_addr, &len) < 0) {
|
||||
ESP_LOGW(TAG, "Event socket address failed: %d", errno);
|
||||
lwip_close(this->notify_fd_);
|
||||
this->notify_fd_ = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
// Connect to self (loopback) - allows using send() instead of sendto()
|
||||
// After connect(), no need to store notify_addr - the socket remembers it
|
||||
if (lwip_connect(this->notify_fd_, (struct sockaddr *) ¬ify_addr, sizeof(notify_addr)) < 0) {
|
||||
ESP_LOGW(TAG, "Event socket connect failed: %d", errno);
|
||||
lwip_close(this->notify_fd_);
|
||||
this->notify_fd_ = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
// Set non-blocking mode
|
||||
int flags = lwip_fcntl(this->notify_fd_, F_GETFL, 0);
|
||||
lwip_fcntl(this->notify_fd_, F_SETFL, flags | O_NONBLOCK);
|
||||
|
||||
// Register with application's select() loop
|
||||
if (!App.register_socket_fd(this->notify_fd_)) {
|
||||
ESP_LOGW(TAG, "Event socket register failed");
|
||||
lwip_close(this->notify_fd_);
|
||||
this->notify_fd_ = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Event socket ready");
|
||||
}
|
||||
|
||||
void ESP32BLE::cleanup_event_notification_() {
|
||||
if (this->notify_fd_ >= 0) {
|
||||
App.unregister_socket_fd(this->notify_fd_);
|
||||
lwip_close(this->notify_fd_);
|
||||
this->notify_fd_ = -1;
|
||||
ESP_LOGD(TAG, "Event socket closed");
|
||||
}
|
||||
}
|
||||
|
||||
void ESP32BLE::drain_event_notifications_() {
|
||||
// Called from main loop to drain any pending notifications
|
||||
// Must check is_socket_ready() to avoid blocking on empty socket
|
||||
if (this->notify_fd_ >= 0 && App.is_socket_ready(this->notify_fd_)) {
|
||||
char buffer[BLE_EVENT_NOTIFY_DRAIN_BUFFER_SIZE];
|
||||
// Drain all pending notifications with non-blocking reads
|
||||
// Multiple BLE events may have triggered multiple writes, so drain until EWOULDBLOCK
|
||||
// We control both ends of this loopback socket (always write 1 byte per event),
|
||||
// so no error checking needed - any errors indicate catastrophic system failure
|
||||
while (lwip_recvfrom(this->notify_fd_, buffer, sizeof(buffer), 0, nullptr, nullptr) > 0) {
|
||||
// Just draining, no action needed - actual BLE events are already queued
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif // USE_SOCKET_SELECT_SUPPORT
|
||||
|
||||
uint64_t ble_addr_to_uint64(const esp_bd_addr_t address) {
|
||||
uint64_t u = 0;
|
||||
u |= uint64_t(address[0] & 0xFF) << 40;
|
||||
|
||||
@@ -25,6 +25,10 @@
|
||||
#include <esp_gattc_api.h>
|
||||
#include <esp_gatts_api.h>
|
||||
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
#include <lwip/sockets.h>
|
||||
#endif
|
||||
|
||||
namespace esphome::esp32_ble {
|
||||
|
||||
// Maximum size of the BLE event queue
|
||||
@@ -162,6 +166,13 @@ class ESP32BLE : public Component {
|
||||
void advertising_init_();
|
||||
#endif
|
||||
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
void setup_event_notification_(); // Create notification socket
|
||||
void cleanup_event_notification_(); // Close and unregister socket
|
||||
inline void notify_main_loop_(); // Wake up select() from BLE thread (hot path - inlined)
|
||||
void drain_event_notifications_(); // Read pending notifications in main loop
|
||||
#endif
|
||||
|
||||
private:
|
||||
template<typename... Args> friend void enqueue_ble_event(Args... args);
|
||||
|
||||
@@ -196,6 +207,13 @@ class ESP32BLE : public Component {
|
||||
esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; // 4 bytes (enum)
|
||||
uint32_t advertising_cycle_time_{}; // 4 bytes
|
||||
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
// Event notification socket for waking up main loop from BLE thread
|
||||
// Uses connected UDP loopback socket to wake lwip_select() with ~12μs latency vs 0-16ms timeout
|
||||
// Socket is connected during setup, allowing use of send() instead of sendto() for efficiency
|
||||
int notify_fd_{-1}; // 4 bytes (file descriptor)
|
||||
#endif
|
||||
|
||||
// 2-byte aligned members
|
||||
uint16_t appearance_{0}; // 2 bytes
|
||||
|
||||
@@ -207,6 +225,29 @@ class ESP32BLE : public Component {
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
extern ESP32BLE *global_ble;
|
||||
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
// Inline implementations for hot-path functions
|
||||
// These are called from BLE thread (notify) and main loop (drain) on every event
|
||||
|
||||
// Small buffer for draining notification bytes (1 byte sent per BLE event)
|
||||
// Size allows draining multiple notifications per recvfrom() without wasting stack
|
||||
static constexpr size_t BLE_EVENT_NOTIFY_DRAIN_BUFFER_SIZE = 16;
|
||||
|
||||
inline void ESP32BLE::notify_main_loop_() {
|
||||
// Called from BLE thread context when events are queued
|
||||
// Wakes up lwip_select() in main loop by writing to connected loopback socket
|
||||
if (this->notify_fd_ >= 0) {
|
||||
const char dummy = 1;
|
||||
// Non-blocking send - if it fails (unlikely), select() will wake on timeout anyway
|
||||
// No error checking needed: we control both ends of this loopback socket, and the
|
||||
// BLE event is already queued. Notification is best-effort to reduce latency.
|
||||
// This is safe to call from BLE thread - send() is thread-safe in lwip
|
||||
// Socket is already connected to loopback address, so send() is faster than sendto()
|
||||
lwip_send(this->notify_fd_, &dummy, 1, 0);
|
||||
}
|
||||
}
|
||||
#endif // USE_SOCKET_SELECT_SUPPORT
|
||||
|
||||
template<typename... Ts> class BLEEnabledCondition : public Condition<Ts...> {
|
||||
public:
|
||||
bool check(Ts... x) override { return global_ble->is_active(); }
|
||||
|
||||
@@ -461,7 +461,9 @@ async def parse_value(value_config, args):
|
||||
if isinstance(value, str):
|
||||
value = list(value.encode(value_config[CONF_STRING_ENCODING]))
|
||||
if isinstance(value, list):
|
||||
return cg.std_vector.template(cg.uint8)(value)
|
||||
# Generate initializer list {1, 2, 3} instead of std::vector<uint8_t>({1, 2, 3})
|
||||
# This calls the set_value(std::initializer_list<uint8_t>) overload
|
||||
return cg.ArrayInitializer(*value)
|
||||
val = cg.RawExpression(f"{value_config[CONF_TYPE]}({cg.safe_exp(value)})")
|
||||
return ByteBuffer_ns.wrap(val, value_config[CONF_ENDIANNESS])
|
||||
|
||||
|
||||
@@ -35,13 +35,18 @@ BLECharacteristic::BLECharacteristic(const ESPBTUUID uuid, uint32_t properties)
|
||||
|
||||
void BLECharacteristic::set_value(ByteBuffer buffer) { this->set_value(buffer.get_data()); }
|
||||
|
||||
void BLECharacteristic::set_value(const std::vector<uint8_t> &buffer) {
|
||||
void BLECharacteristic::set_value(std::vector<uint8_t> &&buffer) {
|
||||
xSemaphoreTake(this->set_value_lock_, 0L);
|
||||
this->value_ = buffer;
|
||||
this->value_ = std::move(buffer);
|
||||
xSemaphoreGive(this->set_value_lock_);
|
||||
}
|
||||
|
||||
void BLECharacteristic::set_value(std::initializer_list<uint8_t> data) {
|
||||
this->set_value(std::vector<uint8_t>(data)); // Delegate to move overload
|
||||
}
|
||||
|
||||
void BLECharacteristic::set_value(const std::string &buffer) {
|
||||
this->set_value(std::vector<uint8_t>(buffer.begin(), buffer.end()));
|
||||
this->set_value(std::vector<uint8_t>(buffer.begin(), buffer.end())); // Delegate to move overload
|
||||
}
|
||||
|
||||
void BLECharacteristic::notify() {
|
||||
|
||||
@@ -33,7 +33,8 @@ class BLECharacteristic {
|
||||
~BLECharacteristic();
|
||||
|
||||
void set_value(ByteBuffer buffer);
|
||||
void set_value(const std::vector<uint8_t> &buffer);
|
||||
void set_value(std::vector<uint8_t> &&buffer);
|
||||
void set_value(std::initializer_list<uint8_t> data);
|
||||
void set_value(const std::string &buffer);
|
||||
|
||||
void set_broadcast_property(bool value);
|
||||
|
||||
@@ -46,15 +46,17 @@ void BLEDescriptor::do_create(BLECharacteristic *characteristic) {
|
||||
this->state_ = CREATING;
|
||||
}
|
||||
|
||||
void BLEDescriptor::set_value(std::vector<uint8_t> buffer) {
|
||||
size_t length = buffer.size();
|
||||
void BLEDescriptor::set_value(std::vector<uint8_t> &&buffer) { this->set_value_impl_(buffer.data(), buffer.size()); }
|
||||
|
||||
void BLEDescriptor::set_value(std::initializer_list<uint8_t> data) { this->set_value_impl_(data.begin(), data.size()); }
|
||||
|
||||
void BLEDescriptor::set_value_impl_(const uint8_t *data, size_t length) {
|
||||
if (length > this->value_.attr_max_len) {
|
||||
ESP_LOGE(TAG, "Size %d too large, must be no bigger than %d", length, this->value_.attr_max_len);
|
||||
return;
|
||||
}
|
||||
this->value_.attr_len = length;
|
||||
memcpy(this->value_.attr_value, buffer.data(), length);
|
||||
memcpy(this->value_.attr_value, data, length);
|
||||
}
|
||||
|
||||
void BLEDescriptor::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
|
||||
|
||||
@@ -27,7 +27,8 @@ class BLEDescriptor {
|
||||
void do_create(BLECharacteristic *characteristic);
|
||||
ESPBTUUID get_uuid() const { return this->uuid_; }
|
||||
|
||||
void set_value(std::vector<uint8_t> buffer);
|
||||
void set_value(std::vector<uint8_t> &&buffer);
|
||||
void set_value(std::initializer_list<uint8_t> data);
|
||||
void set_value(ByteBuffer buffer) { this->set_value(buffer.get_data()); }
|
||||
|
||||
void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param);
|
||||
@@ -42,6 +43,8 @@ class BLEDescriptor {
|
||||
}
|
||||
|
||||
protected:
|
||||
void set_value_impl_(const uint8_t *data, size_t length);
|
||||
|
||||
BLECharacteristic *characteristic_{nullptr};
|
||||
ESPBTUUID uuid_;
|
||||
uint16_t handle_{0xFFFF};
|
||||
|
||||
@@ -112,7 +112,7 @@ async def to_code(config):
|
||||
|
||||
cg.add_define("USE_IMPROV")
|
||||
|
||||
await improv_base.setup_improv_core(var, config)
|
||||
await improv_base.setup_improv_core(var, config, "esp32_improv")
|
||||
|
||||
cg.add(var.set_identify_duration(config[CONF_IDENTIFY_DURATION]))
|
||||
cg.add(var.set_authorized_duration(config[CONF_AUTHORIZED_DURATION]))
|
||||
|
||||
@@ -270,8 +270,8 @@ void ESP32ImprovComponent::set_error_(improv::Error error) {
|
||||
}
|
||||
}
|
||||
|
||||
void ESP32ImprovComponent::send_response_(std::vector<uint8_t> &response) {
|
||||
this->rpc_response_->set_value(ByteBuffer::wrap(response));
|
||||
void ESP32ImprovComponent::send_response_(std::vector<uint8_t> &&response) {
|
||||
this->rpc_response_->set_value(std::move(response));
|
||||
if (this->state_ != improv::STATE_STOPPED)
|
||||
this->rpc_response_->notify();
|
||||
}
|
||||
@@ -389,11 +389,13 @@ void ESP32ImprovComponent::check_wifi_connection_() {
|
||||
std::string url_strings[3];
|
||||
size_t url_count = 0;
|
||||
|
||||
#ifdef USE_ESP32_IMPROV_NEXT_URL
|
||||
// Add next_url if configured (should be first per Improv BLE spec)
|
||||
std::string next_url = this->get_formatted_next_url_();
|
||||
if (!next_url.empty()) {
|
||||
url_strings[url_count++] = std::move(next_url);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Add default URLs for backward compatibility
|
||||
url_strings[url_count++] = ESPHOME_MY_LINK;
|
||||
@@ -407,10 +409,8 @@ void ESP32ImprovComponent::check_wifi_connection_() {
|
||||
}
|
||||
}
|
||||
#endif
|
||||
// Pass to build_rpc_response using vector constructor from iterators to avoid extra copies
|
||||
std::vector<uint8_t> data = improv::build_rpc_response(
|
||||
improv::WIFI_SETTINGS, std::vector<std::string>(url_strings, url_strings + url_count));
|
||||
this->send_response_(data);
|
||||
this->send_response_(improv::build_rpc_response(improv::WIFI_SETTINGS,
|
||||
std::vector<std::string>(url_strings, url_strings + url_count)));
|
||||
} else if (this->is_active() && this->state_ != improv::STATE_PROVISIONED) {
|
||||
ESP_LOGD(TAG, "WiFi provisioned externally");
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ class ESP32ImprovComponent : public Component, public improv_base::ImprovBase {
|
||||
void set_state_(improv::State state, bool update_advertising = true);
|
||||
void set_error_(improv::Error error);
|
||||
improv::State get_initial_state_() const;
|
||||
void send_response_(std::vector<uint8_t> &response);
|
||||
void send_response_(std::vector<uint8_t> &&response);
|
||||
void process_incoming_data_();
|
||||
void on_wifi_connect_timeout_();
|
||||
void check_wifi_connection_();
|
||||
|
||||
@@ -190,7 +190,9 @@ async def to_code(config):
|
||||
cg.add_define("ESPHOME_VARIANT", "ESP8266")
|
||||
cg.add_define(ThreadModel.SINGLE)
|
||||
|
||||
cg.add_platformio_option("extra_scripts", ["pre:iram_fix.py", "post:post_build.py"])
|
||||
cg.add_platformio_option(
|
||||
"extra_scripts", ["pre:testing_mode.py", "post:post_build.py"]
|
||||
)
|
||||
|
||||
conf = config[CONF_FRAMEWORK]
|
||||
cg.add_platformio_option("framework", "arduino")
|
||||
@@ -230,9 +232,9 @@ async def to_code(config):
|
||||
# For cases where nullptrs can be handled, use nothrow: `new (std::nothrow) T;`
|
||||
cg.add_build_flag("-DNEW_OOM_ABORT")
|
||||
|
||||
# In testing mode, fake a larger IRAM to allow linking grouped component tests
|
||||
# Real ESP8266 hardware only has 32KB IRAM, but for CI testing we pretend it has 2MB
|
||||
# This is done via a pre-build script that generates a custom linker script
|
||||
# In testing mode, fake larger memory to allow linking grouped component tests
|
||||
# Real ESP8266 hardware only has 32KB IRAM and ~80KB RAM, but for CI testing
|
||||
# we pretend it has much larger memory to test that components compile together
|
||||
if CORE.testing_mode:
|
||||
cg.add_build_flag("-DESPHOME_TESTING_MODE")
|
||||
|
||||
@@ -271,8 +273,8 @@ def copy_files():
|
||||
post_build_file,
|
||||
CORE.relative_build_path("post_build.py"),
|
||||
)
|
||||
iram_fix_file = dir / "iram_fix.py.script"
|
||||
testing_mode_file = dir / "testing_mode.py.script"
|
||||
copy_file_if_changed(
|
||||
iram_fix_file,
|
||||
CORE.relative_build_path("iram_fix.py"),
|
||||
testing_mode_file,
|
||||
CORE.relative_build_path("testing_mode.py"),
|
||||
)
|
||||
|
||||
@@ -29,8 +29,8 @@ class ESP8266GPIOPin : public InternalGPIOPin {
|
||||
void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override;
|
||||
|
||||
uint8_t pin_;
|
||||
bool inverted_;
|
||||
gpio::Flags flags_;
|
||||
bool inverted_{};
|
||||
gpio::Flags flags_{};
|
||||
};
|
||||
|
||||
} // namespace esp8266
|
||||
|
||||
@@ -165,7 +165,10 @@ async def esp8266_pin_to_code(config):
|
||||
num = config[CONF_NUMBER]
|
||||
mode = config[CONF_MODE]
|
||||
cg.add(var.set_pin(num))
|
||||
cg.add(var.set_inverted(config[CONF_INVERTED]))
|
||||
# Only set if true to avoid bloating setup() function
|
||||
# (inverted bit in pin_flags_ bitfield is zero-initialized to false)
|
||||
if config[CONF_INVERTED]:
|
||||
cg.add(var.set_inverted(True))
|
||||
cg.add(var.set_flags(pins.gpio_flags_expr(mode)))
|
||||
if num < 16:
|
||||
initial_state: PinInitialState = CORE.data[KEY_ESP8266][KEY_PIN_INITIAL_STATES][
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
# pylint: disable=E0602
|
||||
Import("env") # noqa
|
||||
|
||||
|
||||
def patch_linker_script_after_preprocess(source, target, env):
|
||||
"""Patch the local linker script after PlatformIO preprocesses it."""
|
||||
# Check if we're in testing mode by looking for the define
|
||||
build_flags = env.get("BUILD_FLAGS", [])
|
||||
testing_mode = any("-DESPHOME_TESTING_MODE" in flag for flag in build_flags)
|
||||
|
||||
if not testing_mode:
|
||||
return
|
||||
|
||||
# Get the local linker script path
|
||||
build_dir = env.subst("$BUILD_DIR")
|
||||
local_ld = os.path.join(build_dir, "ld", "local.eagle.app.v6.common.ld")
|
||||
|
||||
if not os.path.exists(local_ld):
|
||||
return
|
||||
|
||||
# Read the linker script
|
||||
with open(local_ld, "r") as f:
|
||||
content = f.read()
|
||||
|
||||
# Replace IRAM size from 0x8000 (32KB) to 0x200000 (2MB)
|
||||
# The line looks like: iram1_0_seg : org = 0x40100000, len = 0x8000
|
||||
updated = re.sub(
|
||||
r"(iram1_0_seg\s*:\s*org\s*=\s*0x40100000\s*,\s*len\s*=\s*)0x8000",
|
||||
r"\g<1>0x200000",
|
||||
content,
|
||||
)
|
||||
|
||||
if updated != content:
|
||||
with open(local_ld, "w") as f:
|
||||
f.write(updated)
|
||||
print("ESPHome: Patched IRAM size to 2MB for testing mode")
|
||||
|
||||
|
||||
# Hook into the build process right before linking
|
||||
# This runs after PlatformIO has already preprocessed the linker scripts
|
||||
env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", patch_linker_script_after_preprocess)
|
||||
166
esphome/components/esp8266/testing_mode.py.script
Normal file
166
esphome/components/esp8266/testing_mode.py.script
Normal file
@@ -0,0 +1,166 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
# pylint: disable=E0602
|
||||
Import("env") # noqa
|
||||
|
||||
|
||||
# Memory sizes for testing mode (allow larger builds for CI component grouping)
|
||||
TESTING_IRAM_SIZE = "0x200000" # 2MB
|
||||
TESTING_DRAM_SIZE = "0x200000" # 2MB
|
||||
TESTING_FLASH_SIZE = "0x2000000" # 32MB
|
||||
|
||||
|
||||
def patch_segment_size(content, segment_name, new_size, label):
|
||||
"""Patch a memory segment's length in linker script.
|
||||
|
||||
Args:
|
||||
content: Linker script content
|
||||
segment_name: Name of the segment (e.g., 'iram1_0_seg')
|
||||
new_size: New size as hex string (e.g., '0x200000')
|
||||
label: Human-readable label for logging (e.g., 'IRAM')
|
||||
|
||||
Returns:
|
||||
Tuple of (patched_content, was_patched)
|
||||
"""
|
||||
# Match: segment_name : org = 0x..., len = 0x...
|
||||
pattern = rf"({segment_name}\s*:\s*org\s*=\s*0x[0-9a-fA-F]+\s*,\s*len\s*=\s*)0x[0-9a-fA-F]+"
|
||||
new_content = re.sub(pattern, rf"\g<1>{new_size}", content)
|
||||
return new_content, new_content != content
|
||||
|
||||
|
||||
def apply_memory_patches(content):
|
||||
"""Apply IRAM, DRAM, and Flash patches to linker script content.
|
||||
|
||||
Args:
|
||||
content: Linker script content as string
|
||||
|
||||
Returns:
|
||||
Patched content as string
|
||||
"""
|
||||
patches_applied = []
|
||||
|
||||
# Patch IRAM (for larger code in IRAM)
|
||||
content, patched = patch_segment_size(content, "iram1_0_seg", TESTING_IRAM_SIZE, "IRAM")
|
||||
if patched:
|
||||
patches_applied.append("IRAM")
|
||||
|
||||
# Patch DRAM (for larger BSS/data sections)
|
||||
content, patched = patch_segment_size(content, "dram0_0_seg", TESTING_DRAM_SIZE, "DRAM")
|
||||
if patched:
|
||||
patches_applied.append("DRAM")
|
||||
|
||||
# Patch Flash (for larger code sections)
|
||||
content, patched = patch_segment_size(content, "irom0_0_seg", TESTING_FLASH_SIZE, "Flash")
|
||||
if patched:
|
||||
patches_applied.append("Flash")
|
||||
|
||||
if patches_applied:
|
||||
iram_mb = int(TESTING_IRAM_SIZE, 16) // (1024 * 1024)
|
||||
dram_mb = int(TESTING_DRAM_SIZE, 16) // (1024 * 1024)
|
||||
flash_mb = int(TESTING_FLASH_SIZE, 16) // (1024 * 1024)
|
||||
print(f" Patched memory segments: {', '.join(patches_applied)} (IRAM/DRAM: {iram_mb}MB, Flash: {flash_mb}MB)")
|
||||
|
||||
return content
|
||||
|
||||
|
||||
def patch_linker_script_file(filepath, description):
|
||||
"""Patch a linker script file in the build directory with enlarged memory segments.
|
||||
|
||||
This function modifies linker scripts in the build directory only (never SDK files).
|
||||
It patches IRAM, DRAM, and Flash segments to allow larger builds in testing mode.
|
||||
|
||||
Args:
|
||||
filepath: Path to the linker script file in the build directory
|
||||
description: Human-readable description for logging
|
||||
|
||||
Returns:
|
||||
True if the file was patched, False if already patched or not found
|
||||
"""
|
||||
if not os.path.exists(filepath):
|
||||
print(f"ESPHome: {description} not found at {filepath}")
|
||||
return False
|
||||
|
||||
print(f"ESPHome: Patching {description}...")
|
||||
with open(filepath, "r") as f:
|
||||
content = f.read()
|
||||
|
||||
patched_content = apply_memory_patches(content)
|
||||
|
||||
if patched_content != content:
|
||||
with open(filepath, "w") as f:
|
||||
f.write(patched_content)
|
||||
print(f"ESPHome: Successfully patched {description}")
|
||||
return True
|
||||
else:
|
||||
print(f"ESPHome: {description} already patched or no changes needed")
|
||||
return False
|
||||
|
||||
|
||||
def patch_local_linker_script(source, target, env):
|
||||
"""Patch the local.eagle.app.v6.common.ld in build directory.
|
||||
|
||||
This patches the preprocessed linker script that PlatformIO creates in the build
|
||||
directory, enlarging IRAM, DRAM, and Flash segments for testing mode.
|
||||
|
||||
Args:
|
||||
source: SCons source nodes
|
||||
target: SCons target nodes
|
||||
env: SCons environment
|
||||
"""
|
||||
# Check if we're in testing mode
|
||||
build_flags = env.get("BUILD_FLAGS", [])
|
||||
testing_mode = any("-DESPHOME_TESTING_MODE" in flag for flag in build_flags)
|
||||
|
||||
if not testing_mode:
|
||||
return
|
||||
|
||||
# Patch the local linker script if it exists
|
||||
build_dir = env.subst("$BUILD_DIR")
|
||||
ld_dir = os.path.join(build_dir, "ld")
|
||||
if os.path.exists(ld_dir):
|
||||
local_ld = os.path.join(ld_dir, "local.eagle.app.v6.common.ld")
|
||||
if os.path.exists(local_ld):
|
||||
patch_linker_script_file(local_ld, "local.eagle.app.v6.common.ld")
|
||||
|
||||
|
||||
# Check if we're in testing mode
|
||||
build_flags = env.get("BUILD_FLAGS", [])
|
||||
testing_mode = any("-DESPHOME_TESTING_MODE" in flag for flag in build_flags)
|
||||
|
||||
if testing_mode:
|
||||
# Create a custom linker script in the build directory with patched memory limits
|
||||
# This allows larger IRAM/DRAM/Flash for CI component grouping tests
|
||||
build_dir = env.subst("$BUILD_DIR")
|
||||
ldscript = env.GetProjectOption("board_build.ldscript", "")
|
||||
assert ldscript, "No linker script configured in board_build.ldscript"
|
||||
|
||||
framework_dir = env.PioPlatform().get_package_dir("framework-arduinoespressif8266")
|
||||
assert framework_dir is not None, "Could not find framework-arduinoespressif8266 package"
|
||||
|
||||
# Read the original SDK linker script (read-only, SDK is never modified)
|
||||
sdk_ld = os.path.join(framework_dir, "tools", "sdk", "ld", ldscript)
|
||||
# Create a custom version in the build directory (isolated, temporary)
|
||||
custom_ld = os.path.join(build_dir, f"testing_{ldscript}")
|
||||
|
||||
if os.path.exists(sdk_ld) and not os.path.exists(custom_ld):
|
||||
# Read the SDK linker script
|
||||
with open(sdk_ld, "r") as f:
|
||||
content = f.read()
|
||||
|
||||
# Apply memory patches (IRAM: 2MB, DRAM: 2MB, Flash: 32MB)
|
||||
patched_content = apply_memory_patches(content)
|
||||
|
||||
# Write the patched linker script to the build directory
|
||||
with open(custom_ld, "w") as f:
|
||||
f.write(patched_content)
|
||||
|
||||
print(f"ESPHome: Created custom linker script: {custom_ld}")
|
||||
|
||||
# Tell the linker to use our custom script from the build directory
|
||||
assert os.path.exists(custom_ld), f"Custom linker script not found: {custom_ld}"
|
||||
env.Replace(LDSCRIPT_PATH=custom_ld)
|
||||
print(f"ESPHome: Using custom linker script with patched memory limits")
|
||||
|
||||
# Also patch local.eagle.app.v6.common.ld after PlatformIO creates it
|
||||
env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", patch_local_linker_script)
|
||||
@@ -281,19 +281,15 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
#endif
|
||||
|
||||
// Acknowledge auth OK - 1 byte
|
||||
buf[0] = ota::OTA_RESPONSE_AUTH_OK;
|
||||
this->writeall_(buf, 1);
|
||||
this->write_byte_(ota::OTA_RESPONSE_AUTH_OK);
|
||||
|
||||
// Read size, 4 bytes MSB first
|
||||
if (!this->readall_(buf, 4)) {
|
||||
this->log_read_error_(LOG_STR("size"));
|
||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
}
|
||||
ota_size = 0;
|
||||
for (uint8_t i = 0; i < 4; i++) {
|
||||
ota_size <<= 8;
|
||||
ota_size |= buf[i];
|
||||
}
|
||||
ota_size = (static_cast<size_t>(buf[0]) << 24) | (static_cast<size_t>(buf[1]) << 16) |
|
||||
(static_cast<size_t>(buf[2]) << 8) | buf[3];
|
||||
ESP_LOGV(TAG, "Size is %u bytes", ota_size);
|
||||
|
||||
// Now that we've passed authentication and are actually
|
||||
@@ -313,8 +309,7 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
update_started = true;
|
||||
|
||||
// Acknowledge prepare OK - 1 byte
|
||||
buf[0] = ota::OTA_RESPONSE_UPDATE_PREPARE_OK;
|
||||
this->writeall_(buf, 1);
|
||||
this->write_byte_(ota::OTA_RESPONSE_UPDATE_PREPARE_OK);
|
||||
|
||||
// Read binary MD5, 32 bytes
|
||||
if (!this->readall_(buf, 32)) {
|
||||
@@ -326,8 +321,7 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
this->backend_->set_update_md5(sbuf);
|
||||
|
||||
// Acknowledge MD5 OK - 1 byte
|
||||
buf[0] = ota::OTA_RESPONSE_BIN_MD5_OK;
|
||||
this->writeall_(buf, 1);
|
||||
this->write_byte_(ota::OTA_RESPONSE_BIN_MD5_OK);
|
||||
|
||||
while (total < ota_size) {
|
||||
// TODO: timeout check
|
||||
@@ -354,8 +348,7 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
total += read;
|
||||
#if USE_OTA_VERSION == 2
|
||||
while (size_acknowledged + OTA_BLOCK_SIZE <= total || (total == ota_size && size_acknowledged < ota_size)) {
|
||||
buf[0] = ota::OTA_RESPONSE_CHUNK_OK;
|
||||
this->writeall_(buf, 1);
|
||||
this->write_byte_(ota::OTA_RESPONSE_CHUNK_OK);
|
||||
size_acknowledged += OTA_BLOCK_SIZE;
|
||||
}
|
||||
#endif
|
||||
@@ -374,8 +367,7 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
}
|
||||
|
||||
// Acknowledge receive OK - 1 byte
|
||||
buf[0] = ota::OTA_RESPONSE_RECEIVE_OK;
|
||||
this->writeall_(buf, 1);
|
||||
this->write_byte_(ota::OTA_RESPONSE_RECEIVE_OK);
|
||||
|
||||
error_code = this->backend_->end();
|
||||
if (error_code != ota::OTA_RESPONSE_OK) {
|
||||
@@ -384,8 +376,7 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
}
|
||||
|
||||
// Acknowledge Update end OK - 1 byte
|
||||
buf[0] = ota::OTA_RESPONSE_UPDATE_END_OK;
|
||||
this->writeall_(buf, 1);
|
||||
this->write_byte_(ota::OTA_RESPONSE_UPDATE_END_OK);
|
||||
|
||||
// Read ACK
|
||||
if (!this->readall_(buf, 1) || buf[0] != ota::OTA_RESPONSE_OK) {
|
||||
@@ -404,8 +395,7 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
App.safe_reboot();
|
||||
|
||||
error:
|
||||
buf[0] = static_cast<uint8_t>(error_code);
|
||||
this->writeall_(buf, 1);
|
||||
this->write_byte_(static_cast<uint8_t>(error_code));
|
||||
this->cleanup_connection_();
|
||||
|
||||
if (this->backend_ != nullptr && update_started) {
|
||||
|
||||
@@ -53,6 +53,7 @@ class ESPHomeOTAComponent : public ota::OTAComponent {
|
||||
#endif // USE_OTA_PASSWORD
|
||||
bool readall_(uint8_t *buf, size_t len);
|
||||
bool writeall_(const uint8_t *buf, size_t len);
|
||||
inline bool write_byte_(uint8_t byte) { return this->writeall_(&byte, 1); }
|
||||
|
||||
bool try_read_(size_t to_read, const LogString *desc);
|
||||
bool try_write_(size_t to_write, const LogString *desc);
|
||||
|
||||
@@ -14,13 +14,13 @@ template<typename... Ts> class SendAction : public Action<Ts...>, public Parente
|
||||
TEMPLATABLE_VALUE(std::vector<uint8_t>, data);
|
||||
|
||||
public:
|
||||
void add_on_sent(const std::vector<Action<Ts...> *> &actions) {
|
||||
void add_on_sent(const std::initializer_list<Action<Ts...> *> &actions) {
|
||||
this->sent_.add_actions(actions);
|
||||
if (this->flags_.wait_for_sent) {
|
||||
this->sent_.add_action(new LambdaAction<Ts...>([this](Ts... x) { this->play_next_(x...); }));
|
||||
}
|
||||
}
|
||||
void add_on_error(const std::vector<Action<Ts...> *> &actions) {
|
||||
void add_on_error(const std::initializer_list<Action<Ts...> *> &actions) {
|
||||
this->error_.add_actions(actions);
|
||||
if (this->flags_.wait_for_sent) {
|
||||
this->error_.add_action(new LambdaAction<Ts...>([this](Ts... x) {
|
||||
|
||||
@@ -14,7 +14,7 @@ from esphome.components.esp32.const import (
|
||||
VARIANT_ESP32S2,
|
||||
VARIANT_ESP32S3,
|
||||
)
|
||||
from esphome.components.network import IPAddress
|
||||
from esphome.components.network import ip_address_literal
|
||||
from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
@@ -32,6 +32,7 @@ from esphome.const import (
|
||||
CONF_MISO_PIN,
|
||||
CONF_MODE,
|
||||
CONF_MOSI_PIN,
|
||||
CONF_NUMBER,
|
||||
CONF_PAGE_ID,
|
||||
CONF_PIN,
|
||||
CONF_POLLING_INTERVAL,
|
||||
@@ -52,12 +53,36 @@ from esphome.core import (
|
||||
coroutine_with_priority,
|
||||
)
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CONFLICTS_WITH = ["wifi"]
|
||||
DEPENDENCIES = ["esp32"]
|
||||
AUTO_LOAD = ["network"]
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# RMII pins that are hardcoded on ESP32 classic and cannot be changed
|
||||
# These pins are used by the internal Ethernet MAC when using RMII PHYs
|
||||
ESP32_RMII_FIXED_PINS = {
|
||||
19: "EMAC_TXD0",
|
||||
21: "EMAC_TX_EN",
|
||||
22: "EMAC_TXD1",
|
||||
25: "EMAC_RXD0",
|
||||
26: "EMAC_RXD1",
|
||||
27: "EMAC_RX_CRS_DV",
|
||||
}
|
||||
|
||||
# RMII default pins for ESP32-P4
|
||||
# These are the default pins used by ESP-IDF and are configurable in principle,
|
||||
# but ESPHome's ethernet component currently has no way to change them
|
||||
ESP32P4_RMII_DEFAULT_PINS = {
|
||||
34: "EMAC_TXD0",
|
||||
35: "EMAC_TXD1",
|
||||
28: "EMAC_RX_CRS_DV",
|
||||
29: "EMAC_RXD0",
|
||||
30: "EMAC_RXD1",
|
||||
49: "EMAC_TX_EN",
|
||||
}
|
||||
|
||||
ethernet_ns = cg.esphome_ns.namespace("ethernet")
|
||||
PHYRegister = ethernet_ns.struct("PHYRegister")
|
||||
CONF_PHY_ADDR = "phy_addr"
|
||||
@@ -273,7 +298,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
def _final_validate(config):
|
||||
def _final_validate_spi(config):
|
||||
if config[CONF_TYPE] not in SPI_ETHERNET_TYPES:
|
||||
return
|
||||
if spi_configs := fv.full_config.get().get(CONF_SPI):
|
||||
@@ -292,17 +317,14 @@ def _final_validate(config):
|
||||
)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
def manual_ip(config):
|
||||
return cg.StructInitializer(
|
||||
ManualIP,
|
||||
("static_ip", IPAddress(str(config[CONF_STATIC_IP]))),
|
||||
("gateway", IPAddress(str(config[CONF_GATEWAY]))),
|
||||
("subnet", IPAddress(str(config[CONF_SUBNET]))),
|
||||
("dns1", IPAddress(str(config[CONF_DNS1]))),
|
||||
("dns2", IPAddress(str(config[CONF_DNS2]))),
|
||||
("static_ip", ip_address_literal(config[CONF_STATIC_IP])),
|
||||
("gateway", ip_address_literal(config[CONF_GATEWAY])),
|
||||
("subnet", ip_address_literal(config[CONF_SUBNET])),
|
||||
("dns1", ip_address_literal(config[CONF_DNS1])),
|
||||
("dns2", ip_address_literal(config[CONF_DNS2])),
|
||||
)
|
||||
|
||||
|
||||
@@ -383,3 +405,57 @@ async def to_code(config):
|
||||
|
||||
if CORE.using_arduino:
|
||||
cg.add_library("WiFi", None)
|
||||
|
||||
|
||||
def _final_validate_rmii_pins(config: ConfigType) -> None:
|
||||
"""Validate that RMII pins are not used by other components."""
|
||||
# Only validate for RMII-based PHYs on ESP32/ESP32P4
|
||||
if config[CONF_TYPE] in SPI_ETHERNET_TYPES or config[CONF_TYPE] == "OPENETH":
|
||||
return # SPI and OPENETH don't use RMII
|
||||
|
||||
variant = get_esp32_variant()
|
||||
if variant == VARIANT_ESP32:
|
||||
rmii_pins = ESP32_RMII_FIXED_PINS
|
||||
is_configurable = False
|
||||
elif variant == VARIANT_ESP32P4:
|
||||
rmii_pins = ESP32P4_RMII_DEFAULT_PINS
|
||||
is_configurable = True
|
||||
else:
|
||||
return # No RMII validation needed for other variants
|
||||
|
||||
# Check all used pins against RMII reserved pins
|
||||
for pin_list in pins.PIN_SCHEMA_REGISTRY.pins_used.values():
|
||||
for pin_path, _, pin_config in pin_list:
|
||||
pin_num = pin_config.get(CONF_NUMBER)
|
||||
if pin_num not in rmii_pins:
|
||||
continue
|
||||
# Found a conflict - show helpful error message
|
||||
pin_function = rmii_pins[pin_num]
|
||||
component_path = ".".join(str(p) for p in pin_path)
|
||||
if is_configurable:
|
||||
error_msg = (
|
||||
f"GPIO{pin_num} is used by Ethernet RMII "
|
||||
f"({pin_function}) with the current default "
|
||||
f"configuration. This conflicts with '{component_path}'. "
|
||||
f"Please choose a different GPIO pin for "
|
||||
f"'{component_path}'."
|
||||
)
|
||||
else:
|
||||
error_msg = (
|
||||
f"GPIO{pin_num} is reserved for Ethernet RMII "
|
||||
f"({pin_function}) and cannot be used. This pin is "
|
||||
f"hardcoded by ESP-IDF and cannot be changed when using "
|
||||
f"RMII Ethernet PHYs. Please choose a different GPIO pin "
|
||||
f"for '{component_path}'."
|
||||
)
|
||||
raise cv.Invalid(error_msg, path=pin_path)
|
||||
|
||||
|
||||
def _final_validate(config: ConfigType) -> ConfigType:
|
||||
"""Final validation for Ethernet component."""
|
||||
_final_validate_spi(config)
|
||||
_final_validate_rmii_pins(config)
|
||||
return config
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
@@ -85,11 +85,6 @@ def event_schema(
|
||||
return _EVENT_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
# Remove before 2025.11.0
|
||||
EVENT_SCHEMA = event_schema()
|
||||
EVENT_SCHEMA.add_extra(cv.deprecated_schema_constant("event"))
|
||||
|
||||
|
||||
async def setup_event_core_(var, config, *, event_types: list[str]):
|
||||
await setup_entity(var, config, "event")
|
||||
|
||||
|
||||
@@ -8,12 +8,19 @@ namespace event {
|
||||
static const char *const TAG = "event";
|
||||
|
||||
void Event::trigger(const std::string &event_type) {
|
||||
auto found = types_.find(event_type);
|
||||
if (found == types_.end()) {
|
||||
// Linear search - faster than std::set for small datasets (1-5 items typical)
|
||||
const std::string *found = nullptr;
|
||||
for (const auto &type : this->types_) {
|
||||
if (type == event_type) {
|
||||
found = &type;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found == nullptr) {
|
||||
ESP_LOGE(TAG, "'%s': invalid event type for trigger(): %s", this->get_name().c_str(), event_type.c_str());
|
||||
return;
|
||||
}
|
||||
last_event_type = &(*found);
|
||||
last_event_type = found;
|
||||
ESP_LOGD(TAG, "'%s' Triggered event '%s'", this->get_name().c_str(), last_event_type->c_str());
|
||||
this->event_callback_.call(event_type);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <set>
|
||||
#include <string>
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
@@ -26,13 +25,13 @@ class Event : public EntityBase, public EntityBase_DeviceClass {
|
||||
const std::string *last_event_type;
|
||||
|
||||
void trigger(const std::string &event_type);
|
||||
void set_event_types(const std::set<std::string> &event_types) { this->types_ = event_types; }
|
||||
std::set<std::string> get_event_types() const { return this->types_; }
|
||||
void set_event_types(const std::initializer_list<std::string> &event_types) { this->types_ = event_types; }
|
||||
const FixedVector<std::string> &get_event_types() const { return this->types_; }
|
||||
void add_on_event_callback(std::function<void(const std::string &event_type)> &&callback);
|
||||
|
||||
protected:
|
||||
CallbackManager<void(const std::string &event_type)> event_callback_;
|
||||
std::set<std::string> types_;
|
||||
FixedVector<std::string> types_;
|
||||
};
|
||||
|
||||
} // namespace event
|
||||
|
||||
@@ -189,10 +189,6 @@ def fan_schema(
|
||||
return _FAN_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
# Remove before 2025.11.0
|
||||
FAN_SCHEMA = fan_schema(Fan)
|
||||
FAN_SCHEMA.add_extra(cv.deprecated_schema_constant("fan"))
|
||||
|
||||
_PRESET_MODES_SCHEMA = cv.All(
|
||||
cv.ensure_list(cv.string_strict),
|
||||
cv.Length(min=1),
|
||||
|
||||
@@ -51,7 +51,14 @@ void FanCall::validate_() {
|
||||
|
||||
if (!this->preset_mode_.empty()) {
|
||||
const auto &preset_modes = traits.supported_preset_modes();
|
||||
if (preset_modes.find(this->preset_mode_) == preset_modes.end()) {
|
||||
bool found = false;
|
||||
for (const auto &mode : preset_modes) {
|
||||
if (strcmp(mode, this->preset_mode_.c_str()) == 0) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
ESP_LOGW(TAG, "%s: Preset mode '%s' not supported", this->parent_.get_name().c_str(), this->preset_mode_.c_str());
|
||||
this->preset_mode_.clear();
|
||||
}
|
||||
@@ -92,11 +99,12 @@ FanCall FanRestoreState::to_call(Fan &fan) {
|
||||
call.set_speed(this->speed);
|
||||
call.set_direction(this->direction);
|
||||
|
||||
if (fan.get_traits().supports_preset_modes()) {
|
||||
auto traits = fan.get_traits();
|
||||
if (traits.supports_preset_modes()) {
|
||||
// Use stored preset index to get preset name
|
||||
const auto &preset_modes = fan.get_traits().supported_preset_modes();
|
||||
const auto &preset_modes = traits.supported_preset_modes();
|
||||
if (this->preset_mode < preset_modes.size()) {
|
||||
call.set_preset_mode(*std::next(preset_modes.begin(), this->preset_mode));
|
||||
call.set_preset_mode(preset_modes[this->preset_mode]);
|
||||
}
|
||||
}
|
||||
return call;
|
||||
@@ -107,11 +115,12 @@ void FanRestoreState::apply(Fan &fan) {
|
||||
fan.speed = this->speed;
|
||||
fan.direction = this->direction;
|
||||
|
||||
if (fan.get_traits().supports_preset_modes()) {
|
||||
auto traits = fan.get_traits();
|
||||
if (traits.supports_preset_modes()) {
|
||||
// Use stored preset index to get preset name
|
||||
const auto &preset_modes = fan.get_traits().supported_preset_modes();
|
||||
const auto &preset_modes = traits.supported_preset_modes();
|
||||
if (this->preset_mode < preset_modes.size()) {
|
||||
fan.preset_mode = *std::next(preset_modes.begin(), this->preset_mode);
|
||||
fan.preset_mode = preset_modes[this->preset_mode];
|
||||
}
|
||||
}
|
||||
fan.publish_state();
|
||||
@@ -182,18 +191,25 @@ void Fan::save_state_() {
|
||||
return;
|
||||
}
|
||||
|
||||
auto traits = this->get_traits();
|
||||
|
||||
FanRestoreState state{};
|
||||
state.state = this->state;
|
||||
state.oscillating = this->oscillating;
|
||||
state.speed = this->speed;
|
||||
state.direction = this->direction;
|
||||
|
||||
if (this->get_traits().supports_preset_modes() && !this->preset_mode.empty()) {
|
||||
const auto &preset_modes = this->get_traits().supported_preset_modes();
|
||||
if (traits.supports_preset_modes() && !this->preset_mode.empty()) {
|
||||
const auto &preset_modes = traits.supported_preset_modes();
|
||||
// Store index of current preset mode
|
||||
auto preset_iterator = preset_modes.find(this->preset_mode);
|
||||
if (preset_iterator != preset_modes.end())
|
||||
state.preset_mode = std::distance(preset_modes.begin(), preset_iterator);
|
||||
size_t i = 0;
|
||||
for (const auto &mode : preset_modes) {
|
||||
if (strcmp(mode, this->preset_mode.c_str()) == 0) {
|
||||
state.preset_mode = i;
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
this->rtc_.save(&state);
|
||||
@@ -216,8 +232,8 @@ void Fan::dump_traits_(const char *tag, const char *prefix) {
|
||||
}
|
||||
if (traits.supports_preset_modes()) {
|
||||
ESP_LOGCONFIG(tag, "%s Supported presets:", prefix);
|
||||
for (const std::string &s : traits.supported_preset_modes())
|
||||
ESP_LOGCONFIG(tag, "%s - %s", prefix, s.c_str());
|
||||
for (const char *s : traits.supported_preset_modes())
|
||||
ESP_LOGCONFIG(tag, "%s - %s", prefix, s);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,8 +60,6 @@ class FanCall {
|
||||
this->speed_ = speed;
|
||||
return *this;
|
||||
}
|
||||
ESPDEPRECATED("set_speed() with string argument is deprecated, use integer argument instead.", "2021.9")
|
||||
FanCall &set_speed(const char *legacy_speed);
|
||||
optional<int> get_speed() const { return this->speed_; }
|
||||
FanCall &set_direction(FanDirection direction) {
|
||||
this->direction_ = direction;
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
#include <set>
|
||||
#include <utility>
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace esphome {
|
||||
#include <vector>
|
||||
#include <initializer_list>
|
||||
|
||||
#ifdef USE_API
|
||||
namespace api {
|
||||
class APIConnection;
|
||||
} // namespace api
|
||||
#endif
|
||||
namespace esphome {
|
||||
|
||||
namespace fan {
|
||||
|
||||
@@ -36,27 +30,27 @@ class FanTraits {
|
||||
/// Set whether this fan supports changing direction
|
||||
void set_direction(bool direction) { this->direction_ = direction; }
|
||||
/// Return the preset modes supported by the fan.
|
||||
std::set<std::string> supported_preset_modes() const { return this->preset_modes_; }
|
||||
/// Set the preset modes supported by the fan.
|
||||
void set_supported_preset_modes(const std::set<std::string> &preset_modes) { this->preset_modes_ = preset_modes; }
|
||||
const std::vector<const char *> &supported_preset_modes() const { return this->preset_modes_; }
|
||||
/// Set the preset modes supported by the fan (from initializer list).
|
||||
void set_supported_preset_modes(std::initializer_list<const char *> preset_modes) {
|
||||
this->preset_modes_ = preset_modes;
|
||||
}
|
||||
/// Set the preset modes supported by the fan (from vector).
|
||||
void set_supported_preset_modes(const std::vector<const char *> &preset_modes) { this->preset_modes_ = preset_modes; }
|
||||
|
||||
// Deleted overloads to catch incorrect std::string usage at compile time with clear error messages
|
||||
void set_supported_preset_modes(const std::vector<std::string> &preset_modes) = delete;
|
||||
void set_supported_preset_modes(std::initializer_list<std::string> preset_modes) = delete;
|
||||
|
||||
/// Return if preset modes are supported
|
||||
bool supports_preset_modes() const { return !this->preset_modes_.empty(); }
|
||||
|
||||
protected:
|
||||
#ifdef USE_API
|
||||
// The API connection is a friend class to access internal methods
|
||||
friend class api::APIConnection;
|
||||
// This method returns a reference to the internal preset modes set.
|
||||
// It is used by the API to avoid copying data when encoding messages.
|
||||
// Warning: Do not use this method outside of the API connection code.
|
||||
// It returns a reference to internal data that can be invalidated.
|
||||
const std::set<std::string> &supported_preset_modes_for_api_() const { return this->preset_modes_; }
|
||||
#endif
|
||||
bool oscillation_{false};
|
||||
bool speed_{false};
|
||||
bool direction_{false};
|
||||
int speed_count_{};
|
||||
std::set<std::string> preset_modes_{};
|
||||
std::vector<const char *> preset_modes_{};
|
||||
};
|
||||
|
||||
} // namespace fan
|
||||
|
||||
@@ -39,6 +39,7 @@ CONFIG_SCHEMA = (
|
||||
# due to hardware limitations or lack of reliable interrupt support. This ensures
|
||||
# stable operation on these platforms. Future maintainers should verify platform
|
||||
# capabilities before changing this default behavior.
|
||||
# nrf52 has no gpio interrupts implemented yet
|
||||
cv.SplitDefault(
|
||||
CONF_USE_INTERRUPT,
|
||||
bk72xx=False,
|
||||
@@ -46,7 +47,7 @@ CONFIG_SCHEMA = (
|
||||
esp8266=True,
|
||||
host=True,
|
||||
ln882x=False,
|
||||
nrf52=True,
|
||||
nrf52=False,
|
||||
rp2040=True,
|
||||
rtl87xx=False,
|
||||
): cv.boolean,
|
||||
@@ -94,6 +95,8 @@ async def to_code(config):
|
||||
)
|
||||
use_interrupt = False
|
||||
|
||||
cg.add(var.set_use_interrupt(use_interrupt))
|
||||
if 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))
|
||||
|
||||
@@ -67,7 +67,7 @@ void GPIOSwitch::write_state(bool state) {
|
||||
this->pin_->digital_write(state);
|
||||
this->publish_state(state);
|
||||
}
|
||||
void GPIOSwitch::set_interlock(const std::vector<Switch *> &interlock) { this->interlock_ = interlock; }
|
||||
void GPIOSwitch::set_interlock(const std::initializer_list<Switch *> &interlock) { this->interlock_ = interlock; }
|
||||
|
||||
} // namespace gpio
|
||||
} // namespace esphome
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/switch/switch.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace esphome {
|
||||
namespace gpio {
|
||||
|
||||
@@ -19,14 +18,14 @@ class GPIOSwitch : public switch_::Switch, public Component {
|
||||
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void set_interlock(const std::vector<Switch *> &interlock);
|
||||
void set_interlock(const std::initializer_list<Switch *> &interlock);
|
||||
void set_interlock_wait_time(uint32_t interlock_wait_time) { interlock_wait_time_ = interlock_wait_time; }
|
||||
|
||||
protected:
|
||||
void write_state(bool state) override;
|
||||
|
||||
GPIOPin *pin_;
|
||||
std::vector<Switch *> interlock_;
|
||||
FixedVector<Switch *> interlock_;
|
||||
uint32_t interlock_wait_time_{0};
|
||||
};
|
||||
|
||||
|
||||
@@ -171,7 +171,7 @@ void HaierClimateBase::toggle_power() {
|
||||
PendingAction({ActionRequest::TOGGLE_POWER, esphome::optional<haier_protocol::HaierMessage>()});
|
||||
}
|
||||
|
||||
void HaierClimateBase::set_supported_swing_modes(const std::set<climate::ClimateSwingMode> &modes) {
|
||||
void HaierClimateBase::set_supported_swing_modes(climate::ClimateSwingModeMask modes) {
|
||||
this->traits_.set_supported_swing_modes(modes);
|
||||
if (!modes.empty())
|
||||
this->traits_.add_supported_swing_mode(climate::CLIMATE_SWING_OFF);
|
||||
@@ -179,13 +179,13 @@ void HaierClimateBase::set_supported_swing_modes(const std::set<climate::Climate
|
||||
|
||||
void HaierClimateBase::set_answer_timeout(uint32_t timeout) { this->haier_protocol_.set_answer_timeout(timeout); }
|
||||
|
||||
void HaierClimateBase::set_supported_modes(const std::set<climate::ClimateMode> &modes) {
|
||||
void HaierClimateBase::set_supported_modes(climate::ClimateModeMask modes) {
|
||||
this->traits_.set_supported_modes(modes);
|
||||
this->traits_.add_supported_mode(climate::CLIMATE_MODE_OFF); // Always available
|
||||
this->traits_.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL); // Always available
|
||||
}
|
||||
|
||||
void HaierClimateBase::set_supported_presets(const std::set<climate::ClimatePreset> &presets) {
|
||||
void HaierClimateBase::set_supported_presets(climate::ClimatePresetMask presets) {
|
||||
this->traits_.set_supported_presets(presets);
|
||||
if (!presets.empty())
|
||||
this->traits_.add_supported_preset(climate::CLIMATE_PRESET_NONE);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <set>
|
||||
#include "esphome/components/climate/climate.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#include "esphome/core/automation.h"
|
||||
@@ -60,9 +59,9 @@ class HaierClimateBase : public esphome::Component,
|
||||
void send_power_off_command();
|
||||
void toggle_power();
|
||||
void reset_protocol() { this->reset_protocol_request_ = true; };
|
||||
void set_supported_modes(const std::set<esphome::climate::ClimateMode> &modes);
|
||||
void set_supported_swing_modes(const std::set<esphome::climate::ClimateSwingMode> &modes);
|
||||
void set_supported_presets(const std::set<esphome::climate::ClimatePreset> &presets);
|
||||
void set_supported_modes(esphome::climate::ClimateModeMask modes);
|
||||
void set_supported_swing_modes(esphome::climate::ClimateSwingModeMask modes);
|
||||
void set_supported_presets(esphome::climate::ClimatePresetMask presets);
|
||||
bool valid_connection() const { return this->protocol_phase_ >= ProtocolPhases::IDLE; };
|
||||
size_t available() noexcept override { return esphome::uart::UARTDevice::available(); };
|
||||
size_t read_array(uint8_t *data, size_t len) noexcept override {
|
||||
|
||||
@@ -1033,9 +1033,9 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
|
||||
{
|
||||
// Swing mode
|
||||
ClimateSwingMode old_swing_mode = this->swing_mode;
|
||||
const std::set<ClimateSwingMode> &swing_modes = traits_.get_supported_swing_modes();
|
||||
bool vertical_swing_supported = swing_modes.find(CLIMATE_SWING_VERTICAL) != swing_modes.end();
|
||||
bool horizontal_swing_supported = swing_modes.find(CLIMATE_SWING_HORIZONTAL) != swing_modes.end();
|
||||
const auto &swing_modes = traits_.get_supported_swing_modes();
|
||||
bool vertical_swing_supported = swing_modes.count(CLIMATE_SWING_VERTICAL);
|
||||
bool horizontal_swing_supported = swing_modes.count(CLIMATE_SWING_HORIZONTAL);
|
||||
if (horizontal_swing_supported &&
|
||||
(packet.control.horizontal_swing_mode == (uint8_t) hon_protocol::HorizontalSwingMode::AUTO)) {
|
||||
if (vertical_swing_supported &&
|
||||
@@ -1218,13 +1218,13 @@ void HonClimate::fill_control_messages_queue_() {
|
||||
(uint8_t) hon_protocol::DataParameters::QUIET_MODE,
|
||||
quiet_mode_buf, 2);
|
||||
}
|
||||
if ((fast_mode_buf[1] != 0xFF) && ((presets.find(climate::ClimatePreset::CLIMATE_PRESET_BOOST) != presets.end()))) {
|
||||
if ((fast_mode_buf[1] != 0xFF) && presets.count(climate::ClimatePreset::CLIMATE_PRESET_BOOST)) {
|
||||
this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL,
|
||||
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
|
||||
(uint8_t) hon_protocol::DataParameters::FAST_MODE,
|
||||
fast_mode_buf, 2);
|
||||
}
|
||||
if ((away_mode_buf[1] != 0xFF) && ((presets.find(climate::ClimatePreset::CLIMATE_PRESET_AWAY) != presets.end()))) {
|
||||
if ((away_mode_buf[1] != 0xFF) && presets.count(climate::ClimatePreset::CLIMATE_PRESET_AWAY)) {
|
||||
this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL,
|
||||
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
|
||||
(uint8_t) hon_protocol::DataParameters::TEN_DEGREE,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <set>
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/components/output/binary_output.h"
|
||||
#include "esphome/components/output/float_output.h"
|
||||
@@ -22,7 +20,7 @@ class HBridgeFan : public Component, public fan::Fan {
|
||||
void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; }
|
||||
void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; }
|
||||
void set_enable_pin(output::FloatOutput *enable) { enable_ = enable; }
|
||||
void set_preset_modes(const std::set<std::string> &presets) { preset_modes_ = presets; }
|
||||
void set_preset_modes(std::initializer_list<const char *> presets) { preset_modes_ = presets; }
|
||||
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
@@ -38,7 +36,7 @@ class HBridgeFan : public Component, public fan::Fan {
|
||||
int speed_count_{};
|
||||
DecayMode decay_mode_{DECAY_MODE_SLOW};
|
||||
fan::FanTraits traits_;
|
||||
std::set<std::string> preset_modes_{};
|
||||
std::vector<const char *> preset_modes_{};
|
||||
|
||||
void control(const fan::FanCall &call) override;
|
||||
void write_state_();
|
||||
|
||||
1
esphome/components/hdc2010/__init__.py
Normal file
1
esphome/components/hdc2010/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
CODEOWNERS = ["@optimusprimespace", "@ssieb"]
|
||||
111
esphome/components/hdc2010/hdc2010.cpp
Normal file
111
esphome/components/hdc2010/hdc2010.cpp
Normal file
@@ -0,0 +1,111 @@
|
||||
#include "esphome/core/hal.h"
|
||||
#include "hdc2010.h"
|
||||
// https://github.com/vigsterkr/homebridge-hdc2010/blob/main/src/hdc2010.js
|
||||
// https://github.com/lime-labs/HDC2080-Arduino/blob/master/src/HDC2080.cpp
|
||||
namespace esphome {
|
||||
namespace hdc2010 {
|
||||
|
||||
static const char *const TAG = "hdc2010";
|
||||
|
||||
static const uint8_t HDC2010_ADDRESS = 0x40; // 0b1000000 or 0b1000001 from datasheet
|
||||
static const uint8_t HDC2010_CMD_CONFIGURATION_MEASUREMENT = 0x8F;
|
||||
static const uint8_t HDC2010_CMD_START_MEASUREMENT = 0xF9;
|
||||
static const uint8_t HDC2010_CMD_TEMPERATURE_LOW = 0x00;
|
||||
static const uint8_t HDC2010_CMD_TEMPERATURE_HIGH = 0x01;
|
||||
static const uint8_t HDC2010_CMD_HUMIDITY_LOW = 0x02;
|
||||
static const uint8_t HDC2010_CMD_HUMIDITY_HIGH = 0x03;
|
||||
static const uint8_t CONFIG = 0x0E;
|
||||
static const uint8_t MEASUREMENT_CONFIG = 0x0F;
|
||||
|
||||
void HDC2010Component::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup");
|
||||
|
||||
const uint8_t data[2] = {
|
||||
0b00000000, // resolution 14bit for both humidity and temperature
|
||||
0b00000000 // reserved
|
||||
};
|
||||
|
||||
if (!this->write_bytes(HDC2010_CMD_CONFIGURATION_MEASUREMENT, data, 2)) {
|
||||
ESP_LOGW(TAG, "Initial config instruction error");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
|
||||
// Set measurement mode to temperature and humidity
|
||||
uint8_t config_contents;
|
||||
this->read_register(MEASUREMENT_CONFIG, &config_contents, 1);
|
||||
config_contents = (config_contents & 0xF9); // Always set to TEMP_AND_HUMID mode
|
||||
this->write_bytes(MEASUREMENT_CONFIG, &config_contents, 1);
|
||||
|
||||
// Set rate to manual
|
||||
this->read_register(CONFIG, &config_contents, 1);
|
||||
config_contents &= 0x8F;
|
||||
this->write_bytes(CONFIG, &config_contents, 1);
|
||||
|
||||
// Set temperature resolution to 14bit
|
||||
this->read_register(CONFIG, &config_contents, 1);
|
||||
config_contents &= 0x3F;
|
||||
this->write_bytes(CONFIG, &config_contents, 1);
|
||||
|
||||
// Set humidity resolution to 14bit
|
||||
this->read_register(CONFIG, &config_contents, 1);
|
||||
config_contents &= 0xCF;
|
||||
this->write_bytes(CONFIG, &config_contents, 1);
|
||||
}
|
||||
|
||||
void HDC2010Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "HDC2010:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
|
||||
}
|
||||
|
||||
void HDC2010Component::update() {
|
||||
// Trigger measurement
|
||||
uint8_t config_contents;
|
||||
this->read_register(CONFIG, &config_contents, 1);
|
||||
config_contents |= 0x01;
|
||||
this->write_bytes(MEASUREMENT_CONFIG, &config_contents, 1);
|
||||
|
||||
// 1ms delay after triggering the sample
|
||||
set_timeout(1, [this]() {
|
||||
if (this->temperature_sensor_ != nullptr) {
|
||||
float temp = this->read_temp();
|
||||
this->temperature_sensor_->publish_state(temp);
|
||||
ESP_LOGD(TAG, "Temp=%.1f°C", temp);
|
||||
}
|
||||
|
||||
if (this->humidity_sensor_ != nullptr) {
|
||||
float humidity = this->read_humidity();
|
||||
this->humidity_sensor_->publish_state(humidity);
|
||||
ESP_LOGD(TAG, "Humidity=%.1f%%", humidity);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
float HDC2010Component::read_temp() {
|
||||
uint8_t byte[2];
|
||||
|
||||
this->read_register(HDC2010_CMD_TEMPERATURE_LOW, &byte[0], 1);
|
||||
this->read_register(HDC2010_CMD_TEMPERATURE_HIGH, &byte[1], 1);
|
||||
|
||||
uint16_t temp = encode_uint16(byte[1], byte[0]);
|
||||
return (float) temp * 0.0025177f - 40.0f;
|
||||
}
|
||||
|
||||
float HDC2010Component::read_humidity() {
|
||||
uint8_t byte[2];
|
||||
|
||||
this->read_register(HDC2010_CMD_HUMIDITY_LOW, &byte[0], 1);
|
||||
this->read_register(HDC2010_CMD_HUMIDITY_HIGH, &byte[1], 1);
|
||||
|
||||
uint16_t humidity = encode_uint16(byte[1], byte[0]);
|
||||
return (float) humidity * 0.001525879f;
|
||||
}
|
||||
|
||||
} // namespace hdc2010
|
||||
} // namespace esphome
|
||||
32
esphome/components/hdc2010/hdc2010.h
Normal file
32
esphome/components/hdc2010/hdc2010.h
Normal file
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace hdc2010 {
|
||||
|
||||
class HDC2010Component : public PollingComponent, public i2c::I2CDevice {
|
||||
public:
|
||||
void set_temperature_sensor(sensor::Sensor *temperature) { this->temperature_sensor_ = temperature; }
|
||||
|
||||
void set_humidity_sensor(sensor::Sensor *humidity) { this->humidity_sensor_ = humidity; }
|
||||
|
||||
/// Setup the sensor and check for connection.
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
/// Retrieve the latest sensor values. This operation takes approximately 16ms.
|
||||
void update() override;
|
||||
|
||||
float read_temp();
|
||||
|
||||
float read_humidity();
|
||||
|
||||
protected:
|
||||
sensor::Sensor *temperature_sensor_{nullptr};
|
||||
sensor::Sensor *humidity_sensor_{nullptr};
|
||||
};
|
||||
|
||||
} // namespace hdc2010
|
||||
} // namespace esphome
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user