mirror of
https://github.com/esphome/esphome.git
synced 2026-06-26 06:09:16 +00:00
Compare commits
434 Commits
fix-rotary
...
split-hal-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36250682b0 | ||
|
|
4f75647f63 | ||
|
|
eec770d622 | ||
|
|
d7b21a84a3 | ||
|
|
f05243bd9d | ||
|
|
35cb28edfe | ||
|
|
1363f661e6 | ||
|
|
8af499b591 | ||
|
|
1a57d9bc2f | ||
|
|
9768380856 | ||
|
|
676f26919e | ||
|
|
29d3a3a498 | ||
|
|
77b76ac48a | ||
|
|
0b5835284a | ||
|
|
15df477472 | ||
|
|
be0ee73847 | ||
|
|
a241c9e622 | ||
|
|
2f433c78bd | ||
|
|
e39c474577 | ||
|
|
a62e3fe4fc | ||
|
|
7d6b9bee19 | ||
|
|
ab6bda50e4 | ||
|
|
3d195d748c | ||
|
|
16cf4fb5e8 | ||
|
|
70503442f4 | ||
|
|
594b269dba | ||
|
|
8157c721a5 | ||
|
|
9af557de6d | ||
|
|
1f4136e76f | ||
|
|
c8dffcc9b8 | ||
|
|
44fbb7f5a9 | ||
|
|
eb01d43feb | ||
|
|
7891fd5cf1 | ||
|
|
4ee9cc432b | ||
|
|
42ff10afe5 | ||
|
|
6b3df66bdc | ||
|
|
968878a62d | ||
|
|
daf3f4d2f1 | ||
|
|
52e8c50f45 | ||
|
|
0a4d9b430f | ||
|
|
0759a3c681 | ||
|
|
8921e3bb3f | ||
|
|
52f80618d4 | ||
|
|
876c8c4c2a | ||
|
|
41458d72e0 | ||
|
|
49d3df2698 | ||
|
|
792f2e8363 | ||
|
|
42c9fdc87e | ||
|
|
5f6bbb98ce | ||
|
|
4e0509435a | ||
|
|
a03de7cea2 | ||
|
|
95b5ab7e78 | ||
|
|
3ac0939f55 | ||
|
|
191d3bc7e4 | ||
|
|
a186f6fea9 | ||
|
|
aea88aef5e | ||
|
|
433bbdb016 | ||
|
|
4137d93cbf | ||
|
|
6a5919ee87 | ||
|
|
b753ee4e94 | ||
|
|
c26ea52620 | ||
|
|
39a69385fb | ||
|
|
a34836c290 | ||
|
|
01ac223913 | ||
|
|
7198c912c7 | ||
|
|
24c6a0d711 | ||
|
|
dec5d0449b | ||
|
|
79b741b8dc | ||
|
|
112646a9c4 | ||
|
|
2e096bb036 | ||
|
|
e87e78c544 | ||
|
|
0f25d91e68 | ||
|
|
8dbdcfc128 | ||
|
|
8950afc3c4 | ||
|
|
04d067196d | ||
|
|
502c010465 | ||
|
|
180105bb4b | ||
|
|
4c0dfb0e0d | ||
|
|
df987a7ffb | ||
|
|
c8d4420408 | ||
|
|
b084fa4490 | ||
|
|
68625a1b76 | ||
|
|
dc57969afd | ||
|
|
f092e619d8 | ||
|
|
58f6ad2d0c | ||
|
|
bc33260c61 | ||
|
|
4cab262ef8 | ||
|
|
9ad820c921 | ||
|
|
4f8feb86f0 | ||
|
|
b5ccd55f4e | ||
|
|
a437b3086b | ||
|
|
c27f9e512b | ||
|
|
f62972c2c6 | ||
|
|
f36efbc762 | ||
|
|
9f5121e271 | ||
|
|
a1e3ec7118 | ||
|
|
9caf9ee023 | ||
|
|
d3bae21d13 | ||
|
|
9d138e73c9 | ||
|
|
e23a6bf59f | ||
|
|
94e300389c | ||
|
|
55bcf33446 | ||
|
|
f132b7dc07 | ||
|
|
baa6d5f96b | ||
|
|
773b4d887b | ||
|
|
ac7f0f0b74 | ||
|
|
bc7f35b569 | ||
|
|
ae02ab3865 | ||
|
|
eceb534895 | ||
|
|
404620b99c | ||
|
|
3ccaa771a7 | ||
|
|
b4a86e46b2 | ||
|
|
ddf1426f86 | ||
|
|
90d7bfe02e | ||
|
|
d759f1a567 | ||
|
|
f757cd1210 | ||
|
|
9b45b046a8 | ||
|
|
70ae614abd | ||
|
|
8f9b91eece | ||
|
|
3ca86fc3fc | ||
|
|
b38db617a2 | ||
|
|
13fe881f70 | ||
|
|
50c181671c | ||
|
|
43a371caab | ||
|
|
64290d32a1 | ||
|
|
9685d4eb0b | ||
|
|
4c2efd4165 | ||
|
|
6f00ea1457 | ||
|
|
a881121110 | ||
|
|
f8167c9a70 | ||
|
|
e1d629f0d2 | ||
|
|
224cc7b419 | ||
|
|
4d4347d33a | ||
|
|
6ca5b31fab | ||
|
|
17f9269841 | ||
|
|
6253947311 | ||
|
|
00b71208a6 | ||
|
|
76eb8f697f | ||
|
|
2a3bd8bc85 | ||
|
|
629da4d878 | ||
|
|
5c2ceb63e0 | ||
|
|
92cb6dd7fd | ||
|
|
06e5931ad7 | ||
|
|
dc5b06285d | ||
|
|
3d0a2421a6 | ||
|
|
22f6791dea | ||
|
|
70b1d9a087 | ||
|
|
36720c8495 | ||
|
|
c48ab2ef92 | ||
|
|
162ee2ecaf | ||
|
|
a73bac0b5f | ||
|
|
4e84611ae7 | ||
|
|
ea2e36e55a | ||
|
|
fcbc4d64fe | ||
|
|
dcd103cec0 | ||
|
|
5e715692d6 | ||
|
|
d5263cd46e | ||
|
|
c399cd2fa2 | ||
|
|
f6bf6dc8e5 | ||
|
|
e35b435f02 | ||
|
|
886cd7ab72 | ||
|
|
73714dc489 | ||
|
|
5218bbd791 | ||
|
|
23ad30cb4c | ||
|
|
a3b49d1ed9 | ||
|
|
9c80cbf19c | ||
|
|
699cf9690a | ||
|
|
67576d4879 | ||
|
|
edcf96d057 | ||
|
|
bb81c91d0c | ||
|
|
78f1467be4 | ||
|
|
da44d43981 | ||
|
|
9cebce1b6e | ||
|
|
b20fedd806 | ||
|
|
ee91ad8f06 | ||
|
|
7560112144 | ||
|
|
43c6b839cd | ||
|
|
0c9d443a5c | ||
|
|
14defb69b6 | ||
|
|
3a6f3dfb94 | ||
|
|
7bd36e0c8d | ||
|
|
e4f413adad | ||
|
|
1504ac3d19 | ||
|
|
947c714f89 | ||
|
|
e4d5886383 | ||
|
|
f504099485 | ||
|
|
cb56f9a9bf | ||
|
|
26a656af29 | ||
|
|
a8bd035b62 | ||
|
|
f05fa45747 | ||
|
|
78875abee4 | ||
|
|
37608c2656 | ||
|
|
a5b1f3eece | ||
|
|
0d3a3552da | ||
|
|
0a0176d600 | ||
|
|
4cb7ea2584 | ||
|
|
a43ee15b56 | ||
|
|
213ab312d2 | ||
|
|
94f30d5950 | ||
|
|
6af341bb5b | ||
|
|
82656cb0cf | ||
|
|
b72f5447c3 | ||
|
|
73b8e8ac09 | ||
|
|
9459f0426d | ||
|
|
0dae41aa22 | ||
|
|
7321e6e52f | ||
|
|
f0c21520aa | ||
|
|
b0c133201f | ||
|
|
572fb83015 | ||
|
|
0d3db2b670 | ||
|
|
e5f6a734ba | ||
|
|
bab9cd3e7a | ||
|
|
36812591eb | ||
|
|
1862c6115f | ||
|
|
ef780886c3 | ||
|
|
602305b20d | ||
|
|
78701debec | ||
|
|
08ac61ae94 | ||
|
|
6d5340f253 | ||
|
|
e2dfef5ddc | ||
|
|
1d88027618 | ||
|
|
9841deec31 | ||
|
|
ed5852c2d6 | ||
|
|
b26601a3dc | ||
|
|
f5806818cd | ||
|
|
c3e739eba9 | ||
|
|
b167b64f06 | ||
|
|
722cfae04c | ||
|
|
9cb2b562b9 | ||
|
|
81fb6712fe | ||
|
|
227dfa3730 | ||
|
|
aa80bdbbc6 | ||
|
|
914ed10bcc | ||
|
|
92c99a7d41 | ||
|
|
af1aaba547 | ||
|
|
5a2b7546f6 | ||
|
|
4047d5af5f | ||
|
|
6857e1ceb4 | ||
|
|
4479212008 | ||
|
|
cb90ac45c3 | ||
|
|
1847666e75 | ||
|
|
aad1318b4a | ||
|
|
7a23a339e9 | ||
|
|
38d894dfe7 | ||
|
|
b293be23b0 | ||
|
|
ccb53e34ca | ||
|
|
ec9d59f3dc | ||
|
|
df72aa26c0 | ||
|
|
d3691c7ca5 | ||
|
|
562ce541a0 | ||
|
|
6ebe1e92eb | ||
|
|
1bf455cfbb | ||
|
|
290e213cd0 | ||
|
|
b1b0005574 | ||
|
|
70ea527161 | ||
|
|
34c35c84d5 | ||
|
|
bcbfc843ae | ||
|
|
d4fe46bb24 | ||
|
|
523c6f2376 | ||
|
|
b018ac67bc | ||
|
|
1a529a62aa | ||
|
|
6a46437a5f | ||
|
|
cfe8c0eeee | ||
|
|
b232fc91ab | ||
|
|
ac50f33388 | ||
|
|
ff52bb3029 | ||
|
|
627e440bd6 | ||
|
|
6bb90a1268 | ||
|
|
7d8add70a7 | ||
|
|
9094392870 | ||
|
|
c6ad23fbc0 | ||
|
|
6af7a9ed8f | ||
|
|
0b051289f5 | ||
|
|
d8329dba22 | ||
|
|
ee70a4aa72 | ||
|
|
04a58159d0 | ||
|
|
4c758fa1da | ||
|
|
c8e21802db | ||
|
|
b40ffacb8d | ||
|
|
e0118dd8eb | ||
|
|
e7194dce75 | ||
|
|
01b5bef37f | ||
|
|
403a9f7b7e | ||
|
|
10f52f2056 | ||
|
|
274c01ca74 | ||
|
|
3b82c6e38b | ||
|
|
f59a1011df | ||
|
|
82c0cb8929 | ||
|
|
2bdd9f6217 | ||
|
|
193e7d476d | ||
|
|
1b3e7d5ec4 | ||
|
|
767a8c49b0 | ||
|
|
4c43f7e9d0 | ||
|
|
3ef140e25d | ||
|
|
0a568a3e1e | ||
|
|
ef44491c69 | ||
|
|
089a2c99e2 | ||
|
|
311812c8cc | ||
|
|
a77ab59436 | ||
|
|
89fbfc6f71 | ||
|
|
28f3bcdba3 | ||
|
|
445715b9fd | ||
|
|
8843c36ec6 | ||
|
|
bd63f63b36 | ||
|
|
033e144e06 | ||
|
|
20d49f9a7c | ||
|
|
3b2caa1f5b | ||
|
|
c3769e4fce | ||
|
|
6d894dd6ee | ||
|
|
2db2b89eb1 | ||
|
|
e48c7165c5 | ||
|
|
506edaadd5 | ||
|
|
3f82a3a519 | ||
|
|
79cee864cb | ||
|
|
9f5ed938e5 | ||
|
|
4729efbd04 | ||
|
|
da9fbb8044 | ||
|
|
cf01163c8c | ||
|
|
5ba8c644e4 | ||
|
|
c833ff4a84 | ||
|
|
2a530a4bf4 | ||
|
|
6b4b653462 | ||
|
|
edb16a27d3 | ||
|
|
21df5d9bf6 | ||
|
|
73c972a604 | ||
|
|
8cdffef82a | ||
|
|
4034809281 | ||
|
|
ce6bffb65c | ||
|
|
e8bc4bedb4 | ||
|
|
b85a7ef317 | ||
|
|
9f7e310526 | ||
|
|
af7cb1d81e | ||
|
|
53ce2a2f7f | ||
|
|
fb0283e0ee | ||
|
|
5d0cfc31fa | ||
|
|
f30f0a0edc | ||
|
|
6aa538a61d | ||
|
|
7918a93a7f | ||
|
|
fe6ecb24b4 | ||
|
|
6db787d5e4 | ||
|
|
5b4385a084 | ||
|
|
4f69c3b850 | ||
|
|
c62a75ee17 | ||
|
|
d4e9c62d92 | ||
|
|
ac8a2467a5 | ||
|
|
dc1dd9ebb7 | ||
|
|
0c06d78a4f | ||
|
|
41c9ed28cd | ||
|
|
5608aa10a5 | ||
|
|
daa68a2a60 | ||
|
|
a408b5a4fe | ||
|
|
e264c97454 | ||
|
|
8790dec137 | ||
|
|
6480868e6e | ||
|
|
0578e43352 | ||
|
|
2a89d4835f | ||
|
|
5084c61016 | ||
|
|
b45f94d511 | ||
|
|
66a4752e13 | ||
|
|
4d4f78de81 | ||
|
|
0faa641c8a | ||
|
|
0f16d27a72 | ||
|
|
835ee456a5 | ||
|
|
17f3b7dbd5 | ||
|
|
171a429526 | ||
|
|
e4ee2b7c04 | ||
|
|
c85a062e23 | ||
|
|
873378fa1f | ||
|
|
4f00ad409e | ||
|
|
20b516ff11 | ||
|
|
8754bbfa89 | ||
|
|
6d92cc3d2b | ||
|
|
2f684bf4f3 | ||
|
|
45af21bf38 | ||
|
|
e6318a2d16 | ||
|
|
bef4c8a86c | ||
|
|
6e67864510 | ||
|
|
c2af4874f9 | ||
|
|
2001b91280 | ||
|
|
5460ee7edd | ||
|
|
40081e5ae7 | ||
|
|
a7c5b0ab46 | ||
|
|
e1a813e11f | ||
|
|
1dfeef0265 | ||
|
|
395610c117 | ||
|
|
ae96f82b82 | ||
|
|
2c610abcd0 | ||
|
|
d3591c8d9e | ||
|
|
ec420d5792 | ||
|
|
17209df7b5 | ||
|
|
9cf9b02ba2 | ||
|
|
c90fa2378a | ||
|
|
c04dfa922e | ||
|
|
668007707d | ||
|
|
ab71f5276f | ||
|
|
d062f62656 | ||
|
|
03db32d045 | ||
|
|
8f6d489a9a | ||
|
|
dd07fba943 | ||
|
|
6f5d642a31 | ||
|
|
2721f08bcc | ||
|
|
eafc5df3f2 | ||
|
|
46d0c29be5 | ||
|
|
abdbbf4dd2 | ||
|
|
4dc0599a7d | ||
|
|
ded0936b2a | ||
|
|
52c35ec09c | ||
|
|
76490e45bc | ||
|
|
0a8130858c | ||
|
|
ff5ba99d16 | ||
|
|
14ec82084b | ||
|
|
8e02d0a20e | ||
|
|
faa05031a7 | ||
|
|
d4cce142c5 | ||
|
|
576d89a82a | ||
|
|
4a18ef87d7 | ||
|
|
2cd92a311b | ||
|
|
94f1e48d95 | ||
|
|
19c8f0ac7a | ||
|
|
312dea7ddb | ||
|
|
fb0033947c | ||
|
|
4b8f99ed10 | ||
|
|
4a764ae1e3 | ||
|
|
5b840c1662 | ||
|
|
62d84db5a4 | ||
|
|
019d415bbd | ||
|
|
7de060ed55 | ||
|
|
cfa41b3467 | ||
|
|
0a42a11f1c | ||
|
|
063a8ce666 | ||
|
|
a2bd83382b | ||
|
|
869cace2f3 | ||
|
|
b83edf6c17 | ||
|
|
e1aa92b983 |
@@ -1 +1 @@
|
||||
f31f13994768b5b07e29624406c9b053bf4bb26e1623ac2bc1e9d4a9477502d6
|
||||
1b1ce6324c50c4595703c7df0a8a479b4fe84b71ff1a8793cce1a16f17a33324
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"--privileged",
|
||||
"-e",
|
||||
"GIT_EDITOR=code --wait"
|
||||
// uncomment and edit the path in order to pass though local USB serial to the conatiner
|
||||
// uncomment and edit the path in order to pass through local USB serial to the container
|
||||
// , "--device=/dev/ttyACM0"
|
||||
],
|
||||
"appPort": 6052,
|
||||
|
||||
4
.github/actions/build-image/action.yaml
vendored
4
.github/actions/build-image/action.yaml
vendored
@@ -47,7 +47,7 @@ runs:
|
||||
|
||||
- name: Build and push to ghcr by digest
|
||||
id: build-ghcr
|
||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
env:
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
DOCKER_BUILD_RECORD_UPLOAD: false
|
||||
@@ -73,7 +73,7 @@ runs:
|
||||
|
||||
- name: Build and push to dockerhub by digest
|
||||
id: build-dockerhub
|
||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
env:
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
DOCKER_BUILD_RECORD_UPLOAD: false
|
||||
|
||||
2
.github/actions/restore-python/action.yml
vendored
2
.github/actions/restore-python/action.yml
vendored
@@ -22,7 +22,7 @@ runs:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: venv
|
||||
# yamllint disable-line rule:line-length
|
||||
|
||||
1
.github/scripts/auto-label-pr/constants.js
vendored
1
.github/scripts/auto-label-pr/constants.js
vendored
@@ -4,6 +4,7 @@ module.exports = {
|
||||
CODEOWNERS_MARKER: '<!-- codeowners-request -->',
|
||||
TOO_BIG_MARKER: '<!-- too-big-request -->',
|
||||
DEPRECATED_COMPONENT_MARKER: '<!-- deprecated-component-request -->',
|
||||
ORG_FORK_MARKER: '<!-- maintainer-access-warning -->',
|
||||
|
||||
MANAGED_LABELS: [
|
||||
'new-component',
|
||||
|
||||
19
.github/scripts/auto-label-pr/detectors.js
vendored
19
.github/scripts/auto-label-pr/detectors.js
vendored
@@ -281,6 +281,24 @@ async function detectDeprecatedComponents(github, context, changedFiles) {
|
||||
return { labels, deprecatedInfo };
|
||||
}
|
||||
|
||||
// Strategy: Detect when maintainers cannot modify the PR branch
|
||||
function detectMaintainerAccess(context) {
|
||||
const pr = context.payload.pull_request;
|
||||
|
||||
// Only relevant for cross-repo PRs (forks)
|
||||
if (!pr.head.repo || pr.head.repo.full_name === pr.base.repo.full_name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (pr.maintainer_can_modify) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isOrgFork = pr.head.repo.owner.type === 'Organization';
|
||||
console.log(`Maintainer cannot modify PR branch (${isOrgFork ? 'org fork: ' + pr.head.repo.owner.login : 'user disabled'})`);
|
||||
return { isOrgFork, orgName: pr.head.repo.owner.login };
|
||||
}
|
||||
|
||||
// Strategy: Requirements detection
|
||||
async function detectRequirements(allLabels, prFiles, context) {
|
||||
const labels = new Set();
|
||||
@@ -329,5 +347,6 @@ module.exports = {
|
||||
detectTests,
|
||||
detectPRTemplateCheckboxes,
|
||||
detectDeprecatedComponents,
|
||||
detectMaintainerAccess,
|
||||
detectRequirements
|
||||
};
|
||||
|
||||
16
.github/scripts/auto-label-pr/index.js
vendored
16
.github/scripts/auto-label-pr/index.js
vendored
@@ -12,9 +12,10 @@ const {
|
||||
detectTests,
|
||||
detectPRTemplateCheckboxes,
|
||||
detectDeprecatedComponents,
|
||||
detectMaintainerAccess,
|
||||
detectRequirements
|
||||
} = require('./detectors');
|
||||
const { handleReviews } = require('./reviews');
|
||||
const { handleReviews, handleMaintainerAccessComment } = require('./reviews');
|
||||
const { applyLabels, removeOldLabels } = require('./labels');
|
||||
|
||||
// Fetch API data
|
||||
@@ -114,7 +115,8 @@ module.exports = async ({ github, context }) => {
|
||||
codeOwnerLabels,
|
||||
testLabels,
|
||||
checkboxLabels,
|
||||
deprecatedResult
|
||||
deprecatedResult,
|
||||
maintainerAccess
|
||||
] = await Promise.all([
|
||||
detectMergeBranch(context),
|
||||
detectComponentPlatforms(changedFiles, apiData),
|
||||
@@ -127,7 +129,8 @@ module.exports = async ({ github, context }) => {
|
||||
detectCodeOwner(github, context, changedFiles),
|
||||
detectTests(changedFiles),
|
||||
detectPRTemplateCheckboxes(context),
|
||||
detectDeprecatedComponents(github, context, changedFiles)
|
||||
detectDeprecatedComponents(github, context, changedFiles),
|
||||
detectMaintainerAccess(context)
|
||||
]);
|
||||
|
||||
// Extract deprecated component info
|
||||
@@ -177,8 +180,11 @@ module.exports = async ({ github, context }) => {
|
||||
|
||||
console.log('Computed labels:', finalLabels.join(', '));
|
||||
|
||||
// Handle reviews
|
||||
await handleReviews(github, context, finalLabels, originalLabelCount, deprecatedInfo, prFiles, totalAdditions, totalDeletions, MAX_LABELS, TOO_BIG_THRESHOLD);
|
||||
// Handle reviews and org fork comment
|
||||
await Promise.all([
|
||||
handleReviews(github, context, finalLabels, originalLabelCount, deprecatedInfo, prFiles, totalAdditions, totalDeletions, MAX_LABELS, TOO_BIG_THRESHOLD),
|
||||
handleMaintainerAccessComment(github, context, maintainerAccess)
|
||||
]);
|
||||
|
||||
// Apply labels
|
||||
await applyLabels(github, context, finalLabels);
|
||||
|
||||
92
.github/scripts/auto-label-pr/reviews.js
vendored
92
.github/scripts/auto-label-pr/reviews.js
vendored
@@ -2,7 +2,8 @@ const {
|
||||
BOT_COMMENT_MARKER,
|
||||
CODEOWNERS_MARKER,
|
||||
TOO_BIG_MARKER,
|
||||
DEPRECATED_COMPONENT_MARKER
|
||||
DEPRECATED_COMPONENT_MARKER,
|
||||
ORG_FORK_MARKER
|
||||
} = require('./constants');
|
||||
|
||||
// Generate review messages
|
||||
@@ -40,16 +41,36 @@ function generateReviewMessages(finalLabels, originalLabelCount, deprecatedInfo,
|
||||
|
||||
let message = `${TOO_BIG_MARKER}\n### 📦 Pull Request Size\n\n`;
|
||||
|
||||
message +=
|
||||
`Hey @${prAuthor}, thanks for the contribution! Just a heads up, ` +
|
||||
`this PR is on the large side `;
|
||||
|
||||
if (tooManyLabels && tooManyChanges) {
|
||||
message += `This PR is too large with ${nonTestChanges} line changes (excluding tests) and affects ${originalLabelCount} different components/areas.`;
|
||||
message +=
|
||||
`(${nonTestChanges} line changes excluding tests, across ` +
|
||||
`${originalLabelCount} different components/areas)`;
|
||||
} else if (tooManyLabels) {
|
||||
message += `This PR affects ${originalLabelCount} different components/areas.`;
|
||||
message +=
|
||||
`(it touches ${originalLabelCount} different components/areas)`;
|
||||
} else {
|
||||
message += `This PR is too large with ${nonTestChanges} line changes (excluding tests).`;
|
||||
message += `(${nonTestChanges} line changes excluding tests)`;
|
||||
}
|
||||
|
||||
message += ` Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.\n\n`;
|
||||
message += `For guidance on breaking down large PRs, see: https://developers.esphome.io/contributing/submitting-your-work/#how-to-approach-large-submissions`;
|
||||
message += `, which makes it harder for maintainers to review.\n\n`;
|
||||
message +=
|
||||
`Smaller, focused PRs tend to be reviewed much faster since they ` +
|
||||
`fit into the short gaps between other maintainer work; large ones ` +
|
||||
`often have to wait for a rare long uninterrupted block of time. ` +
|
||||
`If you can break this up into smaller pieces that can be reviewed ` +
|
||||
`independently, it will almost certainly land faster overall.\n\n`;
|
||||
message +=
|
||||
`Before putting more time in, it's also worth popping into ` +
|
||||
`\`#devs\` on [Discord](https://esphome.io/chat) so we can help ` +
|
||||
`you scope things and flag anything already in flight.\n\n`;
|
||||
message +=
|
||||
`For more details (including how to split the work up), see: ` +
|
||||
`https://developers.esphome.io/contributing/submitting-your-work/` +
|
||||
`#how-to-approach-large-submissions`;
|
||||
|
||||
messages.push(message);
|
||||
}
|
||||
@@ -136,6 +157,63 @@ async function handleReviews(github, context, finalLabels, originalLabelCount, d
|
||||
}
|
||||
}
|
||||
|
||||
// Handle maintainer access warning comment
|
||||
async function handleMaintainerAccessComment(github, context, maintainerAccess) {
|
||||
if (!maintainerAccess) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { owner, repo } = context.repo;
|
||||
const pr_number = context.issue.number;
|
||||
const prAuthor = context.payload.pull_request.user.login;
|
||||
|
||||
// Check if we already posted the warning (iterate pages to exit early)
|
||||
let existingComment;
|
||||
for await (const { data: comments } of github.paginate.iterator(
|
||||
github.rest.issues.listComments,
|
||||
{ owner, repo, issue_number: pr_number }
|
||||
)) {
|
||||
existingComment = comments.find(comment =>
|
||||
comment.user.type === 'Bot' &&
|
||||
comment.body && comment.body.includes(ORG_FORK_MARKER)
|
||||
);
|
||||
if (existingComment) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (existingComment) {
|
||||
console.log('Maintainer access warning comment already exists, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
let body;
|
||||
if (maintainerAccess.isOrgFork) {
|
||||
body = `${ORG_FORK_MARKER}\n### ⚠️ Organization Fork Detected\n\n` +
|
||||
`Hey there @${prAuthor},\n` +
|
||||
`It looks like this PR was submitted from a fork owned by the **${maintainerAccess.orgName}** organization. ` +
|
||||
`GitHub does not allow maintainers to push changes to pull request branches when the fork is owned by an organization. ` +
|
||||
`This means we won't be able to make small adjustments or fixups to your PR directly.\n\n` +
|
||||
`To allow maintainer collaboration, please re-submit this PR from a personal fork instead.\n\n` +
|
||||
`See: [Setting up the local repository](https://developers.esphome.io/contributing/development-environment/?h=org#set-up-the-local-repository) for more details.`;
|
||||
} else {
|
||||
body = `${ORG_FORK_MARKER}\n### ⚠️ Maintainer Access Disabled\n\n` +
|
||||
`Hey there @${prAuthor},\n` +
|
||||
`It looks like this PR does not have the "Allow edits from maintainers" option enabled. ` +
|
||||
`This means we won't be able to make small adjustments or fixups to your PR directly.\n\n` +
|
||||
`Please enable this option in the PR sidebar to allow maintainer collaboration.`;
|
||||
}
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr_number,
|
||||
body
|
||||
});
|
||||
console.log('Created maintainer access warning comment');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
handleReviews
|
||||
handleReviews,
|
||||
handleMaintainerAccessComment
|
||||
};
|
||||
|
||||
6
.github/workflows/auto-label-pr.yml
vendored
6
.github/workflows/auto-label-pr.yml
vendored
@@ -20,20 +20,20 @@ env:
|
||||
jobs:
|
||||
label:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.action != 'labeled' || github.event.sender.type != 'Bot'
|
||||
if: github.event.pull_request.state == 'open' && (github.event.action != 'labeled' || github.event.sender.type != 'Bot')
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v2
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v2
|
||||
with:
|
||||
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Auto Label PR
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{ steps.generate-token.outputs.token }}
|
||||
script: |
|
||||
|
||||
6
.github/workflows/ci-api-proto.yml
vendored
6
.github/workflows/ci-api-proto.yml
vendored
@@ -47,7 +47,7 @@ jobs:
|
||||
fi
|
||||
- if: failure()
|
||||
name: Review PR
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
await github.rest.pulls.createReview({
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
run: git diff
|
||||
- if: failure()
|
||||
name: Archive artifacts
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: generated-proto-files
|
||||
path: |
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
esphome/components/api/api_pb2_service.*
|
||||
- if: success()
|
||||
name: Dismiss review
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
let reviews = await github.rest.pulls.listReviews({
|
||||
|
||||
4
.github/workflows/ci-clang-tidy-hash.yml
vendored
4
.github/workflows/ci-clang-tidy-hash.yml
vendored
@@ -42,7 +42,7 @@ jobs:
|
||||
|
||||
- if: failure() && github.event.pull_request.head.repo.full_name == github.repository
|
||||
name: Request changes
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
await github.rest.pulls.createReview({
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
|
||||
- if: success() && github.event.pull_request.head.repo.full_name == github.repository
|
||||
name: Dismiss review
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
let reviews = await github.rest.pulls.listReviews({
|
||||
|
||||
76
.github/workflows/ci.yml
vendored
76
.github/workflows/ci.yml
vendored
@@ -39,7 +39,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Generate cache-key
|
||||
id: cache-key
|
||||
run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
|
||||
run: echo key="${{ hashFiles('requirements.txt', 'requirements_dev.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: venv
|
||||
# yamllint disable-line rule:line-length
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
python -m venv venv
|
||||
. venv/bin/activate
|
||||
python --version
|
||||
pip install -r requirements.txt -r requirements_test.txt pre-commit
|
||||
pip install -r requirements.txt -r requirements_dev.txt -r requirements_test.txt pre-commit
|
||||
pip install -e .
|
||||
|
||||
pylint:
|
||||
@@ -108,6 +108,34 @@ jobs:
|
||||
script/generate-esp32-boards.py --check
|
||||
script/generate-rp2040-boards.py --check
|
||||
|
||||
import-time:
|
||||
name: Check import esphome.__main__ time
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- common
|
||||
- determine-jobs
|
||||
if: needs.determine-jobs.outputs.import-time == 'true'
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Check import time against budget and write waterfall HAR
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
script/check_import_time.py --check --har importtime.har
|
||||
- name: Upload waterfall HAR
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: import-time-waterfall
|
||||
path: importtime.har
|
||||
if-no-files-found: ignore
|
||||
retention-days: 14
|
||||
|
||||
pytest:
|
||||
name: Run pytest
|
||||
strategy:
|
||||
@@ -159,7 +187,7 @@ jobs:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
- name: Save Python virtual environment cache
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: venv
|
||||
key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
|
||||
@@ -176,6 +204,7 @@ jobs:
|
||||
clang-tidy: ${{ steps.determine.outputs.clang-tidy }}
|
||||
clang-tidy-mode: ${{ steps.determine.outputs.clang-tidy-mode }}
|
||||
python-linters: ${{ steps.determine.outputs.python-linters }}
|
||||
import-time: ${{ steps.determine.outputs.import-time }}
|
||||
changed-components: ${{ steps.determine.outputs.changed-components }}
|
||||
changed-components-with-tests: ${{ steps.determine.outputs.changed-components-with-tests }}
|
||||
directly-changed-components-with-tests: ${{ steps.determine.outputs.directly-changed-components-with-tests }}
|
||||
@@ -198,7 +227,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Restore components graph cache
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: .temp/components_graph.json
|
||||
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
|
||||
@@ -219,6 +248,7 @@ jobs:
|
||||
echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT
|
||||
echo "clang-tidy-mode=$(echo "$output" | jq -r '.clang_tidy_mode')" >> $GITHUB_OUTPUT
|
||||
echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT
|
||||
echo "import-time=$(echo "$output" | jq -r '.import_time')" >> $GITHUB_OUTPUT
|
||||
echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT
|
||||
echo "changed-components-with-tests=$(echo "$output" | jq -c '.changed_components_with_tests')" >> $GITHUB_OUTPUT
|
||||
echo "directly-changed-components-with-tests=$(echo "$output" | jq -c '.directly_changed_components_with_tests')" >> $GITHUB_OUTPUT
|
||||
@@ -231,7 +261,7 @@ jobs:
|
||||
echo "benchmarks=$(echo "$output" | jq -r '.benchmarks')" >> $GITHUB_OUTPUT
|
||||
- name: Save components graph cache
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: .temp/components_graph.json
|
||||
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
|
||||
@@ -253,7 +283,7 @@ jobs:
|
||||
python-version: "3.13"
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: venv
|
||||
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
|
||||
@@ -339,7 +369,7 @@ jobs:
|
||||
echo "binary=$BINARY" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Run CodSpeed benchmarks
|
||||
uses: CodSpeedHQ/action@d872884a306dd4853acf0f584f4b706cf0cc72a2 # v4
|
||||
uses: CodSpeedHQ/action@c381be0bfd20e844fb45594f6aa182ffcd94545c # v4.15.0
|
||||
with:
|
||||
run: ${{ steps.build.outputs.binary }}
|
||||
mode: simulation
|
||||
@@ -387,14 +417,14 @@ jobs:
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref != 'refs/heads/dev'
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
||||
@@ -466,14 +496,14 @@ jobs:
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref != 'refs/heads/dev'
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
@@ -555,14 +585,14 @@ jobs:
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref != 'refs/heads/dev'
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
@@ -817,7 +847,7 @@ jobs:
|
||||
- name: Restore cached memory analysis
|
||||
id: cache-memory-analysis
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true'
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: memory-analysis-target.json
|
||||
key: ${{ steps.cache-key.outputs.cache-key }}
|
||||
@@ -841,7 +871,7 @@ jobs:
|
||||
|
||||
- name: Cache platformio
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
|
||||
@@ -868,7 +898,8 @@ jobs:
|
||||
python script/test_build_components.py \
|
||||
-e compile \
|
||||
-c "$component_list" \
|
||||
-t "$platform" 2>&1 | \
|
||||
-t "$platform" \
|
||||
--base-only 2>&1 | \
|
||||
tee /dev/stderr | \
|
||||
python script/ci_memory_impact_extract.py \
|
||||
--output-env \
|
||||
@@ -882,7 +913,7 @@ jobs:
|
||||
|
||||
- name: Save memory analysis to cache
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success'
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: memory-analysis-target.json
|
||||
key: ${{ steps.cache-key.outputs.cache-key }}
|
||||
@@ -903,7 +934,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Upload memory analysis JSON
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: memory-analysis-target
|
||||
path: memory-analysis-target.json
|
||||
@@ -929,7 +960,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Cache platformio
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
|
||||
@@ -954,7 +985,8 @@ jobs:
|
||||
python script/test_build_components.py \
|
||||
-e compile \
|
||||
-c "$component_list" \
|
||||
-t "$platform" 2>&1 | \
|
||||
-t "$platform" \
|
||||
--base-only 2>&1 | \
|
||||
tee /dev/stderr | \
|
||||
python script/ci_memory_impact_extract.py \
|
||||
--output-env \
|
||||
@@ -967,7 +999,7 @@ jobs:
|
||||
--platform "$platform"
|
||||
|
||||
- name: Upload memory analysis JSON
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: memory-analysis-pr
|
||||
path: memory-analysis-pr.json
|
||||
|
||||
72
.github/workflows/close-pr-from-fork-default-branch.yml
vendored
Normal file
72
.github/workflows/close-pr-from-fork-default-branch.yml
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
name: Close PR From Fork Default Branch
|
||||
|
||||
on:
|
||||
# pull_request_target is required so we have permission to comment and close PRs from forks.
|
||||
pull_request_target:
|
||||
types: [opened, reopened]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
close:
|
||||
name: Close PR opened from fork's default branch
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
|
||||
&& github.event.pull_request.head.ref == github.event.repository.default_branch
|
||||
steps:
|
||||
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const prNumber = context.payload.pull_request.number;
|
||||
const author = context.payload.pull_request.user.login;
|
||||
const defaultBranch = context.payload.repository.default_branch;
|
||||
const headRepo = context.payload.pull_request.head.repo.full_name;
|
||||
|
||||
const body = [
|
||||
`Hi @${author}, thanks for opening a pull request! :tada:`,
|
||||
``,
|
||||
`It looks like this PR was opened from the \`${defaultBranch}\` branch of your fork (\`${headRepo}\`), which is the same name as this repository's default branch. Working directly on \`${defaultBranch}\` in your fork causes a few problems:`,
|
||||
``,
|
||||
`- Your fork's \`${defaultBranch}\` branch will permanently diverge from \`esphome/esphome:${defaultBranch}\`, making it hard to keep your fork up to date.`,
|
||||
`- Any additional commits you push to \`${defaultBranch}\` will be added to this PR, so you can't easily work on multiple changes at once.`,
|
||||
`- Pushing maintainer fixes to your branch is awkward, since it means committing directly to your fork's default branch.`,
|
||||
`- It makes local collaboration painful — \`${defaultBranch}\` in a checkout becomes ambiguous between upstream and your fork, and maintainers end up with naming collisions when fetching your branch.`,
|
||||
``,
|
||||
`Please re-open this as a new PR from a dedicated feature branch. The usual flow looks like:`,
|
||||
``,
|
||||
`\`\`\`bash`,
|
||||
`# Make sure your fork's ${defaultBranch} is up to date with upstream`,
|
||||
`git remote add upstream https://github.com/${owner}/${repo}.git # if you haven't already`,
|
||||
`git fetch upstream`,
|
||||
`git checkout ${defaultBranch}`,
|
||||
`git reset --hard upstream/${defaultBranch}`,
|
||||
`git push --force-with-lease origin ${defaultBranch}`,
|
||||
``,
|
||||
`# Create a new branch for your change and cherry-pick / re-apply your commits there`,
|
||||
`git checkout -b my-feature-branch upstream/${defaultBranch}`,
|
||||
`# ...re-apply your changes, then:`,
|
||||
`git push origin my-feature-branch`,
|
||||
`\`\`\``,
|
||||
``,
|
||||
`Then open a new pull request from \`my-feature-branch\` into \`${owner}/${repo}:${defaultBranch}\`.`,
|
||||
``,
|
||||
`Closing this PR for now — sorry for the friction, and thanks again for contributing! :heart:`,
|
||||
].join('\n');
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
body,
|
||||
});
|
||||
|
||||
await github.rest.pulls.update({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: prNumber,
|
||||
state: 'closed',
|
||||
});
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
CODEOWNERS
|
||||
|
||||
- name: Check codeowner approval and update label
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
with:
|
||||
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
|
||||
- name: Request reviews from component codeowners
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const { loadCodeowners, getEffectiveOwners } = require('./.github/scripts/codeowners.js');
|
||||
|
||||
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -58,7 +58,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
|
||||
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
@@ -86,6 +86,6 @@ jobs:
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
|
||||
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
2
.github/workflows/external-component-bot.yml
vendored
2
.github/workflows/external-component-bot.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Add external component comment
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
|
||||
2
.github/workflows/issue-codeowner-notify.yml
vendored
2
.github/workflows/issue-codeowner-notify.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Notify codeowners for component issues
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const owner = context.repo.owner;
|
||||
|
||||
2
.github/workflows/lock.yml
vendored
2
.github/workflows/lock.yml
vendored
@@ -8,4 +8,4 @@ on:
|
||||
|
||||
jobs:
|
||||
lock:
|
||||
uses: esphome/workflows/.github/workflows/lock.yml@main
|
||||
uses: esphome/workflows/.github/workflows/lock.yml@3c4e8446aa1029f1c346a482034b3ee1489077ca # 2026.4.0
|
||||
|
||||
2
.github/workflows/pr-title-check.yml
vendored
2
.github/workflows/pr-title-check.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const {
|
||||
|
||||
14
.github/workflows/release.yml
vendored
14
.github/workflows/release.yml
vendored
@@ -138,7 +138,7 @@ jobs:
|
||||
# version: ${{ needs.init.outputs.tag }}
|
||||
|
||||
- name: Upload digests
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: digests-${{ matrix.platform.arch }}
|
||||
path: /tmp/digests
|
||||
@@ -221,7 +221,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||
with:
|
||||
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
@@ -229,7 +229,7 @@ jobs:
|
||||
repositories: home-assistant-addon
|
||||
|
||||
- name: Trigger Workflow
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{ steps.generate-token.outputs.token }}
|
||||
script: |
|
||||
@@ -256,7 +256,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||
with:
|
||||
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
@@ -264,7 +264,7 @@ jobs:
|
||||
repositories: esphome-schema
|
||||
|
||||
- name: Trigger Workflow
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{ steps.generate-token.outputs.token }}
|
||||
script: |
|
||||
@@ -287,7 +287,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||
with:
|
||||
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
@@ -295,7 +295,7 @@ jobs:
|
||||
repositories: version-notifier
|
||||
|
||||
- name: Trigger Workflow
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{ steps.generate-token.outputs.token }}
|
||||
script: |
|
||||
|
||||
27
.github/workflows/status-check-labels.yml
vendored
27
.github/workflows/status-check-labels.yml
vendored
@@ -2,30 +2,29 @@ name: Status check labels
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled, unlabeled]
|
||||
types: [opened, reopened, labeled, unlabeled, synchronize]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Check ${{ matrix.label }}
|
||||
name: Check blocking labels
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
label:
|
||||
- needs-docs
|
||||
- merge-after-release
|
||||
- chained-pr
|
||||
steps:
|
||||
- name: Check for ${{ matrix.label }} label
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
- name: Check for blocking labels
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const blockingLabels = ['needs-docs', 'merge-after-release', 'chained-pr'];
|
||||
const { data: labels } = await github.rest.issues.listLabelsOnIssue({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number
|
||||
});
|
||||
const hasLabel = labels.find(label => label.name === '${{ matrix.label }}');
|
||||
if (hasLabel) {
|
||||
core.setFailed('Pull request cannot be merged, it is labeled as ${{ matrix.label }}');
|
||||
const labelNames = labels.map(l => l.name);
|
||||
const found = blockingLabels.filter(bl => labelNames.includes(bl));
|
||||
if (found.length > 0) {
|
||||
core.setFailed(`Pull request cannot be merged, it has blocking label(s): ${found.join(', ')}`);
|
||||
}
|
||||
|
||||
2
.github/workflows/sync-device-classes.yml
vendored
2
.github/workflows/sync-device-classes.yml
vendored
@@ -41,7 +41,7 @@ jobs:
|
||||
python script/run-in-env.py pre-commit run --all-files
|
||||
|
||||
- name: Commit changes
|
||||
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
|
||||
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
||||
with:
|
||||
commit-message: "Synchronise Device Classes from Home Assistant"
|
||||
committer: esphomebot <esphome@openhomefoundation.org>
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -146,5 +146,6 @@ sdkconfig.*
|
||||
|
||||
/components
|
||||
/managed_components
|
||||
/dependencies.lock
|
||||
|
||||
api-docs/
|
||||
|
||||
@@ -11,7 +11,7 @@ ci:
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.15.9
|
||||
rev: v0.15.12
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
@@ -58,6 +58,7 @@ repos:
|
||||
entry: python3 script/run-in-env.py pylint
|
||||
language: system
|
||||
types: [python]
|
||||
files: ^esphome/.+\.py$
|
||||
- id: clang-tidy-hash
|
||||
name: Update clang-tidy hash
|
||||
entry: python script/clang_tidy_hash.py --update-if-changed
|
||||
|
||||
10
CODEOWNERS
10
CODEOWNERS
@@ -56,6 +56,7 @@ esphome/components/audio_adc/* @kbx81
|
||||
esphome/components/audio_dac/* @kbx81
|
||||
esphome/components/audio_file/* @kahrendt
|
||||
esphome/components/audio_file/media_source/* @kahrendt
|
||||
esphome/components/audio_http/* @kahrendt
|
||||
esphome/components/axs15231/* @clydebarrow
|
||||
esphome/components/b_parasite/* @rbaron
|
||||
esphome/components/ballu/* @bazuchan
|
||||
@@ -346,6 +347,7 @@ esphome/components/modbus_controller/select/* @martgras @stegm
|
||||
esphome/components/modbus_controller/sensor/* @martgras
|
||||
esphome/components/modbus_controller/switch/* @martgras
|
||||
esphome/components/modbus_controller/text_sensor/* @martgras
|
||||
esphome/components/modbus_server/* @exciton
|
||||
esphome/components/mopeka_ble/* @Fabian-Schmidt @spbrogan
|
||||
esphome/components/mopeka_pro_check/* @spbrogan
|
||||
esphome/components/mopeka_std_check/* @Fabian-Schmidt
|
||||
@@ -403,6 +405,7 @@ esphome/components/qmp6988/* @andrewpc
|
||||
esphome/components/qr_code/* @wjtje
|
||||
esphome/components/qspi_dbi/* @clydebarrow
|
||||
esphome/components/qwiic_pir/* @kahrendt
|
||||
esphome/components/radio_frequency/* @kbx81
|
||||
esphome/components/radon_eye_ble/* @jeffeb3
|
||||
esphome/components/radon_eye_rd200/* @jeffeb3
|
||||
esphome/components/rc522/* @glmnet
|
||||
@@ -438,6 +441,11 @@ esphome/components/sen0321/* @notjj
|
||||
esphome/components/sen21231/* @shreyaskarnik
|
||||
esphome/components/sen5x/* @martgras
|
||||
esphome/components/sen6x/* @martgras @mebner86 @mikelawrence @tuct
|
||||
esphome/components/sendspin/* @kahrendt
|
||||
esphome/components/sendspin/media_player/* @kahrendt
|
||||
esphome/components/sendspin/media_source/* @kahrendt
|
||||
esphome/components/sendspin/sensor/* @kahrendt
|
||||
esphome/components/sendspin/text_sensor/* @kahrendt
|
||||
esphome/components/sensirion_common/* @martgras
|
||||
esphome/components/sensor/* @esphome/core
|
||||
esphome/components/serial_proxy/* @kbx81
|
||||
@@ -599,6 +607,6 @@ esphome/components/xxtea/* @clydebarrow
|
||||
esphome/components/zephyr/* @tomaszduda23
|
||||
esphome/components/zephyr_mcumgr/ota/* @tomaszduda23
|
||||
esphome/components/zhlt01/* @cfeenstra1024
|
||||
esphome/components/zigbee/* @tomaszduda23
|
||||
esphome/components/zigbee/* @luar123 @tomaszduda23
|
||||
esphome/components/zio_ultrasonic/* @kahrendt
|
||||
esphome/components/zwave_proxy/* @kbx81
|
||||
|
||||
2
Doxyfile
2
Doxyfile
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
||||
# could be handy for archiving the generated documentation or if some version
|
||||
# control system is used.
|
||||
|
||||
PROJECT_NUMBER = 2026.4.0-dev
|
||||
PROJECT_NUMBER = 2026.5.0-dev
|
||||
|
||||
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
||||
# for a project that appears at the top of each page and should give viewer a
|
||||
|
||||
@@ -4,4 +4,5 @@ include requirements.txt
|
||||
recursive-include esphome *.yaml
|
||||
recursive-include esphome *.cpp *.h *.tcc *.c
|
||||
recursive-include esphome *.py.script
|
||||
recursive-include esphome *.jinja
|
||||
recursive-include esphome LICENSE.txt
|
||||
|
||||
@@ -39,6 +39,7 @@ from esphome.const import (
|
||||
CONF_MDNS,
|
||||
CONF_MQTT,
|
||||
CONF_NAME,
|
||||
CONF_NAME_ADD_MAC_SUFFIX,
|
||||
CONF_OTA,
|
||||
CONF_PASSWORD,
|
||||
CONF_PLATFORM,
|
||||
@@ -71,6 +72,7 @@ from esphome.util import (
|
||||
run_external_process,
|
||||
safe_print,
|
||||
)
|
||||
from esphome.zeroconf import discover_mdns_devices
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -204,6 +206,64 @@ def _resolve_with_cache(address: str, purpose: Purpose) -> list[str]:
|
||||
return [address]
|
||||
|
||||
|
||||
def _populate_mdns_cache(hosts_to_addresses: dict[str, list[str]]) -> None:
|
||||
"""Store discovered ``host -> [ips]`` entries in ``CORE.address_cache``.
|
||||
|
||||
Ensures ``CORE.address_cache`` exists, then records each mDNS hostname so
|
||||
the downstream resolution path (``resolve_ip_address``) can skip opening a
|
||||
second Zeroconf client.
|
||||
"""
|
||||
from esphome.address_cache import AddressCache
|
||||
|
||||
if CORE.address_cache is None:
|
||||
CORE.address_cache = AddressCache()
|
||||
for host, addresses in hosts_to_addresses.items():
|
||||
if addresses:
|
||||
_LOGGER.debug("Caching mDNS result %s -> %s", host, addresses)
|
||||
CORE.address_cache.add_mdns_addresses(host, addresses)
|
||||
|
||||
|
||||
def _discover_mac_suffix_devices() -> list[str] | None:
|
||||
"""Discover ``<name>-<mac>.local`` devices and cache their IPs.
|
||||
|
||||
Returns:
|
||||
- ``None`` when discovery isn't applicable (``name_add_mac_suffix`` off,
|
||||
mDNS disabled, or ``CORE.address`` is already an IP). Callers should
|
||||
then fall back to whatever default OTA address they normally use.
|
||||
- ``[]`` when discovery ran but found nothing. Callers should NOT fall
|
||||
back to the base name: with ``name_add_mac_suffix`` enabled, the base
|
||||
name by definition doesn't exist on the network.
|
||||
- A non-empty sorted list of ``.local`` hostnames on success.
|
||||
|
||||
Populates ``CORE.address_cache`` so downstream resolution (``espota2`` or
|
||||
``aioesphomeapi`` via :func:`_resolve_network_devices`) reuses the IPs we
|
||||
already have without opening a second Zeroconf client.
|
||||
"""
|
||||
if not (has_name_add_mac_suffix() and has_mdns() and has_non_ip_address()):
|
||||
return None
|
||||
_LOGGER.info("Discovering devices...")
|
||||
if not (discovered := discover_mdns_devices(CORE.name)):
|
||||
_LOGGER.warning(
|
||||
"No devices matching '%s-<mac>.local' were discovered.", CORE.name
|
||||
)
|
||||
return []
|
||||
_populate_mdns_cache(discovered)
|
||||
return list(discovered)
|
||||
|
||||
|
||||
def _ota_hostnames_for_default(purpose: Purpose) -> list[str]:
|
||||
"""Return OTA hostname(s) for the ``--device OTA`` / default-resolve path.
|
||||
|
||||
When ``name_add_mac_suffix`` is enabled, returns discovered
|
||||
``<name>-<mac>.local`` hostnames (possibly empty — in which case the
|
||||
caller should not fall back to the base name). Otherwise falls back to
|
||||
the cache-resolved ``CORE.address``.
|
||||
"""
|
||||
if (discovered := _discover_mac_suffix_devices()) is not None:
|
||||
return discovered
|
||||
return _resolve_with_cache(CORE.address, purpose)
|
||||
|
||||
|
||||
def choose_upload_log_host(
|
||||
default: list[str] | str | None,
|
||||
check_default: str | None,
|
||||
@@ -242,14 +302,14 @@ def choose_upload_log_host(
|
||||
resolved.append("MQTT")
|
||||
|
||||
if has_api() and has_non_ip_address() and has_resolvable_address():
|
||||
resolved.extend(_resolve_with_cache(CORE.address, purpose))
|
||||
resolved.extend(_ota_hostnames_for_default(purpose))
|
||||
|
||||
elif purpose == Purpose.UPLOADING:
|
||||
if has_ota() and has_mqtt_ip_lookup():
|
||||
resolved.append("MQTTIP")
|
||||
|
||||
if has_ota() and has_non_ip_address() and has_resolvable_address():
|
||||
resolved.extend(_resolve_with_cache(CORE.address, purpose))
|
||||
resolved.extend(_ota_hostnames_for_default(purpose))
|
||||
else:
|
||||
resolved.append(device)
|
||||
if not resolved:
|
||||
@@ -281,22 +341,29 @@ def choose_upload_log_host(
|
||||
elif bootsel.permission_error:
|
||||
bootsel_permission_error = True
|
||||
|
||||
def add_ota_options() -> None:
|
||||
"""Add OTA options, using mDNS discovery if name_add_mac_suffix is enabled."""
|
||||
if (discovered := _discover_mac_suffix_devices()) is not None:
|
||||
# Discovery was applicable. Use whatever we found — on empty,
|
||||
# intentionally skip the base-name fallback since with
|
||||
# name_add_mac_suffix on, the base name doesn't exist on the net.
|
||||
for host in discovered:
|
||||
options.append((f"Over The Air ({host})", host))
|
||||
elif has_resolvable_address():
|
||||
options.append((f"Over The Air ({CORE.address})", CORE.address))
|
||||
if has_mqtt_ip_lookup():
|
||||
options.append(("Over The Air (MQTT IP lookup)", "MQTTIP"))
|
||||
|
||||
if purpose == Purpose.LOGGING:
|
||||
if has_mqtt_logging():
|
||||
mqtt_config = CORE.config[CONF_MQTT]
|
||||
options.append((f"MQTT ({mqtt_config[CONF_BROKER]})", "MQTT"))
|
||||
|
||||
if has_api():
|
||||
if has_resolvable_address():
|
||||
options.append((f"Over The Air ({CORE.address})", CORE.address))
|
||||
if has_mqtt_ip_lookup():
|
||||
options.append(("Over The Air (MQTT IP lookup)", "MQTTIP"))
|
||||
add_ota_options()
|
||||
|
||||
elif purpose == Purpose.UPLOADING and has_ota():
|
||||
if has_resolvable_address():
|
||||
options.append((f"Over The Air ({CORE.address})", CORE.address))
|
||||
if has_mqtt_ip_lookup():
|
||||
options.append(("Over The Air (MQTT IP lookup)", "MQTTIP"))
|
||||
add_ota_options()
|
||||
|
||||
# Show helpful BOOTSEL instructions for RP2040 when no BOOTSEL device is found
|
||||
if (
|
||||
@@ -407,7 +474,17 @@ def has_resolvable_address() -> bool:
|
||||
return not CORE.address.endswith(".local")
|
||||
|
||||
|
||||
def mqtt_get_ip(config: ConfigType, username: str, password: str, client_id: str):
|
||||
def has_name_add_mac_suffix() -> bool:
|
||||
"""Check if name_add_mac_suffix is enabled in the config."""
|
||||
if CORE.config is None:
|
||||
return False
|
||||
esphome_config = CORE.config.get(CONF_ESPHOME, {})
|
||||
return esphome_config.get(CONF_NAME_ADD_MAC_SUFFIX, False)
|
||||
|
||||
|
||||
def mqtt_get_ip(
|
||||
config: ConfigType, username: str, password: str, client_id: str
|
||||
) -> list[str]:
|
||||
from esphome import mqtt
|
||||
|
||||
return mqtt.get_esphome_device_ip(config, username, password, client_id)
|
||||
@@ -420,6 +497,9 @@ def _resolve_network_devices(
|
||||
|
||||
This function filters the devices list to:
|
||||
- Replace MQTT/MQTTIP magic strings with actual IP addresses via MQTT lookup
|
||||
- Expand hostnames that are already in ``CORE.address_cache`` to their
|
||||
cached IPs so downstream code (e.g. aioesphomeapi) doesn't open a second
|
||||
Zeroconf client to resolve them
|
||||
- Deduplicate addresses while preserving order
|
||||
- Only resolve MQTT once even if multiple MQTT strings are present
|
||||
- If MQTT resolution fails, log a warning and continue with other devices
|
||||
@@ -444,13 +524,29 @@ def _resolve_network_devices(
|
||||
mqtt_ips = mqtt_get_ip(
|
||||
config, args.username, args.password, args.client_id
|
||||
)
|
||||
network_devices.extend(mqtt_ips)
|
||||
# pylint can't infer mqtt_get_ip's return through its
|
||||
# lazy ``from esphome import mqtt`` import, so it flags
|
||||
# the genexpr below.
|
||||
network_devices.extend(
|
||||
addr
|
||||
for addr in mqtt_ips # pylint: disable=not-an-iterable
|
||||
if addr not in network_devices
|
||||
)
|
||||
except EsphomeError as err:
|
||||
_LOGGER.warning(
|
||||
"MQTT IP discovery failed (%s), will try other devices if available",
|
||||
err,
|
||||
)
|
||||
mqtt_resolved = True
|
||||
continue
|
||||
|
||||
# If the hostname is already in the address cache (e.g. populated by
|
||||
# mDNS discovery), substitute the cached IPs so aioesphomeapi doesn't
|
||||
# open its own Zeroconf to re-resolve it.
|
||||
if CORE.address_cache and (cached := CORE.address_cache.get_addresses(device)):
|
||||
network_devices.extend(
|
||||
addr for addr in cached if addr not in network_devices
|
||||
)
|
||||
elif device not in network_devices:
|
||||
# Regular network address or IP - add if not already present
|
||||
network_devices.append(device)
|
||||
@@ -750,8 +846,15 @@ def upload_using_esptool(
|
||||
platformio_api.FlashImage(
|
||||
path=idedata.firmware_bin_path, offset=firmware_offset
|
||||
),
|
||||
*idedata.extra_flash_images,
|
||||
]
|
||||
for image in idedata.extra_flash_images:
|
||||
if not image.path.is_file():
|
||||
_LOGGER.warning(
|
||||
"Skipping missing flash image declared by platform: %s",
|
||||
image.path,
|
||||
)
|
||||
continue
|
||||
flash_images.append(image)
|
||||
|
||||
mcu = "esp8266"
|
||||
if CORE.is_esp32:
|
||||
|
||||
@@ -101,6 +101,17 @@ class AddressCache:
|
||||
"""Check if any cache entries exist."""
|
||||
return bool(self.mdns_cache or self.dns_cache)
|
||||
|
||||
def add_mdns_addresses(self, hostname: str, addresses: list[str]) -> None:
|
||||
"""Store resolved mDNS addresses for ``hostname`` in the cache.
|
||||
|
||||
Callers that discover ``.local`` hosts (e.g. via mDNS browse) can use
|
||||
this to avoid a second resolution round-trip during the upload path.
|
||||
No-op when ``addresses`` is empty.
|
||||
"""
|
||||
if not addresses:
|
||||
return
|
||||
self.mdns_cache[normalize_hostname(hostname)] = addresses
|
||||
|
||||
@classmethod
|
||||
def from_cli_args(
|
||||
cls, mdns_args: Iterable[str], dns_args: Iterable[str]
|
||||
|
||||
56
esphome/async_thread.py
Normal file
56
esphome/async_thread.py
Normal file
@@ -0,0 +1,56 @@
|
||||
"""Helpers for running an async coroutine from sync code via a daemon thread.
|
||||
|
||||
``asyncio.run(coro())`` in the main thread blocks until the loop's cleanup
|
||||
cycle finishes, which can add hundreds of milliseconds before the caller
|
||||
receives the result. Running the loop in a daemon thread lets the caller
|
||||
observe the result as soon as the coroutine completes while cleanup finishes
|
||||
in the background.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Awaitable, Callable
|
||||
import threading
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
||||
class AsyncThreadRunner(threading.Thread, Generic[_T]):
|
||||
"""Run an async coroutine in a daemon thread and expose its result.
|
||||
|
||||
The runner catches all exceptions from the coroutine and stores them in
|
||||
``exception`` so ``event`` is always set — this prevents callers waiting
|
||||
on ``event`` from hanging forever when the coroutine crashes.
|
||||
|
||||
Typical usage::
|
||||
|
||||
runner = AsyncThreadRunner(lambda: my_coro(arg))
|
||||
runner.start()
|
||||
if not runner.event.wait(timeout=5.0):
|
||||
... # timed out
|
||||
if runner.exception is not None:
|
||||
raise runner.exception
|
||||
result = runner.result
|
||||
"""
|
||||
|
||||
def __init__(self, coro_factory: Callable[[], Awaitable[_T]]) -> None:
|
||||
super().__init__(daemon=True)
|
||||
self._coro_factory = coro_factory
|
||||
self.result: _T | None = None
|
||||
self.exception: BaseException | None = None
|
||||
self.event = threading.Event()
|
||||
|
||||
async def _runner(self) -> None:
|
||||
try:
|
||||
self.result = await self._coro_factory()
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
# Capture all exceptions so ``event`` is always set — otherwise a
|
||||
# crash would hang the waiter forever.
|
||||
self.exception = exc
|
||||
finally:
|
||||
self.event.set()
|
||||
|
||||
def run(self) -> None:
|
||||
asyncio.run(self._runner())
|
||||
@@ -199,11 +199,10 @@ def validate_automation(extra_schema=None, extra_validators=None, single=False):
|
||||
return cv.Schema([schema])(value)
|
||||
except cv.Invalid as err2:
|
||||
if "extra keys not allowed" in str(err2) and len(err2.path) == 2:
|
||||
# pylint: disable=raise-missing-from
|
||||
raise err
|
||||
raise err from None
|
||||
if "Unable to find action" in str(err):
|
||||
raise err2
|
||||
raise cv.MultipleInvalid([err, err2])
|
||||
raise err2 from None
|
||||
raise cv.MultipleInvalid([err, err2]) from None
|
||||
elif isinstance(value, dict):
|
||||
if CONF_THEN in value:
|
||||
return [schema(value)]
|
||||
@@ -598,7 +597,7 @@ async def component_resume_action_to_code(
|
||||
comp = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, comp)
|
||||
if CONF_UPDATE_INTERVAL in config:
|
||||
template_ = await cg.templatable(config[CONF_UPDATE_INTERVAL], args, int)
|
||||
template_ = await cg.templatable(config[CONF_UPDATE_INTERVAL], args, cg.uint32)
|
||||
cg.add(var.set_update_interval(template_))
|
||||
return var
|
||||
|
||||
|
||||
@@ -151,8 +151,8 @@ class ConfigBundleCreator:
|
||||
|
||||
def __init__(self, config: dict[str, Any]) -> None:
|
||||
self._config = config
|
||||
self._config_dir = CORE.config_dir
|
||||
self._config_path = CORE.config_path
|
||||
self._config_dir = Path(CORE.config_dir).resolve()
|
||||
self._config_path = Path(CORE.config_path).resolve()
|
||||
self._files: list[BundleFile] = []
|
||||
self._seen_paths: set[Path] = set()
|
||||
self._secrets_paths: set[Path] = set()
|
||||
@@ -258,21 +258,36 @@ class ConfigBundleCreator:
|
||||
def _discover_yaml_includes(self) -> None:
|
||||
"""Discover YAML files loaded during config parsing.
|
||||
|
||||
We track files by wrapping _load_yaml_internal. The config has already
|
||||
been loaded at this point (bundle is a POST_CONFIG_ACTION), so we
|
||||
re-load just to discover the file list.
|
||||
Deliberately uses a fresh re-parse and force-loads every deferred
|
||||
``IncludeFile`` to include *all* potentially-reachable includes,
|
||||
even branches not selected by the local substitutions. Bundles are
|
||||
meant to be compiled on another system where command-line
|
||||
substitution overrides may choose a different branch — e.g.
|
||||
``!include network/${eth_model}/config.yaml`` must ship every
|
||||
candidate so the remote build can pick any one.
|
||||
|
||||
Entries with unresolved substitution variables in the filename
|
||||
path are skipped with a warning (they cannot be resolved without
|
||||
the substitution pass).
|
||||
|
||||
Secrets files are tracked separately so we can filter them to
|
||||
only include the keys this config actually references.
|
||||
"""
|
||||
# Must be a fresh parse: IncludeFile.load() caches its result in
|
||||
# _content, and we discover files by listening for loader calls. On
|
||||
# an already-parsed tree the cache is populated, .load() returns
|
||||
# without calling the loader, the listener never fires, and the
|
||||
# referenced files would be silently dropped from the bundle.
|
||||
with yaml_util.track_yaml_loads() as loaded_files:
|
||||
try:
|
||||
yaml_util.load_yaml(self._config_path)
|
||||
data = yaml_util.load_yaml(self._config_path)
|
||||
except EsphomeError:
|
||||
_LOGGER.debug(
|
||||
"Bundle: re-loading YAML for include discovery failed, "
|
||||
"proceeding with partial file list"
|
||||
)
|
||||
else:
|
||||
_force_load_include_files(data)
|
||||
|
||||
for fpath in loaded_files:
|
||||
if fpath == self._config_path.resolve():
|
||||
@@ -608,6 +623,57 @@ def _add_bytes_to_tar(tar: tarfile.TarFile, name: str, data: bytes) -> None:
|
||||
tar.addfile(info, io.BytesIO(data))
|
||||
|
||||
|
||||
def _force_load_include_files(obj: Any, _seen: set[int] | None = None) -> None:
|
||||
"""Recursively resolve any ``IncludeFile`` instances in a YAML tree.
|
||||
|
||||
Nested ``!include`` returns a deferred ``IncludeFile`` that is only
|
||||
resolved during the substitution pass. During bundle discovery we need
|
||||
the referenced files to actually load so the ``track_yaml_loads``
|
||||
listener fires for them.
|
||||
|
||||
``IncludeFile`` instances with unresolved substitution variables in the
|
||||
filename cannot be loaded — we skip and warn about those.
|
||||
"""
|
||||
if _seen is None:
|
||||
_seen = set()
|
||||
|
||||
if isinstance(obj, yaml_util.IncludeFile):
|
||||
if id(obj) in _seen:
|
||||
return
|
||||
_seen.add(id(obj))
|
||||
if obj.has_unresolved_expressions():
|
||||
_LOGGER.warning(
|
||||
"Bundle: cannot resolve !include %s (referenced from %s) "
|
||||
"with substitutions in path",
|
||||
obj.file,
|
||||
obj.parent_file,
|
||||
)
|
||||
return
|
||||
try:
|
||||
loaded = obj.load()
|
||||
except EsphomeError as err:
|
||||
_LOGGER.warning(
|
||||
"Bundle: failed to load !include %s (referenced from %s): %s",
|
||||
obj.file,
|
||||
obj.parent_file,
|
||||
err,
|
||||
)
|
||||
return
|
||||
_force_load_include_files(loaded, _seen)
|
||||
elif isinstance(obj, dict):
|
||||
if id(obj) in _seen:
|
||||
return
|
||||
_seen.add(id(obj))
|
||||
for value in obj.values():
|
||||
_force_load_include_files(value, _seen)
|
||||
elif isinstance(obj, (list, tuple)):
|
||||
if id(obj) in _seen:
|
||||
return
|
||||
_seen.add(id(obj))
|
||||
for item in obj:
|
||||
_force_load_include_files(item, _seen)
|
||||
|
||||
|
||||
def _resolve_include_path(include_path: Any) -> Path | None:
|
||||
"""Resolve an include path to absolute, skipping system includes."""
|
||||
if isinstance(include_path, str) and include_path.startswith("<"):
|
||||
|
||||
@@ -79,6 +79,7 @@ from esphome.cpp_types import ( # noqa: F401
|
||||
float_,
|
||||
global_ns,
|
||||
gpio_Flags,
|
||||
int8,
|
||||
int16,
|
||||
int32,
|
||||
int64,
|
||||
|
||||
@@ -190,7 +190,7 @@ void AcDimmer::setup() {
|
||||
this->zero_cross_pin_->setup();
|
||||
this->store_.zero_cross_pin = this->zero_cross_pin_->to_isr();
|
||||
this->zero_cross_pin_->attach_interrupt(&AcDimmerDataStore::s_gpio_intr, &this->store_,
|
||||
gpio::INTERRUPT_FALLING_EDGE);
|
||||
this->zero_cross_interrupt_type_);
|
||||
}
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
@@ -226,19 +226,25 @@ void AcDimmer::write_state(float state) {
|
||||
void AcDimmer::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"AcDimmer:\n"
|
||||
" Min Power: %.1f%%\n"
|
||||
" Init with half cycle: %s",
|
||||
" Min Power: %.1f%%\n"
|
||||
" Init with half cycle: %s",
|
||||
this->store_.min_power / 10.0f, YESNO(this->init_with_half_cycle_));
|
||||
LOG_PIN(" Output Pin: ", this->gate_pin_);
|
||||
LOG_PIN(" Zero-Cross Pin: ", this->zero_cross_pin_);
|
||||
if (method_ == DIM_METHOD_LEADING_PULSE) {
|
||||
ESP_LOGCONFIG(TAG, " Method: leading pulse");
|
||||
} else if (method_ == DIM_METHOD_LEADING) {
|
||||
ESP_LOGCONFIG(TAG, " Method: leading");
|
||||
if (this->zero_cross_interrupt_type_ == gpio::INTERRUPT_RISING_EDGE) {
|
||||
ESP_LOGCONFIG(TAG, " Interrupt Type: rising");
|
||||
} else if (this->zero_cross_interrupt_type_ == gpio::INTERRUPT_FALLING_EDGE) {
|
||||
ESP_LOGCONFIG(TAG, " Interrupt Type: falling");
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " Method: trailing");
|
||||
ESP_LOGCONFIG(TAG, " Interrupt Type: any");
|
||||
}
|
||||
if (method_ == DIM_METHOD_LEADING_PULSE) {
|
||||
ESP_LOGCONFIG(TAG, " Method: leading pulse");
|
||||
} else if (method_ == DIM_METHOD_LEADING) {
|
||||
ESP_LOGCONFIG(TAG, " Method: leading");
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " Method: trailing");
|
||||
}
|
||||
|
||||
LOG_FLOAT_OUTPUT(this);
|
||||
ESP_LOGV(TAG, " Estimated Frequency: %.3fHz", 1e6f / this->store_.cycle_time_us / 2);
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ class AcDimmer : public output::FloatOutput, public Component {
|
||||
void dump_config() override;
|
||||
void set_gate_pin(InternalGPIOPin *gate_pin) { gate_pin_ = gate_pin; }
|
||||
void set_zero_cross_pin(InternalGPIOPin *zero_cross_pin) { zero_cross_pin_ = zero_cross_pin; }
|
||||
void set_zero_cross_interrupt_type(gpio::InterruptType type) { zero_cross_interrupt_type_ = type; }
|
||||
void set_init_with_half_cycle(bool init_with_half_cycle) { init_with_half_cycle_ = init_with_half_cycle; }
|
||||
void set_method(DimMethod method) { method_ = method; }
|
||||
|
||||
@@ -56,6 +57,7 @@ class AcDimmer : public output::FloatOutput, public Component {
|
||||
|
||||
InternalGPIOPin *gate_pin_;
|
||||
InternalGPIOPin *zero_cross_pin_;
|
||||
gpio::InterruptType zero_cross_interrupt_type_;
|
||||
AcDimmerDataStore store_;
|
||||
bool init_with_half_cycle_;
|
||||
DimMethod method_;
|
||||
|
||||
@@ -7,6 +7,8 @@ from esphome.core import CORE
|
||||
|
||||
CODEOWNERS = ["@glmnet"]
|
||||
|
||||
gpio_ns = cg.esphome_ns.namespace("gpio")
|
||||
|
||||
ac_dimmer_ns = cg.esphome_ns.namespace("ac_dimmer")
|
||||
AcDimmer = ac_dimmer_ns.class_("AcDimmer", output.FloatOutput, cg.Component)
|
||||
|
||||
@@ -17,15 +19,26 @@ DIM_METHODS = {
|
||||
"TRAILING": DimMethod.DIM_METHOD_TRAILING,
|
||||
}
|
||||
|
||||
ZC_INTERRUPT_TYPES = {
|
||||
"RISING": gpio_ns.INTERRUPT_RISING_EDGE,
|
||||
"FALLING": gpio_ns.INTERRUPT_FALLING_EDGE,
|
||||
"ANY": gpio_ns.INTERRUPT_ANY_EDGE,
|
||||
}
|
||||
|
||||
CONF_GATE_PIN = "gate_pin"
|
||||
CONF_ZERO_CROSS_PIN = "zero_cross_pin"
|
||||
CONF_INIT_WITH_HALF_CYCLE = "init_with_half_cycle"
|
||||
CONF_ZERO_CROSS_INTERRUPT_TYPE = "zero_cross_interrupt_type"
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
output.FLOAT_OUTPUT_SCHEMA.extend(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(AcDimmer),
|
||||
cv.Required(CONF_GATE_PIN): pins.internal_gpio_output_pin_schema,
|
||||
cv.Required(CONF_ZERO_CROSS_PIN): pins.internal_gpio_input_pin_schema,
|
||||
cv.Optional(CONF_ZERO_CROSS_INTERRUPT_TYPE, default="FALLING"): cv.enum(
|
||||
ZC_INTERRUPT_TYPES, upper=True, space="_"
|
||||
),
|
||||
cv.Optional(CONF_INIT_WITH_HALF_CYCLE, default=True): cv.boolean,
|
||||
cv.Optional(CONF_METHOD, default="leading pulse"): cv.enum(
|
||||
DIM_METHODS, upper=True, space="_"
|
||||
@@ -54,5 +67,6 @@ async def to_code(config):
|
||||
cg.add(var.set_gate_pin(pin))
|
||||
pin = await cg.gpio_pin_expression(config[CONF_ZERO_CROSS_PIN])
|
||||
cg.add(var.set_zero_cross_pin(pin))
|
||||
cg.add(var.set_zero_cross_interrupt_type(config[CONF_ZERO_CROSS_INTERRUPT_TYPE]))
|
||||
cg.add(var.set_init_with_half_cycle(config[CONF_INIT_WITH_HALF_CYCLE]))
|
||||
cg.add(var.set_method(config[CONF_METHOD]))
|
||||
|
||||
@@ -2,7 +2,11 @@ import logging
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor, voltage_sampler
|
||||
from esphome.components.esp32 import get_esp32_variant, include_builtin_idf_component
|
||||
from esphome.components.esp32 import (
|
||||
get_esp32_variant,
|
||||
include_builtin_idf_component,
|
||||
require_adc_oneshot_iram,
|
||||
)
|
||||
from esphome.components.nrf52.const import AIN_TO_GPIO, EXTRA_ADC
|
||||
from esphome.components.zephyr import (
|
||||
zephyr_add_overlay,
|
||||
@@ -24,6 +28,7 @@ from esphome.const import (
|
||||
PlatformFramework,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import (
|
||||
ATTENUATION_MODES,
|
||||
@@ -65,6 +70,13 @@ def validate_config(config):
|
||||
return config
|
||||
|
||||
|
||||
def _require_adc_iram(config: ConfigType) -> ConfigType:
|
||||
"""Register ADC oneshot IRAM requirement during config validation."""
|
||||
if CORE.is_esp32:
|
||||
require_adc_oneshot_iram()
|
||||
return config
|
||||
|
||||
|
||||
ADCSensor = adc_ns.class_(
|
||||
"ADCSensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler
|
||||
)
|
||||
@@ -95,6 +107,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s")),
|
||||
validate_config,
|
||||
_require_adc_iram,
|
||||
)
|
||||
|
||||
CONF_ADC_CHANNEL_ID = "adc_channel_id"
|
||||
|
||||
@@ -8,6 +8,9 @@ namespace ade7953_base {
|
||||
|
||||
static const char *const TAG = "ade7953";
|
||||
|
||||
constexpr uint16_t CONFIG_DEFAULT = 0x8004u;
|
||||
constexpr uint16_t CONFIG_LOCK_BIT = 0x8000u;
|
||||
|
||||
static const float ADE_POWER_FACTOR = 154.0f;
|
||||
static const float ADE_WATTSEC_POWER_FACTOR = ADE_POWER_FACTOR * ADE_POWER_FACTOR / 3600;
|
||||
|
||||
@@ -18,7 +21,12 @@ void ADE7953::setup() {
|
||||
|
||||
// The chip might take up to 100ms to initialise
|
||||
this->set_timeout(100, [this]() {
|
||||
// this->ade_write_8(0x0010, 0x04);
|
||||
// Lock communication interface (SPI or I2C)
|
||||
uint16_t config_v = CONFIG_DEFAULT;
|
||||
this->ade_read_16(CONFIG_16, &config_v);
|
||||
config_v &= static_cast<uint16_t>(~CONFIG_LOCK_BIT); // Clear the lock bit
|
||||
this->ade_write_16(CONFIG_16, config_v);
|
||||
// Configure optimum settings according to datasheet
|
||||
this->ade_write_8(0x00FE, 0xAD);
|
||||
this->ade_write_16(0x0120, 0x0030);
|
||||
// Set gains
|
||||
|
||||
@@ -9,31 +9,35 @@
|
||||
namespace esphome {
|
||||
namespace ade7953_base {
|
||||
|
||||
static const uint8_t PGA_V_8 =
|
||||
static constexpr uint8_t PGA_V_8 =
|
||||
0x007; // PGA_V, (R/W) Default: 0x00, Unsigned, Voltage channel gain configuration (Bits[2:0])
|
||||
static const uint8_t PGA_IA_8 =
|
||||
static constexpr uint8_t PGA_IA_8 =
|
||||
0x008; // PGA_IA, (R/W) Default: 0x00, Unsigned, Current Channel A gain configuration (Bits[2:0])
|
||||
static const uint8_t PGA_IB_8 =
|
||||
static constexpr uint8_t PGA_IB_8 =
|
||||
0x009; // PGA_IB, (R/W) Default: 0x00, Unsigned, Current Channel B gain configuration (Bits[2:0])
|
||||
|
||||
static const uint32_t AIGAIN_32 =
|
||||
static constexpr uint16_t CONFIG_16 = 0x102; // CONFIG, (R/W) Default: 0x8004, Unsigned, Configuration register
|
||||
|
||||
static constexpr uint16_t AIGAIN_32 =
|
||||
0x380; // AIGAIN, (R/W) Default: 0x400000, Unsigned,Current channel gain (Current Channel A)(32 bit)
|
||||
static const uint32_t AVGAIN_32 = 0x381; // AVGAIN, (R/W) Default: 0x400000, Unsigned,Voltage channel gain(32 bit)
|
||||
static const uint32_t AWGAIN_32 =
|
||||
static constexpr uint16_t AVGAIN_32 =
|
||||
0x381; // AVGAIN, (R/W) Default: 0x400000, Unsigned,Voltage channel gain(32 bit)
|
||||
static constexpr uint16_t AWGAIN_32 =
|
||||
0x382; // AWGAIN, (R/W) Default: 0x400000, Unsigned,Active power gain (Current Channel A)(32 bit)
|
||||
static const uint32_t AVARGAIN_32 =
|
||||
static constexpr uint16_t AVARGAIN_32 =
|
||||
0x383; // AVARGAIN, (R/W) Default: 0x400000, Unsigned, Reactive power gain (Current Channel A)(32 bit)
|
||||
static const uint32_t AVAGAIN_32 =
|
||||
static constexpr uint16_t AVAGAIN_32 =
|
||||
0x384; // AVAGAIN, (R/W) Default: 0x400000, Unsigned,Apparent power gain (Current Channel A)(32 bit)
|
||||
|
||||
static const uint32_t BIGAIN_32 =
|
||||
static constexpr uint16_t BIGAIN_32 =
|
||||
0x38C; // BIGAIN, (R/W) Default: 0x400000, Unsigned,Current channel gain (Current Channel B)(32 bit)
|
||||
static const uint32_t BVGAIN_32 = 0x38D; // BVGAIN, (R/W) Default: 0x400000, Unsigned,Voltage channel gain(32 bit)
|
||||
static const uint32_t BWGAIN_32 =
|
||||
static constexpr uint16_t BVGAIN_32 =
|
||||
0x38D; // BVGAIN, (R/W) Default: 0x400000, Unsigned,Voltage channel gain(32 bit)
|
||||
static constexpr uint16_t BWGAIN_32 =
|
||||
0x38E; // BWGAIN, (R/W) Default: 0x400000, Unsigned,Active power gain (Current Channel B)(32 bit)
|
||||
static const uint32_t BVARGAIN_32 =
|
||||
static constexpr uint16_t BVARGAIN_32 =
|
||||
0x38F; // BVARGAIN, (R/W) Default: 0x400000, Unsigned, Reactive power gain (Current Channel B)(32 bit)
|
||||
static const uint32_t BVAGAIN_32 =
|
||||
static constexpr uint16_t BVAGAIN_32 =
|
||||
0x390; // BVAGAIN, (R/W) Default: 0x400000, Unsigned,Apparent power gain (Current Channel B)(32 bit)
|
||||
|
||||
class ADE7953 : public PollingComponent, public sensor::Sensor {
|
||||
|
||||
@@ -7,6 +7,9 @@ namespace ade7953_spi {
|
||||
|
||||
static const char *const TAG = "ade7953";
|
||||
|
||||
// Datasheet requires at least 1.2µs after clearing CONFIG LOCK_BIT before raising CS
|
||||
constexpr uint8_t CONFIG_LOCK_SETTLE_US = 2;
|
||||
|
||||
void AdE7953Spi::setup() {
|
||||
this->spi_setup();
|
||||
ade7953_base::ADE7953::setup();
|
||||
@@ -32,6 +35,9 @@ bool AdE7953Spi::ade_write_16(uint16_t reg, uint16_t value) {
|
||||
this->write_byte16(reg);
|
||||
this->transfer_byte(0);
|
||||
this->write_byte16(value);
|
||||
if (reg == ade7953_base::CONFIG_16) {
|
||||
delayMicroseconds(CONFIG_LOCK_SETTLE_US);
|
||||
}
|
||||
this->disable();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace esphome {
|
||||
namespace ade7953_spi {
|
||||
|
||||
class AdE7953Spi : public ade7953_base::ADE7953,
|
||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH, spi::CLOCK_PHASE_LEADING,
|
||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH, spi::CLOCK_PHASE_TRAILING,
|
||||
spi::DATA_RATE_1MHZ> {
|
||||
public:
|
||||
void setup() override;
|
||||
|
||||
@@ -97,7 +97,7 @@ AGS10_NEW_I2C_ADDRESS_SCHEMA = cv.maybe_simple_value(
|
||||
async def ags10newi2caddress_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
address = await cg.templatable(config[CONF_ADDRESS], args, int)
|
||||
address = await cg.templatable(config[CONF_ADDRESS], args, cg.uint8)
|
||||
cg.add(var.set_new_address(address))
|
||||
return var
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ async def aic3204_set_volume_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
template_ = await cg.templatable(config.get(CONF_MODE), args, int)
|
||||
template_ = await cg.templatable(config.get(CONF_MODE), args, cg.uint8)
|
||||
cg.add(var.set_auto_mute_mode(template_))
|
||||
|
||||
return var
|
||||
|
||||
@@ -13,7 +13,11 @@ from esphome.const import (
|
||||
CONF_WEB_SERVER,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
|
||||
from esphome.core.entity_helpers import (
|
||||
entity_duplicate_validator,
|
||||
queue_entity_register,
|
||||
setup_entity,
|
||||
)
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
|
||||
CODEOWNERS = ["@grahambrown11", "@hwstar"]
|
||||
@@ -181,7 +185,7 @@ async def setup_alarm_control_panel_core_(var, config):
|
||||
async def register_alarm_control_panel(var, config):
|
||||
if not CORE.has_id(config[CONF_ID]):
|
||||
var = cg.Pvariable(config[CONF_ID], var)
|
||||
cg.add(cg.App.register_alarm_control_panel(var))
|
||||
queue_entity_register("alarm_control_panel", config)
|
||||
CORE.register_platform_component("alarm_control_panel", var)
|
||||
await setup_alarm_control_panel_core_(var, config)
|
||||
|
||||
|
||||
@@ -40,10 +40,10 @@ async def to_code(config):
|
||||
cg.add(var.set_sensor(sens))
|
||||
|
||||
if isinstance(config[CONF_THRESHOLD], dict):
|
||||
lower = await cg.templatable(config[CONF_THRESHOLD][CONF_LOWER], [], float)
|
||||
upper = await cg.templatable(config[CONF_THRESHOLD][CONF_UPPER], [], float)
|
||||
lower = await cg.templatable(config[CONF_THRESHOLD][CONF_LOWER], [], cg.float_)
|
||||
upper = await cg.templatable(config[CONF_THRESHOLD][CONF_UPPER], [], cg.float_)
|
||||
else:
|
||||
lower = await cg.templatable(config[CONF_THRESHOLD], [], float)
|
||||
lower = await cg.templatable(config[CONF_THRESHOLD], [], cg.float_)
|
||||
upper = lower
|
||||
cg.add(var.set_upper_threshold(upper))
|
||||
cg.add(var.set_lower_threshold(lower))
|
||||
|
||||
@@ -62,7 +62,12 @@ void Animation::set_frame(int frame) {
|
||||
}
|
||||
|
||||
void Animation::update_data_start_() {
|
||||
const uint32_t image_size = this->get_width_stride() * this->height_;
|
||||
uint32_t image_size = this->get_width_stride() * this->height_;
|
||||
// RGB565 with an alpha channel stores the alpha plane immediately after the RGB
|
||||
// plane within each frame, so the per-frame stride includes the alpha bytes.
|
||||
if (this->type_ == image::IMAGE_TYPE_RGB565 && this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
|
||||
image_size += static_cast<uint32_t>(this->width_) * this->height_;
|
||||
}
|
||||
this->data_start_ = this->animation_data_start_ + image_size * this->current_frame_;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#include "esphome/core/alloc_helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace anova {
|
||||
|
||||
@@ -105,14 +107,14 @@ void AnovaCodec::decode(const uint8_t *data, uint16_t length) {
|
||||
}
|
||||
case READ_TARGET_TEMPERATURE:
|
||||
case SET_TARGET_TEMPERATURE: {
|
||||
this->target_temp_ = parse_number<float>(str_until(buf, '\r')).value_or(0.0f);
|
||||
this->target_temp_ = parse_number<float>(str_until(buf, '\r')).value_or(0.0f); // NOLINT
|
||||
if (this->fahrenheit_)
|
||||
this->target_temp_ = ftoc(this->target_temp_);
|
||||
this->has_target_temp_ = true;
|
||||
break;
|
||||
}
|
||||
case READ_CURRENT_TEMPERATURE: {
|
||||
this->current_temp_ = parse_number<float>(str_until(buf, '\r')).value_or(0.0f);
|
||||
this->current_temp_ = parse_number<float>(str_until(buf, '\r')).value_or(0.0f); // NOLINT
|
||||
if (this->fahrenheit_)
|
||||
this->current_temp_ = ftoc(this->current_temp_);
|
||||
this->has_current_temp_ = true;
|
||||
|
||||
@@ -291,12 +291,12 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.SplitDefault(
|
||||
CONF_MAX_CONNECTIONS,
|
||||
esp8266=4, # ~40KB free RAM, each connection uses ~500-1000 bytes
|
||||
esp32=8, # 520KB RAM available
|
||||
esp32=5, # 520KB RAM available
|
||||
rp2040=4, # 264KB RAM but LWIP constraints
|
||||
bk72xx=8, # Moderate RAM
|
||||
rtl87xx=8, # Moderate RAM
|
||||
bk72xx=5, # Moderate RAM
|
||||
rtl87xx=5, # Moderate RAM
|
||||
host=8, # Abundant resources
|
||||
ln882x=8, # Moderate RAM
|
||||
ln882x=5, # Moderate RAM
|
||||
): cv.int_range(min=1, max=20),
|
||||
# Maximum queued send buffers per connection before dropping connection
|
||||
# Each buffer uses ~8-12 bytes overhead plus actual message size
|
||||
@@ -336,8 +336,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add(var.set_batch_delay(config[CONF_BATCH_DELAY]))
|
||||
if CONF_LISTEN_BACKLOG in config:
|
||||
cg.add(var.set_listen_backlog(config[CONF_LISTEN_BACKLOG]))
|
||||
if CONF_MAX_CONNECTIONS in config:
|
||||
cg.add(var.set_max_connections(config[CONF_MAX_CONNECTIONS]))
|
||||
cg.add_define("MAX_API_CONNECTIONS", config[CONF_MAX_CONNECTIONS])
|
||||
cg.add_define("API_MAX_SEND_QUEUE", config[CONF_MAX_SEND_QUEUE])
|
||||
|
||||
# Set USE_API_USER_DEFINED_ACTIONS if any services are enabled
|
||||
|
||||
@@ -671,6 +671,7 @@ message SensorStateResponse {
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_SENSOR";
|
||||
option (no_delay) = true;
|
||||
option (speed_optimized) = true;
|
||||
|
||||
fixed32 key = 1 [(force) = true];
|
||||
float state = 2;
|
||||
@@ -777,9 +778,10 @@ message SubscribeLogsResponse {
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (log) = false;
|
||||
option (no_delay) = false;
|
||||
option (speed_optimized) = true;
|
||||
|
||||
LogLevel level = 1;
|
||||
bytes message = 3;
|
||||
LogLevel level = 1 [(force) = true];
|
||||
bytes message = 3 [(force) = true];
|
||||
}
|
||||
|
||||
// ==================== NOISE ENCRYPTION ====================
|
||||
@@ -1023,6 +1025,13 @@ message CameraImageRequest {
|
||||
bool stream = 2;
|
||||
}
|
||||
|
||||
// ==================== TEMPERATURE UNIT ====================
|
||||
enum TemperatureUnit {
|
||||
TEMPERATURE_UNIT_CELSIUS = 0;
|
||||
TEMPERATURE_UNIT_FAHRENHEIT = 1;
|
||||
TEMPERATURE_UNIT_KELVIN = 2;
|
||||
}
|
||||
|
||||
// ==================== CLIMATE ====================
|
||||
enum ClimateMode {
|
||||
CLIMATE_MODE_OFF = 0;
|
||||
@@ -1108,6 +1117,7 @@ message ListEntitiesClimateResponse {
|
||||
float visual_max_humidity = 25;
|
||||
uint32 device_id = 26 [(field_ifdef) = "USE_DEVICES"];
|
||||
uint32 feature_flags = 27;
|
||||
TemperatureUnit temperature_unit = 28;
|
||||
}
|
||||
message ClimateStateResponse {
|
||||
option (id) = 47;
|
||||
@@ -1201,6 +1211,7 @@ message ListEntitiesWaterHeaterResponse {
|
||||
repeated WaterHeaterMode supported_modes = 11 [(container_pointer_no_template) = "water_heater::WaterHeaterModeMask"];
|
||||
// Bitmask of WaterHeaterFeature flags
|
||||
uint32 supported_features = 12;
|
||||
TemperatureUnit temperature_unit = 13;
|
||||
}
|
||||
|
||||
message WaterHeaterStateResponse {
|
||||
@@ -1408,6 +1419,8 @@ enum LockState {
|
||||
LOCK_STATE_JAMMED = 3;
|
||||
LOCK_STATE_LOCKING = 4;
|
||||
LOCK_STATE_UNLOCKING = 5;
|
||||
LOCK_STATE_OPENING = 6;
|
||||
LOCK_STATE_OPEN = 7;
|
||||
}
|
||||
enum LockCommand {
|
||||
LOCK_UNLOCK = 0;
|
||||
@@ -1625,7 +1638,8 @@ message BluetoothLEAdvertisementResponse {
|
||||
}
|
||||
|
||||
message BluetoothLERawAdvertisement {
|
||||
uint64 address = 1 [(force) = true];
|
||||
option (inline_encode) = true;
|
||||
uint64 address = 1 [(force) = true, (mac_address) = true];
|
||||
sint32 rssi = 2 [(force) = true];
|
||||
uint32 address_type = 3 [(max_value) = 4];
|
||||
|
||||
@@ -1637,6 +1651,7 @@ message BluetoothLERawAdvertisementsResponse {
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_BLUETOOTH_PROXY";
|
||||
option (no_delay) = true;
|
||||
option (speed_optimized) = true;
|
||||
|
||||
repeated BluetoothLERawAdvertisement advertisements = 1 [(fixed_array_with_length_define) = "BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE"];
|
||||
}
|
||||
@@ -2540,27 +2555,50 @@ message ListEntitiesInfraredResponse {
|
||||
message InfraredRFTransmitRawTimingsRequest {
|
||||
option (id) = 136;
|
||||
option (source) = SOURCE_CLIENT;
|
||||
option (ifdef) = "USE_IR_RF";
|
||||
option (ifdef) = "USE_IR_RF || USE_RADIO_FREQUENCY";
|
||||
|
||||
uint32 device_id = 1 [(field_ifdef) = "USE_DEVICES"];
|
||||
fixed32 key = 2 [(force) = true]; // Key identifying the transmitter instance
|
||||
uint32 carrier_frequency = 3; // Carrier frequency in Hz
|
||||
uint32 repeat_count = 4; // Number of times to transmit (1 = once, 2 = twice, etc.)
|
||||
fixed32 key = 2 [(force) = true]; // Key identifying the transmitter instance
|
||||
uint32 carrier_frequency = 3; // Carrier frequency in Hz
|
||||
uint32 repeat_count = 4; // Number of times to transmit (1 = once, 2 = twice, etc.)
|
||||
repeated sint32 timings = 5 [packed = true, (packed_buffer) = true]; // Raw timings in microseconds (zigzag-encoded): positive = mark (LED/TX on), negative = space (LED/TX off)
|
||||
uint32 modulation = 6; // RadioFrequencyModulation enum value (0 = OOK; ignored for IR entities)
|
||||
}
|
||||
|
||||
// Event message for received infrared/RF data
|
||||
message InfraredRFReceiveEvent {
|
||||
option (id) = 137;
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_IR_RF";
|
||||
option (ifdef) = "USE_IR_RF || USE_RADIO_FREQUENCY";
|
||||
option (no_delay) = true;
|
||||
|
||||
uint32 device_id = 1 [(field_ifdef) = "USE_DEVICES"];
|
||||
fixed32 key = 2 [(force) = true]; // Key identifying the receiver instance
|
||||
fixed32 key = 2 [(force) = true]; // Key identifying the receiver instance
|
||||
repeated sint32 timings = 3 [packed = true, (container_pointer_no_template) = "std::vector<int32_t>"]; // Raw timings in microseconds (zigzag-encoded): alternating mark/space periods
|
||||
}
|
||||
|
||||
// ==================== RADIO FREQUENCY ====================
|
||||
|
||||
// Lists available radio frequency entity instances
|
||||
message ListEntitiesRadioFrequencyResponse {
|
||||
option (id) = 148;
|
||||
option (base_class) = "InfoResponseProtoMessage";
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_RADIO_FREQUENCY";
|
||||
|
||||
string object_id = 1 [(max_data_length) = 120, (force) = true];
|
||||
fixed32 key = 2 [(force) = true];
|
||||
string name = 3 [(max_data_length) = 120, (force) = true];
|
||||
string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
|
||||
bool disabled_by_default = 5;
|
||||
EntityCategory entity_category = 6;
|
||||
uint32 device_id = 7 [(field_ifdef) = "USE_DEVICES"];
|
||||
uint32 capabilities = 8; // Bitmask of RadioFrequencyCapabilityFlags: bit 0 = transmitter, bit 1 = receiver
|
||||
uint32 frequency_min = 9; // Minimum tunable frequency in Hz; if min == max (non-zero): fixed frequency; 0 = unspecified
|
||||
uint32 frequency_max = 10; // Maximum tunable frequency in Hz; 0 = unspecified
|
||||
uint32 supported_modulations = 11; // Bitmask of supported RadioFrequencyModulation values (bit N = modulation N supported)
|
||||
}
|
||||
|
||||
// ==================== SERIAL PROXY ====================
|
||||
|
||||
enum SerialProxyParity {
|
||||
|
||||
@@ -49,14 +49,17 @@
|
||||
#ifdef USE_INFRARED
|
||||
#include "esphome/components/infrared/infrared.h"
|
||||
#endif
|
||||
#ifdef USE_RADIO_FREQUENCY
|
||||
#include "esphome/components/radio_frequency/radio_frequency.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
// Read a maximum of 5 messages per loop iteration to prevent starving other components.
|
||||
// Maximum messages to read per loop iteration to prevent starving other components.
|
||||
// This is a balance between API responsiveness and allowing other components to run.
|
||||
// Since each message could contain multiple protobuf messages when using packet batching,
|
||||
// this limits the number of messages processed, not the number of TCP packets.
|
||||
static constexpr uint8_t MAX_MESSAGES_PER_LOOP = 5;
|
||||
static constexpr uint8_t MAX_MESSAGES_PER_LOOP = 10;
|
||||
static constexpr uint8_t MAX_PING_RETRIES = 60;
|
||||
static constexpr uint16_t PING_RETRY_INTERVAL = 1000;
|
||||
static constexpr uint32_t KEEPALIVE_DISCONNECT_TIMEOUT = (KEEPALIVE_TIMEOUT_MS * 5) / 2;
|
||||
@@ -100,6 +103,12 @@ static const int CAMERA_STOP_STREAM = 5000;
|
||||
entity_type *entity_var = App.get_##getter_name##_by_key(msg.key, msg.device_id); \
|
||||
if ((entity_var) == nullptr) \
|
||||
return;
|
||||
|
||||
// Helper macro for multi-entity dispatch: looks up an entity by key and device_id without early return or make_call().
|
||||
// Use when multiple entity types must be checked in sequence (at most one will match).
|
||||
#define ENTITY_COMMAND_LOOKUP(entity_type, entity_var, getter_name) \
|
||||
entity_type *entity_var = App.get_##getter_name##_by_key(msg.key, msg.device_id)
|
||||
|
||||
#else // No device support, use simpler macros
|
||||
// Helper macro for entity command handlers - gets entity by key, returns if not found, and creates call
|
||||
// object
|
||||
@@ -115,6 +124,12 @@ static const int CAMERA_STOP_STREAM = 5000;
|
||||
entity_type *entity_var = App.get_##getter_name##_by_key(msg.key); \
|
||||
if ((entity_var) == nullptr) \
|
||||
return;
|
||||
|
||||
// Helper macro for multi-entity dispatch: looks up an entity by key without early return or make_call().
|
||||
// Use when multiple entity types must be checked in sequence (at most one will match).
|
||||
#define ENTITY_COMMAND_LOOKUP(entity_type, entity_var, getter_name) \
|
||||
entity_type *entity_var = App.get_##getter_name##_by_key(msg.key)
|
||||
|
||||
#endif // USE_DEVICES
|
||||
|
||||
APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent) : parent_(parent) {
|
||||
@@ -220,10 +235,17 @@ void APIConnection::loop() {
|
||||
}
|
||||
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
// Check if socket has data ready before attempting to read
|
||||
if (this->helper_->is_socket_ready()) {
|
||||
// Check if socket has data ready before attempting to read.
|
||||
// Also try reading if we hit the message limit last time — LWIP's rcvevent
|
||||
// (used by is_socket_ready) tracks pbuf dequeues, not bytes. When multiple
|
||||
// messages share a TCP segment, the last message's data stays in LWIP's
|
||||
// lastdata cache after rcvevent hits 0, making is_socket_ready() return false
|
||||
// even though data remains.
|
||||
if (this->helper_->is_socket_ready() || this->flags_.may_have_remaining_data) {
|
||||
this->flags_.may_have_remaining_data = false;
|
||||
// Read up to MAX_MESSAGES_PER_LOOP messages per loop to improve throughput
|
||||
for (uint8_t message_count = 0; message_count < MAX_MESSAGES_PER_LOOP; message_count++) {
|
||||
uint8_t message_count = 0;
|
||||
for (; message_count < MAX_MESSAGES_PER_LOOP; message_count++) {
|
||||
ReadPacketBuffer buffer;
|
||||
err = this->helper_->read_packet(&buffer);
|
||||
if (err == APIError::WOULD_BLOCK) {
|
||||
@@ -245,6 +267,11 @@ void APIConnection::loop() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// If we hit the limit, there may be more data remaining in LWIP's
|
||||
// lastdata cache that rcvevent doesn't account for.
|
||||
if (message_count == MAX_MESSAGES_PER_LOOP) {
|
||||
this->flags_.may_have_remaining_data = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Process deferred batch if scheduled and timer has expired
|
||||
@@ -315,6 +342,8 @@ void APIConnection::process_active_iterator_() {
|
||||
this->destroy_active_iterator_();
|
||||
if (this->flags_.state_subscription) {
|
||||
this->begin_iterator_(ActiveIterator::INITIAL_STATE);
|
||||
} else {
|
||||
this->finalize_iterator_sync_();
|
||||
}
|
||||
} else {
|
||||
this->process_iterator_batch_(this->iterator_storage_.list_entities);
|
||||
@@ -322,21 +351,27 @@ void APIConnection::process_active_iterator_() {
|
||||
} else { // INITIAL_STATE
|
||||
if (this->iterator_storage_.initial_state.completed()) {
|
||||
this->destroy_active_iterator_();
|
||||
// Process any remaining batched messages immediately
|
||||
if (!this->deferred_batch_.empty()) {
|
||||
this->process_batch_();
|
||||
}
|
||||
// Now that everything is sent, enable immediate sending for future state changes
|
||||
this->flags_.should_try_send_immediately = true;
|
||||
// Release excess memory from buffers that grew during initial sync
|
||||
this->deferred_batch_.release_buffer();
|
||||
this->helper_->release_buffers();
|
||||
this->finalize_iterator_sync_();
|
||||
} else {
|
||||
this->process_iterator_batch_(this->iterator_storage_.initial_state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void APIConnection::finalize_iterator_sync_() {
|
||||
// Flush any remaining batched messages immediately so clients
|
||||
// receive completion responses (e.g. ListEntitiesDoneResponse)
|
||||
// without waiting for the batch timer.
|
||||
if (!this->deferred_batch_.empty()) {
|
||||
this->process_batch_();
|
||||
}
|
||||
// Enable immediate sending for future state changes
|
||||
this->flags_.should_try_send_immediately = true;
|
||||
// Release excess memory from buffers that grew during initial sync
|
||||
this->deferred_batch_.release_buffer();
|
||||
this->helper_->release_buffers();
|
||||
}
|
||||
|
||||
void APIConnection::process_iterator_batch_(ComponentIterator &iterator) {
|
||||
size_t initial_size = this->deferred_batch_.size();
|
||||
size_t max_batch = this->get_max_batch_size_();
|
||||
@@ -406,7 +441,7 @@ uint16_t APIConnection::fill_and_encode_entity_info(EntityBase *entity, InfoResp
|
||||
#ifdef USE_DEVICES
|
||||
msg.device_id = entity->get_device_id();
|
||||
#endif
|
||||
return encode_to_buffer(size_fn(&msg), encode_fn, &msg, conn, remaining_size);
|
||||
return encode_to_buffer_slow(size_fn(&msg), encode_fn, &msg, conn, remaining_size);
|
||||
}
|
||||
|
||||
uint16_t APIConnection::fill_and_encode_entity_info_with_device_class(EntityBase *entity, InfoResponseProtoMessage &msg,
|
||||
@@ -1451,19 +1486,36 @@ uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *c
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_IR_RF
|
||||
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
|
||||
void APIConnection::on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg) {
|
||||
// TODO: When RF is implemented, add a field to the message to distinguish IR vs RF
|
||||
// and dispatch to the appropriate entity type based on that field.
|
||||
// Dispatch by key: infrared entities are checked first, then radio frequency entities.
|
||||
// The key is unique across all entity instances on a device, so at most one lookup will succeed.
|
||||
#ifdef USE_INFRARED
|
||||
ENTITY_COMMAND_MAKE_CALL(infrared::Infrared, infrared, infrared)
|
||||
call.set_carrier_frequency(msg.carrier_frequency);
|
||||
call.set_raw_timings_packed(msg.timings_data_, msg.timings_length_, msg.timings_count_);
|
||||
call.set_repeat_count(msg.repeat_count);
|
||||
call.perform();
|
||||
ENTITY_COMMAND_LOOKUP(infrared::Infrared, infrared, infrared);
|
||||
if (infrared != nullptr) {
|
||||
auto call = infrared->make_call();
|
||||
call.set_carrier_frequency(msg.carrier_frequency);
|
||||
call.set_raw_timings_packed(msg.timings_data_, msg.timings_length_, msg.timings_count_);
|
||||
call.set_repeat_count(msg.repeat_count);
|
||||
call.perform();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_RADIO_FREQUENCY
|
||||
ENTITY_COMMAND_LOOKUP(radio_frequency::RadioFrequency, radio_frequency, radio_frequency);
|
||||
if (radio_frequency != nullptr) {
|
||||
auto call = radio_frequency->make_call();
|
||||
call.set_frequency(msg.carrier_frequency);
|
||||
call.set_modulation(static_cast<radio_frequency::RadioFrequencyModulation>(msg.modulation));
|
||||
call.set_repeat_count(msg.repeat_count);
|
||||
call.set_raw_timings_packed(msg.timings_data_, msg.timings_length_, msg.timings_count_);
|
||||
call.perform();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
|
||||
void APIConnection::send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg) { this->send_message(msg); }
|
||||
#endif
|
||||
|
||||
@@ -1560,6 +1612,19 @@ uint16_t APIConnection::try_send_infrared_info(EntityBase *entity, APIConnection
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_RADIO_FREQUENCY
|
||||
uint16_t APIConnection::try_send_radio_frequency_info(EntityBase *entity, APIConnection *conn,
|
||||
uint32_t remaining_size) {
|
||||
auto *rf = static_cast<radio_frequency::RadioFrequency *>(entity);
|
||||
ListEntitiesRadioFrequencyResponse msg;
|
||||
msg.capabilities = rf->get_capability_flags();
|
||||
msg.frequency_min = rf->get_traits().get_frequency_min_hz();
|
||||
msg.frequency_max = rf->get_traits().get_frequency_max_hz();
|
||||
msg.supported_modulations = rf->get_traits().get_supported_modulations();
|
||||
return fill_and_encode_entity_info(rf, msg, conn, remaining_size);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_UPDATE
|
||||
bool APIConnection::send_update_state(update::UpdateEntity *update) {
|
||||
return this->send_message_smart_(update, UpdateStateResponse::MESSAGE_TYPE, UpdateStateResponse::ESTIMATED_SIZE);
|
||||
@@ -2005,48 +2070,12 @@ bool APIConnection::send_message_(uint32_t payload_size, uint8_t message_type, M
|
||||
encode_fn(msg, buffer PROTO_ENCODE_DEBUG_INIT(&shared_buf));
|
||||
return this->send_buffer(ProtoWriteBuffer{&shared_buf}, message_type);
|
||||
}
|
||||
// Encodes a message to the buffer and returns the total number of bytes used,
|
||||
// including header and footer overhead. Returns 0 if the message doesn't fit.
|
||||
uint16_t APIConnection::encode_to_buffer(uint32_t calculated_size, MessageEncodeFn encode_fn, const void *msg,
|
||||
APIConnection *conn, uint32_t remaining_size) {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
if (conn->flags_.log_only_mode) {
|
||||
auto *proto_msg = static_cast<const ProtoMessage *>(msg);
|
||||
DumpBuffer dump_buf;
|
||||
conn->log_send_message_(proto_msg->message_name(), proto_msg->dump_to(dump_buf));
|
||||
return 1;
|
||||
}
|
||||
#endif
|
||||
// Cache frame sizes to avoid repeated virtual calls
|
||||
const uint8_t header_padding = conn->helper_->frame_header_padding();
|
||||
const uint8_t footer_size = conn->helper_->frame_footer_size();
|
||||
// encode_to_buffer is defined inline in api_connection.h (ESPHOME_ALWAYS_INLINE)
|
||||
|
||||
// Calculate total size with padding for buffer allocation
|
||||
size_t total_calculated_size = calculated_size + header_padding + footer_size;
|
||||
|
||||
// Check if it fits
|
||||
if (total_calculated_size > remaining_size)
|
||||
return 0; // Doesn't fit
|
||||
|
||||
auto &shared_buf = conn->parent_->get_shared_buffer_ref();
|
||||
|
||||
size_t to_add;
|
||||
if (conn->flags_.batch_first_message) {
|
||||
// First message - buffer already prepared by caller, just clear flag
|
||||
conn->flags_.batch_first_message = false;
|
||||
to_add = calculated_size;
|
||||
} else {
|
||||
// Batch message second or later
|
||||
// Reserve for full message, resize to include footer gap + header padding + payload
|
||||
to_add = total_calculated_size;
|
||||
}
|
||||
|
||||
shared_buf.resize(shared_buf.size() + to_add);
|
||||
ProtoWriteBuffer buffer{&shared_buf, shared_buf.size() - calculated_size};
|
||||
encode_fn(msg, buffer PROTO_ENCODE_DEBUG_INIT(&shared_buf));
|
||||
|
||||
// Return total size (header + payload + footer)
|
||||
return static_cast<uint16_t>(total_calculated_size);
|
||||
// Noinline version for cold paths — single shared copy
|
||||
uint16_t APIConnection::encode_to_buffer_slow(uint32_t calculated_size, MessageEncodeFn encode_fn, const void *msg,
|
||||
APIConnection *conn, uint32_t remaining_size) {
|
||||
return encode_to_buffer(calculated_size, encode_fn, msg, conn, remaining_size);
|
||||
}
|
||||
bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
|
||||
const bool is_log_message = (message_type == SubscribeLogsResponse::MESSAGE_TYPE);
|
||||
@@ -2114,6 +2143,13 @@ void APIConnection::process_batch_() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure TCP_NODELAY is on before draining overflow and writing batch data.
|
||||
// Log messages enable Nagle (NODELAY off) to coalesce small packets.
|
||||
// If Nagle is still on when we try to drain, LWIP holds data in the
|
||||
// Nagle buffer, the TCP send buffer stays full, and the overflow
|
||||
// buffer can never drain — blocking the batch write indefinitely.
|
||||
this->helper_->set_nodelay_for_message(false);
|
||||
|
||||
// Try to clear buffer first
|
||||
if (!this->try_to_clear_buffer(true)) {
|
||||
// Can't write now, we'll try again later
|
||||
@@ -2173,17 +2209,15 @@ void APIConnection::process_batch_multi_(APIBuffer &shared_buf, size_t num_items
|
||||
"MessageInfo must remain trivially destructible with this placement-new approach");
|
||||
|
||||
const size_t messages_to_process = std::min(num_items, MAX_MESSAGES_PER_BATCH);
|
||||
const uint8_t frame_overhead = header_padding + footer_size;
|
||||
|
||||
// Stack-allocated array for message info
|
||||
alignas(MessageInfo) char message_info_storage[MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo)];
|
||||
MessageInfo *message_info = reinterpret_cast<MessageInfo *>(message_info_storage);
|
||||
size_t items_processed = 0;
|
||||
uint16_t remaining_size = std::numeric_limits<uint16_t>::max();
|
||||
// Track where each message's header padding begins in the buffer
|
||||
// For plaintext: this is where the 6-byte header padding starts
|
||||
// For noise: this is where the 7-byte header padding starts
|
||||
// The actual message data follows after the header padding
|
||||
// Track where each message's header begins in the buffer
|
||||
// First message: offset 0 (max padding, may have unused leading bytes)
|
||||
// Subsequent messages: offset points to exact header start (no gaps)
|
||||
uint32_t current_offset = 0;
|
||||
|
||||
// Process items and encode directly to buffer (up to our limit)
|
||||
@@ -2199,13 +2233,14 @@ void APIConnection::process_batch_multi_(APIBuffer &shared_buf, size_t num_items
|
||||
}
|
||||
|
||||
// Message was encoded successfully
|
||||
// payload_size is header_padding + actual payload size + footer_size
|
||||
uint16_t proto_payload_size = payload_size - frame_overhead;
|
||||
// payload_size = header_size + proto_payload_size + footer_size
|
||||
uint16_t proto_payload_size = payload_size - this->batch_header_size_ - footer_size;
|
||||
// Use placement new to construct MessageInfo in pre-allocated stack array
|
||||
// This avoids default-constructing all MAX_MESSAGES_PER_BATCH elements
|
||||
// Explicit destruction is not needed because MessageInfo is trivially destructible,
|
||||
// as ensured by the static_assert in its definition.
|
||||
new (&message_info[items_processed++]) MessageInfo(item.message_type, current_offset, proto_payload_size);
|
||||
new (&message_info[items_processed++])
|
||||
MessageInfo(item.message_type, current_offset, proto_payload_size, this->batch_header_size_);
|
||||
// After first message, set remaining size to MAX_BATCH_PACKET_SIZE to avoid fragmentation
|
||||
if (items_processed == 1) {
|
||||
remaining_size = MAX_BATCH_PACKET_SIZE;
|
||||
@@ -2255,6 +2290,7 @@ void APIConnection::process_batch_multi_(APIBuffer &shared_buf, size_t num_items
|
||||
uint16_t APIConnection::dispatch_message_(const DeferredBatch::BatchItem &item, uint32_t remaining_size,
|
||||
bool batch_first) {
|
||||
this->flags_.batch_first_message = batch_first;
|
||||
this->batch_message_type_ = item.message_type;
|
||||
#ifdef USE_EVENT
|
||||
// Events need aux_data_index to look up event type from entity
|
||||
if (item.message_type == EventResponse::MESSAGE_TYPE) {
|
||||
@@ -2350,6 +2386,9 @@ uint16_t APIConnection::dispatch_message_(const DeferredBatch::BatchItem &item,
|
||||
#ifdef USE_INFRARED
|
||||
CASE_INFO_ONLY(infrared, ListEntitiesInfraredResponse)
|
||||
#endif
|
||||
#ifdef USE_RADIO_FREQUENCY
|
||||
CASE_INFO_ONLY(radio_frequency, ListEntitiesRadioFrequencyResponse)
|
||||
#endif
|
||||
#ifdef USE_EVENT
|
||||
CASE_INFO_ONLY(event, ListEntitiesEventResponse)
|
||||
#endif
|
||||
|
||||
@@ -223,7 +223,7 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
void on_water_heater_command_request(const WaterHeaterCommandRequest &msg);
|
||||
#endif
|
||||
|
||||
#ifdef USE_IR_RF
|
||||
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
|
||||
void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg);
|
||||
void send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg);
|
||||
#endif
|
||||
@@ -276,6 +276,7 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
App.schedule_dump_config();
|
||||
#ifdef USE_ESP32_CRASH_HANDLER
|
||||
esp32::crash_handler_log();
|
||||
esp32::crash_handler_clear();
|
||||
#endif
|
||||
#ifdef USE_RP2040_CRASH_HANDLER
|
||||
rp2040::crash_handler_log();
|
||||
@@ -410,16 +411,59 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
// Non-template buffer management for send_message
|
||||
bool send_message_(uint32_t payload_size, uint8_t message_type, MessageEncodeFn encode_fn, const void *msg);
|
||||
|
||||
// Non-template buffer management for batch encoding
|
||||
static uint16_t encode_to_buffer(uint32_t calculated_size, MessageEncodeFn encode_fn, const void *msg,
|
||||
APIConnection *conn, uint32_t remaining_size);
|
||||
// Core batch encoding logic. Computes header size, checks fit, resizes buffer, encodes.
|
||||
// ALWAYS_INLINE so the compiler can devirtualize encode_fn at hot call sites.
|
||||
static inline uint16_t ESPHOME_ALWAYS_INLINE encode_to_buffer(uint32_t calculated_size, MessageEncodeFn encode_fn,
|
||||
const void *msg, APIConnection *conn,
|
||||
uint32_t remaining_size) {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
if (conn->flags_.log_only_mode) {
|
||||
auto *proto_msg = static_cast<const ProtoMessage *>(msg);
|
||||
DumpBuffer dump_buf;
|
||||
conn->log_send_message_(proto_msg->message_name(), proto_msg->dump_to(dump_buf));
|
||||
return 1;
|
||||
}
|
||||
#endif
|
||||
const uint8_t footer_size = conn->helper_->frame_footer_size();
|
||||
|
||||
// Thin template wrapper — computes size, delegates buffer work to non-template helper
|
||||
// First message uses max padding (already in buffer), subsequent use exact header size
|
||||
size_t to_add;
|
||||
if (conn->flags_.batch_first_message) {
|
||||
conn->flags_.batch_first_message = false;
|
||||
conn->batch_header_size_ = conn->helper_->frame_header_padding();
|
||||
to_add = calculated_size;
|
||||
} else {
|
||||
conn->batch_header_size_ = conn->helper_->frame_header_size(calculated_size, conn->batch_message_type_);
|
||||
to_add = calculated_size + conn->batch_header_size_ + footer_size;
|
||||
}
|
||||
|
||||
// Check if it fits (using actual header size, not max padding)
|
||||
uint16_t total_calculated_size = calculated_size + conn->batch_header_size_ + footer_size;
|
||||
if (total_calculated_size > remaining_size)
|
||||
return 0;
|
||||
|
||||
auto &shared_buf = conn->parent_->get_shared_buffer_ref();
|
||||
shared_buf.resize(shared_buf.size() + to_add);
|
||||
ProtoWriteBuffer buffer{&shared_buf, shared_buf.size() - calculated_size};
|
||||
encode_fn(msg, buffer PROTO_ENCODE_DEBUG_INIT(&shared_buf));
|
||||
|
||||
return total_calculated_size;
|
||||
}
|
||||
|
||||
// Noinline version of encode_to_buffer for cold paths (entity info, zero-payload messages).
|
||||
// All cold callers share this single copy instead of each getting an ALWAYS_INLINE expansion.
|
||||
static uint16_t encode_to_buffer_slow(uint32_t calculated_size, MessageEncodeFn encode_fn, const void *msg,
|
||||
APIConnection *conn, uint32_t remaining_size);
|
||||
|
||||
// Thin template wrapper — uses noinline encode_to_buffer_slow since
|
||||
// encode_message_to_buffer callers are cold paths (zero-payload control messages).
|
||||
// Hot paths (state/info) go through fill_and_encode_entity_state/info instead.
|
||||
// batch_message_type_ is already set by dispatch_message_ before reaching here.
|
||||
template<typename T> static uint16_t encode_message_to_buffer(T &msg, APIConnection *conn, uint32_t remaining_size) {
|
||||
if constexpr (T::ESTIMATED_SIZE == 0) {
|
||||
return encode_to_buffer(0, &encode_msg_noop, &msg, conn, remaining_size);
|
||||
return encode_to_buffer_slow(0, &encode_msg_noop, &msg, conn, remaining_size);
|
||||
} else {
|
||||
return encode_to_buffer(msg.calculate_size(), &proto_encode_msg<T>, &msg, conn, remaining_size);
|
||||
return encode_to_buffer_slow(msg.calculate_size(), &proto_encode_msg<T>, &msg, conn, remaining_size);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -568,6 +612,9 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
#ifdef USE_INFRARED
|
||||
static uint16_t try_send_infrared_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
#endif
|
||||
#ifdef USE_RADIO_FREQUENCY
|
||||
static uint16_t try_send_radio_frequency_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
#endif
|
||||
#ifdef USE_EVENT
|
||||
static uint16_t try_send_event_response(event::Event *event, StringRef event_type, APIConnection *conn,
|
||||
uint32_t remaining_size);
|
||||
@@ -618,6 +665,7 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
// Helper methods for iterator lifecycle management
|
||||
void destroy_active_iterator_();
|
||||
void begin_iterator_(ActiveIterator type);
|
||||
void finalize_iterator_sync_();
|
||||
#ifdef USE_CAMERA
|
||||
std::unique_ptr<camera::CameraImageReader> image_reader_;
|
||||
#endif
|
||||
@@ -726,6 +774,7 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
uint8_t batch_scheduled : 1;
|
||||
uint8_t batch_first_message : 1; // For batch buffer allocation
|
||||
uint8_t should_try_send_immediately : 1; // True after initial states are sent
|
||||
uint8_t may_have_remaining_data : 1; // Read loop hit limit, retry without ready check
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
uint8_t log_only_mode : 1;
|
||||
#endif
|
||||
@@ -734,9 +783,14 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
// 2-byte types immediately after flags_ (no padding between them)
|
||||
uint16_t client_api_version_major_{0};
|
||||
uint16_t client_api_version_minor_{0};
|
||||
// 1-byte type to fill padding
|
||||
// 1-byte types to fill remaining space before next 4-byte boundary
|
||||
ActiveIterator active_iterator_{ActiveIterator::NONE};
|
||||
// Total: 2 (flags) + 2 + 2 + 1 = 7 bytes, then 1 byte padding to next 4-byte boundary
|
||||
uint8_t batch_message_type_{0}; // Current message type during batch encoding
|
||||
// Total: 2 (flags) + 2 + 2 + 1 + 1 = 8 bytes, aligned to 4-byte boundary
|
||||
|
||||
// Actual header size used by encode_to_buffer for the current message.
|
||||
// Read by process_batch_multi_ to pass into MessageInfo.
|
||||
uint8_t batch_header_size_{0};
|
||||
|
||||
uint32_t get_batch_delay_ms_() const { return this->parent_->get_batch_delay(); }
|
||||
// Message will use 8 more bytes than the minimum size, and typical
|
||||
|
||||
@@ -100,10 +100,17 @@ const LogString *api_error_to_logstr(APIError err) {
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
|
||||
#ifdef HELPER_LOG_PACKETS
|
||||
void APIFrameHelper::log_packet_sending_(const void *data, uint16_t len) {
|
||||
LOG_PACKET_SENDING(reinterpret_cast<const uint8_t *>(data), len);
|
||||
}
|
||||
#endif
|
||||
|
||||
APIError APIFrameHelper::drain_overflow_and_handle_errors_() {
|
||||
if (this->overflow_buf_.try_drain(this->socket_.get()) == -1) {
|
||||
int err = errno;
|
||||
if (this->check_socket_write_err_(err) != APIError::WOULD_BLOCK) {
|
||||
if (err != EWOULDBLOCK && err != EAGAIN) {
|
||||
this->state_ = State::FAILED;
|
||||
HELPER_LOG("Socket write failed with errno %d", err);
|
||||
return APIError::SOCKET_WRITE_FAILED;
|
||||
}
|
||||
@@ -111,45 +118,58 @@ APIError APIFrameHelper::drain_overflow_and_handle_errors_() {
|
||||
return APIError::OK;
|
||||
}
|
||||
|
||||
// Write data to socket, overflow to backlog buffer if LWIP TCP send buffer is full.
|
||||
// Returns OK if all data was sent or successfully queued.
|
||||
// Returns SOCKET_WRITE_FAILED on hard error (sets state to FAILED).
|
||||
APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_t total_write_len) {
|
||||
// Single-buffer write path: wraps in iovec and delegates.
|
||||
APIError APIFrameHelper::write_raw_buf_(const void *data, uint16_t len, ssize_t sent) {
|
||||
struct iovec iov = {const_cast<void *>(data), len};
|
||||
APIError err = this->write_raw_iov_(&iov, 1, len, sent);
|
||||
#ifdef HELPER_LOG_PACKETS
|
||||
for (int i = 0; i < iovcnt; i++) {
|
||||
LOG_PACKET_SENDING(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len);
|
||||
}
|
||||
// Log after write/enqueue so re-entrant log sends can't corrupt data before it's sent
|
||||
if (err == APIError::OK)
|
||||
LOG_PACKET_SENDING(reinterpret_cast<const uint8_t *>(data), len);
|
||||
#endif
|
||||
return err;
|
||||
}
|
||||
|
||||
uint16_t skip = 0;
|
||||
|
||||
// Drain any existing backlog first
|
||||
if (!this->overflow_buf_.empty()) [[unlikely]] {
|
||||
APIError err = this->drain_overflow_and_handle_errors_();
|
||||
if (err != APIError::OK)
|
||||
return err;
|
||||
}
|
||||
|
||||
// If backlog is clear, try direct send
|
||||
if (this->overflow_buf_.empty()) [[likely]] {
|
||||
ssize_t sent =
|
||||
(iovcnt == 1) ? this->socket_->write(iov[0].iov_base, iov[0].iov_len) : this->socket_->writev(iov, iovcnt);
|
||||
|
||||
if (sent == -1) [[unlikely]] {
|
||||
// Handles partial writes, errors, and overflow buffering.
|
||||
// Called when the inline fast path couldn't complete the write,
|
||||
// or directly from cold paths (handshake, error handling).
|
||||
APIError APIFrameHelper::write_raw_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len, ssize_t sent) {
|
||||
if (sent <= 0) {
|
||||
if (sent == WRITE_NOT_ATTEMPTED) {
|
||||
// Cold path: no write attempted yet, drain overflow and try
|
||||
if (!this->overflow_buf_.empty()) {
|
||||
APIError err = this->drain_overflow_and_handle_errors_();
|
||||
if (err != APIError::OK)
|
||||
return err;
|
||||
}
|
||||
if (this->overflow_buf_.empty()) {
|
||||
sent = this->write_iov_to_socket_(iov, iovcnt);
|
||||
if (sent == static_cast<ssize_t>(total_write_len))
|
||||
return APIError::OK;
|
||||
// Partial write or -1: fall through to error check / enqueue below
|
||||
} else {
|
||||
// Overflow backlog remains after drain; skip socket write, enqueue everything
|
||||
sent = 0;
|
||||
}
|
||||
}
|
||||
// WRITE_FAILED (-1): fast path or retry write returned -1, check errno
|
||||
if (sent == WRITE_FAILED) {
|
||||
int err = errno;
|
||||
if (this->check_socket_write_err_(err) != APIError::WOULD_BLOCK) {
|
||||
if (err != EWOULDBLOCK && err != EAGAIN) {
|
||||
this->state_ = State::FAILED;
|
||||
HELPER_LOG("Socket write failed with errno %d", err);
|
||||
return APIError::SOCKET_WRITE_FAILED;
|
||||
}
|
||||
} else if (static_cast<uint16_t>(sent) >= total_write_len) [[likely]] {
|
||||
return APIError::OK;
|
||||
} else {
|
||||
skip = static_cast<uint16_t>(sent);
|
||||
sent = 0; // Treat WOULD_BLOCK as zero bytes sent
|
||||
}
|
||||
}
|
||||
|
||||
// Full write completed (possible when called directly, not via write_raw_fast_buf_)
|
||||
if (sent == static_cast<ssize_t>(total_write_len))
|
||||
return APIError::OK;
|
||||
|
||||
// Queue unsent data into overflow buffer
|
||||
if (!this->overflow_buf_.enqueue_iov(iov, iovcnt, total_write_len, skip)) {
|
||||
if (!this->overflow_buf_.enqueue_iov(iov, iovcnt, total_write_len, static_cast<uint16_t>(sent))) {
|
||||
HELPER_LOG("Overflow buffer full, dropping connection");
|
||||
this->state_ = State::FAILED;
|
||||
return APIError::SOCKET_WRITE_FAILED;
|
||||
|
||||
@@ -49,12 +49,17 @@ struct ReadPacketBuffer {
|
||||
};
|
||||
|
||||
// Packed message info structure to minimize memory usage
|
||||
// Note: message_type is uint8_t — all current protobuf message types fit in 8 bits.
|
||||
// The noise wire format encodes types as 16-bit, but the high byte is always 0.
|
||||
// If message types ever exceed 255, this and encrypt_noise_message_ must be updated.
|
||||
struct MessageInfo {
|
||||
uint16_t offset; // Offset in buffer where message starts
|
||||
uint16_t payload_size; // Size of the message payload
|
||||
uint8_t message_type; // Message type (0-255)
|
||||
uint8_t header_size; // Actual header size used (avoids recomputation in write path)
|
||||
|
||||
MessageInfo(uint8_t type, uint16_t off, uint16_t size) : offset(off), payload_size(size), message_type(type) {}
|
||||
MessageInfo(uint8_t type, uint16_t off, uint16_t size, uint8_t hdr)
|
||||
: offset(off), payload_size(size), message_type(type), header_size(hdr) {}
|
||||
};
|
||||
|
||||
enum class APIError : uint16_t {
|
||||
@@ -161,23 +166,39 @@ class APIFrameHelper {
|
||||
this->nodelay_counter_ = 0;
|
||||
}
|
||||
}
|
||||
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
|
||||
// Resize buffer to include footer space if needed (e.g. Noise MAC)
|
||||
if (frame_footer_size_)
|
||||
buffer.get_buffer()->resize(buffer.get_buffer()->size() + frame_footer_size_);
|
||||
MessageInfo msg{type, 0,
|
||||
static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)};
|
||||
return write_protobuf_messages(buffer, std::span<const MessageInfo>(&msg, 1));
|
||||
}
|
||||
// Write multiple protobuf messages in a single operation
|
||||
// messages contains (message_type, offset, length) for each message in the buffer
|
||||
// The buffer contains all messages with appropriate padding before each
|
||||
// Write a single protobuf message - the hot path (87-100% of all writes).
|
||||
// Caller must ensure state is DATA before calling.
|
||||
virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0;
|
||||
// Write multiple protobuf messages in a single batched operation.
|
||||
// Caller must ensure state is DATA and messages is not empty.
|
||||
// messages contains (message_type, offset, length) for each message in the buffer.
|
||||
// The buffer contains all messages with appropriate padding before each.
|
||||
virtual APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) = 0;
|
||||
// Get the frame header padding required by this protocol
|
||||
// Get the maximum frame header padding required by this protocol (worst case)
|
||||
uint8_t frame_header_padding() const { return frame_header_padding_; }
|
||||
// Get the actual frame header size for a specific message.
|
||||
// For noise: always returns frame_header_padding_ (fixed 7-byte header).
|
||||
// For plaintext: computes actual size from varint lengths (3-6 bytes).
|
||||
// Distinguishes protocols via frame_footer_size_ (noise always has a non-zero MAC
|
||||
// footer, plaintext has footer=0). If a protocol with a plaintext footer is ever
|
||||
// added, this should become a virtual method.
|
||||
uint8_t frame_header_size(uint16_t payload_size, uint8_t message_type) const {
|
||||
#if defined(USE_API_NOISE) && defined(USE_API_PLAINTEXT)
|
||||
return this->frame_footer_size_
|
||||
? this->frame_header_padding_
|
||||
: static_cast<uint8_t>(1 + ProtoSize::varint16(payload_size) + ProtoSize::varint8(message_type));
|
||||
#elif defined(USE_API_NOISE)
|
||||
return this->frame_header_padding_;
|
||||
#else // USE_API_PLAINTEXT only
|
||||
return static_cast<uint8_t>(1 + ProtoSize::varint16(payload_size) + ProtoSize::varint8(message_type));
|
||||
#endif
|
||||
}
|
||||
// Get the frame footer size required by this protocol
|
||||
uint8_t frame_footer_size() const { return frame_footer_size_; }
|
||||
// Check if socket has data ready to read
|
||||
// Check if socket has buffered data ready to read.
|
||||
// Contract: callers must read until it would block (EAGAIN/EWOULDBLOCK)
|
||||
// or track that they stopped early and retry without this check.
|
||||
// See Socket::ready() for details.
|
||||
bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); }
|
||||
// Release excess memory from internal buffers after initial sync
|
||||
void release_buffers() {
|
||||
@@ -196,18 +217,41 @@ class APIFrameHelper {
|
||||
// Returns OK for transient errors (WOULD_BLOCK), SOCKET_WRITE_FAILED for hard errors.
|
||||
APIError drain_overflow_and_handle_errors_();
|
||||
|
||||
// Common implementation for writing raw data to socket
|
||||
APIError write_raw_(const struct iovec *iov, int iovcnt, uint16_t total_write_len);
|
||||
// Sentinel values for the sent parameter in write_raw_ methods
|
||||
static constexpr ssize_t WRITE_FAILED = -1; // Fast path: write()/writev() returned -1
|
||||
static constexpr ssize_t WRITE_NOT_ATTEMPTED = -2; // Cold path: no write attempted yet
|
||||
|
||||
// Check if a socket write errno is a hard error (not WOULD_BLOCK/EAGAIN).
|
||||
// Returns WOULD_BLOCK for transient errors, SOCKET_WRITE_FAILED for hard errors.
|
||||
APIError check_socket_write_err_(int err) {
|
||||
if (err == EWOULDBLOCK || err == EAGAIN)
|
||||
return APIError::WOULD_BLOCK;
|
||||
this->state_ = State::FAILED;
|
||||
return APIError::SOCKET_WRITE_FAILED;
|
||||
// Dispatch to write() or writev() based on iovec count
|
||||
inline ssize_t ESPHOME_ALWAYS_INLINE write_iov_to_socket_(const struct iovec *iov, int iovcnt) {
|
||||
return (iovcnt == 1) ? this->socket_->write(iov[0].iov_base, iov[0].iov_len) : this->socket_->writev(iov, iovcnt);
|
||||
}
|
||||
|
||||
// Inlined write methods — used by hot paths (write_protobuf_packet, write_protobuf_messages)
|
||||
// These inline the fast path (overflow empty + full write) and tail-call the out-of-line
|
||||
// slow path only on failure/partial write.
|
||||
inline APIError ESPHOME_ALWAYS_INLINE write_raw_fast_buf_(const void *data, uint16_t len) {
|
||||
if (this->overflow_buf_.empty()) [[likely]] {
|
||||
ssize_t sent = this->socket_->write(data, len);
|
||||
if (sent == static_cast<ssize_t>(len)) [[likely]] {
|
||||
#ifdef HELPER_LOG_PACKETS
|
||||
this->log_packet_sending_(data, len);
|
||||
#endif
|
||||
return APIError::OK;
|
||||
}
|
||||
// sent is -1 (WRITE_FAILED) or partial write count
|
||||
return this->write_raw_buf_(data, len, sent);
|
||||
}
|
||||
return this->write_raw_buf_(data, len, WRITE_NOT_ATTEMPTED);
|
||||
}
|
||||
// Out-of-line write paths: handle partial writes, errors, overflow buffering
|
||||
// sent: WRITE_NOT_ATTEMPTED (cold path), WRITE_FAILED (fast path write returned -1), or bytes sent (partial write)
|
||||
APIError write_raw_buf_(const void *data, uint16_t len, ssize_t sent = WRITE_NOT_ATTEMPTED);
|
||||
APIError write_raw_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len,
|
||||
ssize_t sent = WRITE_NOT_ATTEMPTED);
|
||||
#ifdef HELPER_LOG_PACKETS
|
||||
void log_packet_sending_(const void *data, uint16_t len);
|
||||
#endif
|
||||
|
||||
// Socket ownership (4 bytes on 32-bit, 8 bytes on 64-bit)
|
||||
std::unique_ptr<socket::Socket> socket_;
|
||||
|
||||
|
||||
@@ -47,15 +47,8 @@ static constexpr size_t API_MAX_LOG_BYTES = 168;
|
||||
format_hex_pretty_to(hex_buf_, (buffer).data(), \
|
||||
(buffer).size() < API_MAX_LOG_BYTES ? (buffer).size() : API_MAX_LOG_BYTES)); \
|
||||
} while (0)
|
||||
#define LOG_PACKET_SENDING(data, len) \
|
||||
do { \
|
||||
char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \
|
||||
ESP_LOGVV(TAG, "Sending raw: %s", \
|
||||
format_hex_pretty_to(hex_buf_, data, (len) < API_MAX_LOG_BYTES ? (len) : API_MAX_LOG_BYTES)); \
|
||||
} while (0)
|
||||
#else
|
||||
#define LOG_PACKET_RECEIVED(buffer) ((void) 0)
|
||||
#define LOG_PACKET_SENDING(data, len) ((void) 0)
|
||||
#endif
|
||||
|
||||
/// Convert a noise error code to a readable error
|
||||
@@ -464,65 +457,83 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
||||
buffer->type = type;
|
||||
return APIError::OK;
|
||||
}
|
||||
APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) {
|
||||
APIError aerr = this->check_data_state_();
|
||||
// Encrypt a single noise message in place and return the encrypted frame length.
|
||||
// Returns APIError::OK on success.
|
||||
APIError APINoiseFrameHelper::encrypt_noise_message_(uint8_t *buf_start, uint16_t payload_size, uint8_t message_type,
|
||||
uint16_t &encrypted_len_out) {
|
||||
// Write noise header
|
||||
buf_start[0] = 0x01; // indicator
|
||||
// buf_start[1], buf_start[2] to be set after encryption
|
||||
|
||||
// Write message header (to be encrypted)
|
||||
constexpr uint8_t msg_offset = 3;
|
||||
buf_start[msg_offset] = static_cast<uint8_t>(message_type >> 8); // type high byte
|
||||
buf_start[msg_offset + 1] = static_cast<uint8_t>(message_type); // type low byte
|
||||
buf_start[msg_offset + 2] = static_cast<uint8_t>(payload_size >> 8); // data_len high byte
|
||||
buf_start[msg_offset + 3] = static_cast<uint8_t>(payload_size); // data_len low byte
|
||||
// payload data is already in the buffer starting at offset + 7
|
||||
|
||||
// Encrypt the message in place
|
||||
NoiseBuffer mbuf;
|
||||
noise_buffer_init(mbuf);
|
||||
noise_buffer_set_inout(mbuf, buf_start + msg_offset, 4 + payload_size, 4 + payload_size + this->frame_footer_size_);
|
||||
|
||||
int err = noise_cipherstate_encrypt(this->send_cipher_, &mbuf);
|
||||
APIError aerr =
|
||||
this->handle_noise_error_(err, LOG_STR("noise_cipherstate_encrypt"), APIError::CIPHERSTATE_ENCRYPT_FAILED);
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
|
||||
if (messages.empty()) {
|
||||
return APIError::OK;
|
||||
}
|
||||
// Fill in the encrypted size
|
||||
buf_start[1] = static_cast<uint8_t>(mbuf.size >> 8);
|
||||
buf_start[2] = static_cast<uint8_t>(mbuf.size);
|
||||
|
||||
encrypted_len_out = static_cast<uint16_t>(3 + mbuf.size); // indicator + size + encrypted data
|
||||
return APIError::OK;
|
||||
}
|
||||
|
||||
APIError APINoiseFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
assert(this->state_ == State::DATA);
|
||||
#endif
|
||||
|
||||
// Resize buffer to include footer space for Noise MAC
|
||||
if (this->frame_footer_size_)
|
||||
buffer.get_buffer()->resize(buffer.get_buffer()->size() + this->frame_footer_size_);
|
||||
|
||||
uint16_t payload_size =
|
||||
static_cast<uint16_t>(buffer.get_buffer()->size() - HEADER_PADDING - this->frame_footer_size_);
|
||||
uint8_t *buf_start = buffer.get_buffer()->data();
|
||||
uint16_t encrypted_len;
|
||||
APIError aerr = this->encrypt_noise_message_(buf_start, payload_size, type, encrypted_len);
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
return this->write_raw_fast_buf_(buf_start, encrypted_len);
|
||||
}
|
||||
|
||||
APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) {
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
assert(this->state_ == State::DATA);
|
||||
assert(!messages.empty());
|
||||
#endif
|
||||
|
||||
// Noise messages are already contiguous in the buffer:
|
||||
// HEADER_PADDING (7) exactly matches the fixed header size, and
|
||||
// footer space (16) is consumed by the encryption MAC.
|
||||
uint8_t *buffer_data = buffer.get_buffer()->data();
|
||||
|
||||
// Stack-allocated iovec array - no heap allocation
|
||||
StaticVector<struct iovec, MAX_MESSAGES_PER_BATCH> iovs;
|
||||
uint8_t *write_start = buffer_data + messages[0].offset;
|
||||
uint16_t total_write_len = 0;
|
||||
|
||||
// We need to encrypt each message in place
|
||||
for (const auto &msg : messages) {
|
||||
// The buffer already has padding at offset
|
||||
uint8_t *buf_start = buffer_data + msg.offset;
|
||||
|
||||
// Write noise header
|
||||
buf_start[0] = 0x01; // indicator
|
||||
// buf_start[1], buf_start[2] to be set after encryption
|
||||
|
||||
// Write message header (to be encrypted)
|
||||
constexpr uint8_t msg_offset = 3;
|
||||
buf_start[msg_offset] = static_cast<uint8_t>(msg.message_type >> 8); // type high byte
|
||||
buf_start[msg_offset + 1] = static_cast<uint8_t>(msg.message_type); // type low byte
|
||||
buf_start[msg_offset + 2] = static_cast<uint8_t>(msg.payload_size >> 8); // data_len high byte
|
||||
buf_start[msg_offset + 3] = static_cast<uint8_t>(msg.payload_size); // data_len low byte
|
||||
// payload data is already in the buffer starting at offset + 7
|
||||
|
||||
// Make sure we have space for MAC
|
||||
// The buffer should already have been sized appropriately
|
||||
|
||||
// Encrypt the message in place
|
||||
NoiseBuffer mbuf;
|
||||
noise_buffer_init(mbuf);
|
||||
noise_buffer_set_inout(mbuf, buf_start + msg_offset, 4 + msg.payload_size,
|
||||
4 + msg.payload_size + frame_footer_size_);
|
||||
|
||||
int err = noise_cipherstate_encrypt(send_cipher_, &mbuf);
|
||||
APIError aerr =
|
||||
handle_noise_error_(err, LOG_STR("noise_cipherstate_encrypt"), APIError::CIPHERSTATE_ENCRYPT_FAILED);
|
||||
uint16_t encrypted_len;
|
||||
APIError aerr = this->encrypt_noise_message_(buf_start, msg.payload_size, msg.message_type, encrypted_len);
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
|
||||
// Fill in the encrypted size
|
||||
buf_start[1] = static_cast<uint8_t>(mbuf.size >> 8);
|
||||
buf_start[2] = static_cast<uint8_t>(mbuf.size);
|
||||
|
||||
// Add iovec for this encrypted message
|
||||
size_t msg_len = static_cast<size_t>(3 + mbuf.size); // indicator + size + encrypted data
|
||||
iovs.push_back({buf_start, msg_len});
|
||||
total_write_len += msg_len;
|
||||
total_write_len += encrypted_len;
|
||||
}
|
||||
|
||||
// Send all encrypted messages in one writev call
|
||||
return this->write_raw_(iovs.data(), iovs.size(), total_write_len);
|
||||
return this->write_raw_fast_buf_(write_start, total_write_len);
|
||||
}
|
||||
|
||||
APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) {
|
||||
@@ -531,16 +542,16 @@ APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) {
|
||||
header[1] = (uint8_t) (len >> 8);
|
||||
header[2] = (uint8_t) len;
|
||||
|
||||
if (len == 0) {
|
||||
return this->write_raw_buf_(header, 3);
|
||||
}
|
||||
struct iovec iov[2];
|
||||
iov[0].iov_base = header;
|
||||
iov[0].iov_len = 3;
|
||||
if (len == 0) {
|
||||
return this->write_raw_(iov, 1, 3); // Just header
|
||||
}
|
||||
iov[1].iov_base = const_cast<uint8_t *>(data);
|
||||
iov[1].iov_len = len;
|
||||
|
||||
return this->write_raw_(iov, 2, 3 + len); // Header + data
|
||||
return this->write_raw_iov_(iov, 2, 3 + len);
|
||||
}
|
||||
|
||||
/** Initiate the data structures for the handshake.
|
||||
@@ -606,7 +617,7 @@ APIError APINoiseFrameHelper::check_handshake_finished_() {
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
|
||||
frame_footer_size_ = noise_cipherstate_get_mac_length(send_cipher_);
|
||||
this->frame_footer_size_ = noise_cipherstate_get_mac_length(send_cipher_);
|
||||
|
||||
HELPER_LOG("Handshake complete!");
|
||||
noise_handshakestate_free(handshake_);
|
||||
|
||||
@@ -9,19 +9,22 @@ namespace esphome::api {
|
||||
|
||||
class APINoiseFrameHelper final : public APIFrameHelper {
|
||||
public:
|
||||
// Noise header structure:
|
||||
// Pos 0: indicator (0x01)
|
||||
// Pos 1-2: encrypted payload size (16-bit big-endian)
|
||||
// Pos 3-6: encrypted type (16-bit) + data_len (16-bit)
|
||||
// Pos 7+: actual payload data
|
||||
static constexpr uint8_t HEADER_PADDING = 1 + 2 + 2 + 2; // indicator + size + type + data_len
|
||||
|
||||
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, APINoiseContext &ctx)
|
||||
: APIFrameHelper(std::move(socket)), ctx_(ctx) {
|
||||
// Noise header structure:
|
||||
// Pos 0: indicator (0x01)
|
||||
// Pos 1-2: encrypted payload size (16-bit big-endian)
|
||||
// Pos 3-6: encrypted type (16-bit) + data_len (16-bit)
|
||||
// Pos 7+: actual payload data
|
||||
frame_header_padding_ = 7;
|
||||
frame_header_padding_ = HEADER_PADDING;
|
||||
}
|
||||
~APINoiseFrameHelper() override;
|
||||
APIError init() override;
|
||||
APIError loop() override;
|
||||
APIError read_packet(ReadPacketBuffer *buffer) override;
|
||||
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
|
||||
APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) override;
|
||||
|
||||
protected:
|
||||
@@ -33,6 +36,8 @@ class APINoiseFrameHelper final : public APIFrameHelper {
|
||||
APIError state_action_handshake_write_();
|
||||
APIError try_read_frame_();
|
||||
APIError write_frame_(const uint8_t *data, uint16_t len);
|
||||
APIError encrypt_noise_message_(uint8_t *buf_start, uint16_t payload_size, uint8_t message_type,
|
||||
uint16_t &encrypted_len_out);
|
||||
APIError init_handshake_();
|
||||
APIError check_handshake_finished_();
|
||||
void send_explicit_handshake_reject_(const LogString *reason);
|
||||
|
||||
@@ -39,15 +39,8 @@ static constexpr size_t API_MAX_LOG_BYTES = 168;
|
||||
format_hex_pretty_to(hex_buf_, (buffer).data(), \
|
||||
(buffer).size() < API_MAX_LOG_BYTES ? (buffer).size() : API_MAX_LOG_BYTES)); \
|
||||
} while (0)
|
||||
#define LOG_PACKET_SENDING(data, len) \
|
||||
do { \
|
||||
char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \
|
||||
ESP_LOGVV(TAG, "Sending raw: %s", \
|
||||
format_hex_pretty_to(hex_buf_, data, (len) < API_MAX_LOG_BYTES ? (len) : API_MAX_LOG_BYTES)); \
|
||||
} while (0)
|
||||
#else
|
||||
#define LOG_PACKET_RECEIVED(buffer) ((void) 0)
|
||||
#define LOG_PACKET_SENDING(data, len) ((void) 0)
|
||||
#endif
|
||||
|
||||
/// Initialize the frame helper, returns OK if successful.
|
||||
@@ -205,7 +198,6 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
||||
// Make sure to tell the remote that we don't
|
||||
// understand the indicator byte so it knows
|
||||
// we do not support it.
|
||||
struct iovec iov[1];
|
||||
// The \x00 first byte is the marker for plaintext.
|
||||
//
|
||||
// The remote will know how to handle the indicator byte,
|
||||
@@ -220,14 +212,12 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
||||
"Bad indicator byte";
|
||||
char msg[INDICATOR_MSG_SIZE];
|
||||
memcpy_P(msg, MSG_PROGMEM, INDICATOR_MSG_SIZE);
|
||||
iov[0].iov_base = (void *) msg;
|
||||
this->write_raw_buf_(msg, INDICATOR_MSG_SIZE);
|
||||
#else
|
||||
static const char MSG[] = "\x00"
|
||||
"Bad indicator byte";
|
||||
iov[0].iov_base = (void *) MSG;
|
||||
this->write_raw_buf_(MSG, INDICATOR_MSG_SIZE);
|
||||
#endif
|
||||
iov[0].iov_len = INDICATOR_MSG_SIZE;
|
||||
this->write_raw_(iov, 1, INDICATOR_MSG_SIZE);
|
||||
}
|
||||
return aerr;
|
||||
}
|
||||
@@ -237,73 +227,101 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
||||
buffer->type = this->rx_header_parsed_type_;
|
||||
return APIError::OK;
|
||||
}
|
||||
|
||||
// Encode a 16-bit varint (1-3 bytes) using pre-computed length.
|
||||
ESPHOME_ALWAYS_INLINE static inline void encode_varint_16(uint16_t value, uint8_t varint_len, uint8_t *p) {
|
||||
if (varint_len >= 2) {
|
||||
*p++ = static_cast<uint8_t>(value | 0x80);
|
||||
value >>= 7;
|
||||
if (varint_len == 3) {
|
||||
*p++ = static_cast<uint8_t>(value | 0x80);
|
||||
value >>= 7;
|
||||
}
|
||||
}
|
||||
*p = static_cast<uint8_t>(value);
|
||||
}
|
||||
|
||||
// Encode an 8-bit varint (1-2 bytes) using pre-computed length.
|
||||
ESPHOME_ALWAYS_INLINE static inline void encode_varint_8(uint8_t value, uint8_t varint_len, uint8_t *p) {
|
||||
if (varint_len == 2) {
|
||||
*p++ = static_cast<uint8_t>(value | 0x80);
|
||||
*p = static_cast<uint8_t>(value >> 7);
|
||||
} else {
|
||||
*p = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Write plaintext header into pre-allocated padding before payload.
|
||||
// padding_size: bytes reserved before payload (HEADER_PADDING for first/single msg,
|
||||
// actual header size for contiguous batch messages).
|
||||
// Returns the total header length (indicator + varints).
|
||||
ESPHOME_ALWAYS_INLINE static inline uint8_t write_plaintext_header(uint8_t *buf_start, uint16_t payload_size,
|
||||
uint8_t message_type, uint8_t padding_size) {
|
||||
uint8_t size_varint_len = ProtoSize::varint16(payload_size);
|
||||
uint8_t type_varint_len = ProtoSize::varint8(message_type);
|
||||
uint8_t total_header_len = 1 + size_varint_len + type_varint_len;
|
||||
|
||||
// The header is right-justified within the padding so it sits immediately before payload.
|
||||
//
|
||||
// Single/first message (padding_size = HEADER_PADDING = 6):
|
||||
// Example (small, header=3): [0-2] unused | [3] 0x00 | [4] size | [5] type | [6...] payload
|
||||
// Example (medium, header=4): [0-1] unused | [2] 0x00 | [3-4] size | [5] type | [6...] payload
|
||||
// Example (large, header=6): [0] 0x00 | [1-3] size | [4-5] type | [6...] payload
|
||||
//
|
||||
// Batch messages 2+ (padding_size = actual header size, no unused bytes):
|
||||
// Example (small, header=3): [0] 0x00 | [1] size | [2] type | [3...] payload
|
||||
// Example (medium, header=4): [0] 0x00 | [1-2] size | [3] type | [4...] payload
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
assert(padding_size >= total_header_len);
|
||||
#endif
|
||||
uint32_t header_offset = padding_size - total_header_len;
|
||||
|
||||
// Write the plaintext header
|
||||
buf_start[header_offset] = 0x00; // indicator
|
||||
|
||||
// Encode varints directly into buffer using pre-computed lengths
|
||||
encode_varint_16(payload_size, size_varint_len, buf_start + header_offset + 1);
|
||||
encode_varint_8(message_type, type_varint_len, buf_start + header_offset + 1 + size_varint_len);
|
||||
|
||||
return total_header_len;
|
||||
}
|
||||
|
||||
APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
assert(this->state_ == State::DATA);
|
||||
#endif
|
||||
|
||||
uint16_t payload_size = static_cast<uint16_t>(buffer.get_buffer()->size() - HEADER_PADDING);
|
||||
uint8_t *buffer_data = buffer.get_buffer()->data();
|
||||
uint8_t header_len = write_plaintext_header(buffer_data, payload_size, type, HEADER_PADDING);
|
||||
return this->write_raw_fast_buf_(buffer_data + HEADER_PADDING - header_len,
|
||||
static_cast<uint16_t>(header_len + payload_size));
|
||||
}
|
||||
|
||||
APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer,
|
||||
std::span<const MessageInfo> messages) {
|
||||
APIError aerr = this->check_data_state_();
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
|
||||
if (messages.empty()) {
|
||||
return APIError::OK;
|
||||
}
|
||||
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
assert(this->state_ == State::DATA);
|
||||
assert(!messages.empty());
|
||||
#endif
|
||||
uint8_t *buffer_data = buffer.get_buffer()->data();
|
||||
|
||||
// Stack-allocated iovec array - no heap allocation
|
||||
StaticVector<struct iovec, MAX_MESSAGES_PER_BATCH> iovs;
|
||||
uint16_t total_write_len = 0;
|
||||
// First message has max padding (header_size = HEADER_PADDING), may have unused leading bytes.
|
||||
// Subsequent messages were encoded with exact header sizes (header_size = actual header len).
|
||||
// write_plaintext_header right-justifies the header within header_size bytes of padding.
|
||||
const auto &first = messages[0];
|
||||
uint8_t *first_start = buffer_data + first.offset;
|
||||
uint8_t header_len = write_plaintext_header(first_start, first.payload_size, first.message_type, HEADER_PADDING);
|
||||
uint8_t *write_start = first_start + HEADER_PADDING - header_len;
|
||||
uint16_t total_len = header_len + first.payload_size;
|
||||
|
||||
for (const auto &msg : messages) {
|
||||
// Calculate varint sizes for header layout using inline ternary to avoid varint_slow call overhead
|
||||
uint8_t size_varint_len = msg.payload_size < ProtoSize::VARINT_THRESHOLD_1_BYTE
|
||||
? 1
|
||||
: (msg.payload_size < ProtoSize::VARINT_THRESHOLD_2_BYTE ? 2 : 3);
|
||||
uint8_t type_varint_len = msg.message_type < ProtoSize::VARINT_THRESHOLD_1_BYTE ? 1 : 2;
|
||||
uint8_t total_header_len = 1 + size_varint_len + type_varint_len;
|
||||
|
||||
// Calculate where to start writing the header
|
||||
// The header starts at the latest possible position to minimize unused padding
|
||||
//
|
||||
// Example 1 (small values): total_header_len = 3, header_offset = 6 - 3 = 3
|
||||
// [0-2] - Unused padding
|
||||
// [3] - 0x00 indicator byte
|
||||
// [4] - Payload size varint (1 byte, for sizes 0-127)
|
||||
// [5] - Message type varint (1 byte, for types 0-127)
|
||||
// [6...] - Actual payload data
|
||||
//
|
||||
// Example 2 (medium values): total_header_len = 4, header_offset = 6 - 4 = 2
|
||||
// [0-1] - Unused padding
|
||||
// [2] - 0x00 indicator byte
|
||||
// [3-4] - Payload size varint (2 bytes, for sizes 128-16383)
|
||||
// [5] - Message type varint (1 byte, for types 0-127)
|
||||
// [6...] - Actual payload data
|
||||
//
|
||||
// Example 3 (large values): total_header_len = 6, header_offset = 6 - 6 = 0
|
||||
// [0] - 0x00 indicator byte
|
||||
// [1-3] - Payload size varint (3 bytes, for sizes 16384-65535)
|
||||
// [4-5] - Message type varint (2 bytes, for types 128-16383)
|
||||
// [6...] - Actual payload data
|
||||
//
|
||||
// The message starts at offset + frame_header_padding_
|
||||
// So we write the header starting at offset + frame_header_padding_ - total_header_len
|
||||
uint8_t *buf_start = buffer_data + msg.offset;
|
||||
uint32_t header_offset = frame_header_padding_ - total_header_len;
|
||||
|
||||
// Write the plaintext header
|
||||
buf_start[header_offset] = 0x00; // indicator
|
||||
|
||||
// Encode varints directly into buffer
|
||||
encode_varint_to_buffer(msg.payload_size, buf_start + header_offset + 1);
|
||||
encode_varint_to_buffer(msg.message_type, buf_start + header_offset + 1 + size_varint_len);
|
||||
|
||||
// Add iovec for this message (header + payload)
|
||||
size_t msg_len = static_cast<size_t>(total_header_len + msg.payload_size);
|
||||
iovs.push_back({buf_start + header_offset, msg_len});
|
||||
total_write_len += msg_len;
|
||||
for (size_t i = 1; i < messages.size(); i++) {
|
||||
const auto &msg = messages[i];
|
||||
header_len = write_plaintext_header(buffer_data + msg.offset, msg.payload_size, msg.message_type, msg.header_size);
|
||||
total_len += header_len + msg.payload_size;
|
||||
}
|
||||
|
||||
// Send all messages in one writev call
|
||||
return write_raw_(iovs.data(), iovs.size(), total_write_len);
|
||||
return this->write_raw_fast_buf_(write_start, total_len);
|
||||
}
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
@@ -7,18 +7,21 @@ namespace esphome::api {
|
||||
|
||||
class APIPlaintextFrameHelper final : public APIFrameHelper {
|
||||
public:
|
||||
// Plaintext header structure (worst case):
|
||||
// Pos 0: indicator (0x00)
|
||||
// Pos 1-3: payload size varint (up to 3 bytes)
|
||||
// Pos 4-5: message type varint (up to 2 bytes)
|
||||
// Pos 6+: actual payload data
|
||||
static constexpr uint8_t HEADER_PADDING = 1 + 3 + 2; // indicator + size varint + type varint
|
||||
|
||||
explicit APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket) : APIFrameHelper(std::move(socket)) {
|
||||
// Plaintext header structure (worst case):
|
||||
// Pos 0: indicator (0x00)
|
||||
// Pos 1-3: payload size varint (up to 3 bytes)
|
||||
// Pos 4-5: message type varint (up to 2 bytes)
|
||||
// Pos 6+: actual payload data
|
||||
frame_header_padding_ = 6;
|
||||
frame_header_padding_ = HEADER_PADDING;
|
||||
}
|
||||
~APIPlaintextFrameHelper() override = default;
|
||||
APIError init() override;
|
||||
APIError loop() override;
|
||||
APIError read_packet(ReadPacketBuffer *buffer) override;
|
||||
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
|
||||
APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) override;
|
||||
|
||||
protected:
|
||||
|
||||
@@ -22,6 +22,8 @@ extend google.protobuf.MessageOptions {
|
||||
optional bool log = 1039 [default=true];
|
||||
optional bool no_delay = 1040 [default=false];
|
||||
optional string base_class = 1041;
|
||||
optional bool inline_encode = 1042 [default=false];
|
||||
optional bool speed_optimized = 1043 [default=false];
|
||||
}
|
||||
|
||||
extend google.protobuf.FieldOptions {
|
||||
@@ -108,4 +110,10 @@ extend google.protobuf.FieldOptions {
|
||||
// length varint calculations and direct byte writes, since the length
|
||||
// varint is guaranteed to be 1 byte.
|
||||
optional uint32 max_data_length = 50018;
|
||||
|
||||
// mac_address: Field is a 48-bit MAC address stored in a uint64.
|
||||
// Emits encode_varint_raw_48bit which has a 7-byte fast path that avoids
|
||||
// the per-byte loop when the upper bits are non-zero (the common case
|
||||
// for real MAC addresses, since OUIs occupy the top 24 bits).
|
||||
optional bool mac_address = 50019 [default=false];
|
||||
}
|
||||
|
||||
@@ -745,7 +745,9 @@ uint32_t ListEntitiesSensorResponse::calculate_size() const {
|
||||
#endif
|
||||
return size;
|
||||
}
|
||||
uint8_t *SensorStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
|
||||
uint8_t *
|
||||
SensorStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
uint8_t *__restrict__ pos = buffer.get_pos();
|
||||
ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key);
|
||||
ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 2, this->state);
|
||||
@@ -755,7 +757,9 @@ uint8_t *SensorStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG
|
||||
#endif
|
||||
return pos;
|
||||
}
|
||||
uint32_t SensorStateResponse::calculate_size() const {
|
||||
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
|
||||
uint32_t
|
||||
SensorStateResponse::calculate_size() const {
|
||||
uint32_t size = 0;
|
||||
size += 5;
|
||||
size += ProtoSize::calc_float(1, this->state);
|
||||
@@ -912,16 +916,22 @@ bool SubscribeLogsRequest::decode_varint(uint32_t field_id, proto_varint_value_t
|
||||
}
|
||||
return true;
|
||||
}
|
||||
uint8_t *SubscribeLogsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
|
||||
uint8_t *
|
||||
SubscribeLogsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
uint8_t *__restrict__ pos = buffer.get_pos();
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, static_cast<uint32_t>(this->level));
|
||||
ProtoEncode::encode_bytes(pos PROTO_ENCODE_DEBUG_ARG, 3, this->message_ptr_, this->message_len_);
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, static_cast<uint32_t>(this->level), true);
|
||||
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 26);
|
||||
ProtoEncode::encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, this->message_len_);
|
||||
ProtoEncode::encode_raw(pos PROTO_ENCODE_DEBUG_ARG, this->message_ptr_, this->message_len_);
|
||||
return pos;
|
||||
}
|
||||
uint32_t SubscribeLogsResponse::calculate_size() const {
|
||||
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
|
||||
uint32_t
|
||||
SubscribeLogsResponse::calculate_size() const {
|
||||
uint32_t size = 0;
|
||||
size += this->level ? 2 : 0;
|
||||
size += ProtoSize::calc_length(1, this->message_len_);
|
||||
size += 2;
|
||||
size += ProtoSize::calc_length_force(1, this->message_len_);
|
||||
return size;
|
||||
}
|
||||
#ifdef USE_API_NOISE
|
||||
@@ -1429,6 +1439,7 @@ uint8_t *ListEntitiesClimateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCO
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 26, this->device_id);
|
||||
#endif
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 27, this->feature_flags);
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 28, static_cast<uint32_t>(this->temperature_unit));
|
||||
return pos;
|
||||
}
|
||||
uint32_t ListEntitiesClimateResponse::calculate_size() const {
|
||||
@@ -1478,6 +1489,7 @@ uint32_t ListEntitiesClimateResponse::calculate_size() const {
|
||||
size += ProtoSize::calc_uint32(2, this->device_id);
|
||||
#endif
|
||||
size += ProtoSize::calc_uint32(2, this->feature_flags);
|
||||
size += this->temperature_unit ? 3 : 0;
|
||||
return size;
|
||||
}
|
||||
uint8_t *ClimateStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
@@ -1635,6 +1647,7 @@ uint8_t *ListEntitiesWaterHeaterResponse::encode(ProtoWriteBuffer &buffer PROTO_
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 11, static_cast<uint32_t>(it), true);
|
||||
}
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 12, this->supported_features);
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 13, static_cast<uint32_t>(this->temperature_unit));
|
||||
return pos;
|
||||
}
|
||||
uint32_t ListEntitiesWaterHeaterResponse::calculate_size() const {
|
||||
@@ -1657,6 +1670,7 @@ uint32_t ListEntitiesWaterHeaterResponse::calculate_size() const {
|
||||
size += this->supported_modes->size() * 2;
|
||||
}
|
||||
size += ProtoSize::calc_uint32(1, this->supported_features);
|
||||
size += this->temperature_unit ? 2 : 0;
|
||||
return size;
|
||||
}
|
||||
uint8_t *WaterHeaterStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
@@ -2328,40 +2342,41 @@ bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id,
|
||||
}
|
||||
return true;
|
||||
}
|
||||
uint8_t *BluetoothLERawAdvertisement::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
uint8_t *__restrict__ pos = buffer.get_pos();
|
||||
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 8);
|
||||
ProtoEncode::encode_varint_raw_64(pos PROTO_ENCODE_DEBUG_ARG, this->address);
|
||||
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 16);
|
||||
ProtoEncode::encode_varint_raw_short(pos PROTO_ENCODE_DEBUG_ARG, encode_zigzag32(this->rssi));
|
||||
if (this->address_type) {
|
||||
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 24);
|
||||
ProtoEncode::encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, this->address_type);
|
||||
}
|
||||
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 34);
|
||||
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, static_cast<uint8_t>(this->data_len));
|
||||
ProtoEncode::encode_raw(pos PROTO_ENCODE_DEBUG_ARG, this->data, this->data_len);
|
||||
return pos;
|
||||
}
|
||||
uint32_t BluetoothLERawAdvertisement::calculate_size() const {
|
||||
uint32_t size = 0;
|
||||
size += ProtoSize::calc_uint64_force(1, this->address);
|
||||
size += ProtoSize::calc_sint32_force(1, this->rssi);
|
||||
size += this->address_type ? 2 : 0;
|
||||
size += 2 + this->data_len;
|
||||
return size;
|
||||
}
|
||||
uint8_t *BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
|
||||
uint8_t *
|
||||
BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
uint8_t *__restrict__ pos = buffer.get_pos();
|
||||
for (uint16_t i = 0; i < this->advertisements_len; i++) {
|
||||
ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 1, this->advertisements[i]);
|
||||
auto &sub_msg = this->advertisements[i];
|
||||
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 10);
|
||||
uint8_t *len_pos = pos;
|
||||
ProtoEncode::reserve_byte(pos PROTO_ENCODE_DEBUG_ARG);
|
||||
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 8);
|
||||
ProtoEncode::encode_varint_raw_48bit(pos PROTO_ENCODE_DEBUG_ARG, sub_msg.address);
|
||||
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 16);
|
||||
ProtoEncode::encode_varint_raw_short(pos PROTO_ENCODE_DEBUG_ARG, encode_zigzag32(sub_msg.rssi));
|
||||
if (sub_msg.address_type) {
|
||||
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 24);
|
||||
ProtoEncode::encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, sub_msg.address_type);
|
||||
}
|
||||
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 34);
|
||||
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, static_cast<uint8_t>(sub_msg.data_len));
|
||||
ProtoEncode::encode_raw(pos PROTO_ENCODE_DEBUG_ARG, sub_msg.data, sub_msg.data_len);
|
||||
*len_pos = static_cast<uint8_t>(pos - len_pos - 1);
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
uint32_t BluetoothLERawAdvertisementsResponse::calculate_size() const {
|
||||
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
|
||||
uint32_t
|
||||
BluetoothLERawAdvertisementsResponse::calculate_size() const {
|
||||
uint32_t size = 0;
|
||||
for (uint16_t i = 0; i < this->advertisements_len; i++) {
|
||||
size += ProtoSize::calc_message_force(1, this->advertisements[i].calculate_size());
|
||||
auto &sub_msg = this->advertisements[i];
|
||||
size += 2;
|
||||
size += ProtoSize::calc_uint64_48bit_force(1, sub_msg.address);
|
||||
size += ProtoSize::calc_sint32_force(1, sub_msg.rssi);
|
||||
size += sub_msg.address_type ? 2 : 0;
|
||||
size += 2 + sub_msg.data_len;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
@@ -3850,7 +3865,7 @@ uint32_t ListEntitiesInfraredResponse::calculate_size() const {
|
||||
return size;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_IR_RF
|
||||
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
|
||||
bool InfraredRFTransmitRawTimingsRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) {
|
||||
switch (field_id) {
|
||||
#ifdef USE_DEVICES
|
||||
@@ -3864,6 +3879,9 @@ bool InfraredRFTransmitRawTimingsRequest::decode_varint(uint32_t field_id, proto
|
||||
case 4:
|
||||
this->repeat_count = value;
|
||||
break;
|
||||
case 6:
|
||||
this->modulation = value;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -3917,6 +3935,46 @@ uint32_t InfraredRFReceiveEvent::calculate_size() const {
|
||||
return size;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_RADIO_FREQUENCY
|
||||
uint8_t *ListEntitiesRadioFrequencyResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
uint8_t *__restrict__ pos = buffer.get_pos();
|
||||
ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id);
|
||||
ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key);
|
||||
ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name);
|
||||
#ifdef USE_ENTITY_ICON
|
||||
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 4, this->icon);
|
||||
#endif
|
||||
ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 5, this->disabled_by_default);
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 6, static_cast<uint32_t>(this->entity_category));
|
||||
#ifdef USE_DEVICES
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 7, this->device_id);
|
||||
#endif
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 8, this->capabilities);
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 9, this->frequency_min);
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 10, this->frequency_max);
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 11, this->supported_modulations);
|
||||
return pos;
|
||||
}
|
||||
uint32_t ListEntitiesRadioFrequencyResponse::calculate_size() const {
|
||||
uint32_t size = 0;
|
||||
size += 2 + this->object_id.size();
|
||||
size += 5;
|
||||
size += 2 + this->name.size();
|
||||
#ifdef USE_ENTITY_ICON
|
||||
size += !this->icon.empty() ? 2 + this->icon.size() : 0;
|
||||
#endif
|
||||
size += ProtoSize::calc_bool(1, this->disabled_by_default);
|
||||
size += this->entity_category ? 2 : 0;
|
||||
#ifdef USE_DEVICES
|
||||
size += ProtoSize::calc_uint32(1, this->device_id);
|
||||
#endif
|
||||
size += ProtoSize::calc_uint32(1, this->capabilities);
|
||||
size += ProtoSize::calc_uint32(1, this->frequency_min);
|
||||
size += ProtoSize::calc_uint32(1, this->frequency_max);
|
||||
size += ProtoSize::calc_uint32(1, this->supported_modulations);
|
||||
return size;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
bool SerialProxyConfigureRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) {
|
||||
switch (field_id) {
|
||||
|
||||
@@ -92,6 +92,11 @@ enum SupportsResponseType : uint32_t {
|
||||
SUPPORTS_RESPONSE_STATUS = 100,
|
||||
};
|
||||
#endif
|
||||
enum TemperatureUnit : uint32_t {
|
||||
TEMPERATURE_UNIT_CELSIUS = 0,
|
||||
TEMPERATURE_UNIT_FAHRENHEIT = 1,
|
||||
TEMPERATURE_UNIT_KELVIN = 2,
|
||||
};
|
||||
#ifdef USE_CLIMATE
|
||||
enum ClimateMode : uint32_t {
|
||||
CLIMATE_MODE_OFF = 0,
|
||||
@@ -176,6 +181,8 @@ enum LockState : uint32_t {
|
||||
LOCK_STATE_JAMMED = 3,
|
||||
LOCK_STATE_LOCKING = 4,
|
||||
LOCK_STATE_UNLOCKING = 5,
|
||||
LOCK_STATE_OPENING = 6,
|
||||
LOCK_STATE_OPEN = 7,
|
||||
};
|
||||
enum LockCommand : uint32_t {
|
||||
LOCK_UNLOCK = 0,
|
||||
@@ -1372,7 +1379,7 @@ class CameraImageRequest final : public ProtoDecodableMessage {
|
||||
class ListEntitiesClimateResponse final : public InfoResponseProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 46;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 150;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 153;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const LogString *message_name() const override { return LOG_STR("list_entities_climate_response"); }
|
||||
#endif
|
||||
@@ -1394,6 +1401,7 @@ class ListEntitiesClimateResponse final : public InfoResponseProtoMessage {
|
||||
float visual_min_humidity{0.0f};
|
||||
float visual_max_humidity{0.0f};
|
||||
uint32_t feature_flags{0};
|
||||
enums::TemperatureUnit temperature_unit{};
|
||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
|
||||
uint32_t calculate_size() const;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@@ -1471,7 +1479,7 @@ class ClimateCommandRequest final : public CommandProtoMessage {
|
||||
class ListEntitiesWaterHeaterResponse final : public InfoResponseProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 132;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 63;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 65;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const LogString *message_name() const override { return LOG_STR("list_entities_water_heater_response"); }
|
||||
#endif
|
||||
@@ -1480,6 +1488,7 @@ class ListEntitiesWaterHeaterResponse final : public InfoResponseProtoMessage {
|
||||
float target_temperature_step{0.0f};
|
||||
const water_heater::WaterHeaterModeMask *supported_modes{};
|
||||
uint32_t supported_features{0};
|
||||
enums::TemperatureUnit temperature_unit{};
|
||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
|
||||
uint32_t calculate_size() const;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@@ -1888,8 +1897,6 @@ class BluetoothLERawAdvertisement final : public ProtoMessage {
|
||||
uint32_t address_type{0};
|
||||
uint8_t data[62]{};
|
||||
uint8_t data_len{0};
|
||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
|
||||
uint32_t calculate_size() const;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *dump_to(DumpBuffer &out) const override;
|
||||
#endif
|
||||
@@ -3056,11 +3063,11 @@ class ListEntitiesInfraredResponse final : public InfoResponseProtoMessage {
|
||||
protected:
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_IR_RF
|
||||
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
|
||||
class InfraredRFTransmitRawTimingsRequest final : public ProtoDecodableMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 136;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 220;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 224;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const LogString *message_name() const override { return LOG_STR("infrared_rf_transmit_raw_timings_request"); }
|
||||
#endif
|
||||
@@ -3073,6 +3080,7 @@ class InfraredRFTransmitRawTimingsRequest final : public ProtoDecodableMessage {
|
||||
const uint8_t *timings_data_{nullptr};
|
||||
uint16_t timings_length_{0};
|
||||
uint16_t timings_count_{0};
|
||||
uint32_t modulation{0};
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *dump_to(DumpBuffer &out) const override;
|
||||
#endif
|
||||
@@ -3103,6 +3111,27 @@ class InfraredRFReceiveEvent final : public ProtoMessage {
|
||||
protected:
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_RADIO_FREQUENCY
|
||||
class ListEntitiesRadioFrequencyResponse final : public InfoResponseProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 148;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 56;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const LogString *message_name() const override { return LOG_STR("list_entities_radio_frequency_response"); }
|
||||
#endif
|
||||
uint32_t capabilities{0};
|
||||
uint32_t frequency_min{0};
|
||||
uint32_t frequency_max{0};
|
||||
uint32_t supported_modulations{0};
|
||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
|
||||
uint32_t calculate_size() const;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *dump_to(DumpBuffer &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
class SerialProxyConfigureRequest final : public ProtoDecodableMessage {
|
||||
public:
|
||||
|
||||
@@ -297,6 +297,18 @@ template<> const char *proto_enum_to_string<enums::SupportsResponseType>(enums::
|
||||
}
|
||||
}
|
||||
#endif
|
||||
template<> const char *proto_enum_to_string<enums::TemperatureUnit>(enums::TemperatureUnit value) {
|
||||
switch (value) {
|
||||
case enums::TEMPERATURE_UNIT_CELSIUS:
|
||||
return ESPHOME_PSTR("TEMPERATURE_UNIT_CELSIUS");
|
||||
case enums::TEMPERATURE_UNIT_FAHRENHEIT:
|
||||
return ESPHOME_PSTR("TEMPERATURE_UNIT_FAHRENHEIT");
|
||||
case enums::TEMPERATURE_UNIT_KELVIN:
|
||||
return ESPHOME_PSTR("TEMPERATURE_UNIT_KELVIN");
|
||||
default:
|
||||
return ESPHOME_PSTR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
#ifdef USE_CLIMATE
|
||||
template<> const char *proto_enum_to_string<enums::ClimateMode>(enums::ClimateMode value) {
|
||||
switch (value) {
|
||||
@@ -475,6 +487,10 @@ template<> const char *proto_enum_to_string<enums::LockState>(enums::LockState v
|
||||
return ESPHOME_PSTR("LOCK_STATE_LOCKING");
|
||||
case enums::LOCK_STATE_UNLOCKING:
|
||||
return ESPHOME_PSTR("LOCK_STATE_UNLOCKING");
|
||||
case enums::LOCK_STATE_OPENING:
|
||||
return ESPHOME_PSTR("LOCK_STATE_OPENING");
|
||||
case enums::LOCK_STATE_OPEN:
|
||||
return ESPHOME_PSTR("LOCK_STATE_OPEN");
|
||||
default:
|
||||
return ESPHOME_PSTR("UNKNOWN");
|
||||
}
|
||||
@@ -1539,6 +1555,7 @@ const char *ListEntitiesClimateResponse::dump_to(DumpBuffer &out) const {
|
||||
dump_field(out, ESPHOME_PSTR("device_id"), this->device_id);
|
||||
#endif
|
||||
dump_field(out, ESPHOME_PSTR("feature_flags"), this->feature_flags);
|
||||
dump_field(out, ESPHOME_PSTR("temperature_unit"), static_cast<enums::TemperatureUnit>(this->temperature_unit));
|
||||
return out.c_str();
|
||||
}
|
||||
const char *ClimateStateResponse::dump_to(DumpBuffer &out) const {
|
||||
@@ -1612,6 +1629,7 @@ const char *ListEntitiesWaterHeaterResponse::dump_to(DumpBuffer &out) const {
|
||||
dump_field(out, ESPHOME_PSTR("supported_modes"), static_cast<enums::WaterHeaterMode>(it), 4);
|
||||
}
|
||||
dump_field(out, ESPHOME_PSTR("supported_features"), this->supported_features);
|
||||
dump_field(out, ESPHOME_PSTR("temperature_unit"), static_cast<enums::TemperatureUnit>(this->temperature_unit));
|
||||
return out.c_str();
|
||||
}
|
||||
const char *WaterHeaterStateResponse::dump_to(DumpBuffer &out) const {
|
||||
@@ -2576,7 +2594,7 @@ const char *ListEntitiesInfraredResponse::dump_to(DumpBuffer &out) const {
|
||||
return out.c_str();
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_IR_RF
|
||||
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
|
||||
const char *InfraredRFTransmitRawTimingsRequest::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, ESPHOME_PSTR("InfraredRFTransmitRawTimingsRequest"));
|
||||
#ifdef USE_DEVICES
|
||||
@@ -2591,6 +2609,7 @@ const char *InfraredRFTransmitRawTimingsRequest::dump_to(DumpBuffer &out) const
|
||||
out.append_p(ESPHOME_PSTR(" values, "));
|
||||
append_uint(out, this->timings_length_);
|
||||
out.append_p(ESPHOME_PSTR(" bytes]\n"));
|
||||
dump_field(out, ESPHOME_PSTR("modulation"), this->modulation);
|
||||
return out.c_str();
|
||||
}
|
||||
const char *InfraredRFReceiveEvent::dump_to(DumpBuffer &out) const {
|
||||
@@ -2605,6 +2624,27 @@ const char *InfraredRFReceiveEvent::dump_to(DumpBuffer &out) const {
|
||||
return out.c_str();
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_RADIO_FREQUENCY
|
||||
const char *ListEntitiesRadioFrequencyResponse::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesRadioFrequencyResponse"));
|
||||
dump_field(out, ESPHOME_PSTR("object_id"), this->object_id);
|
||||
dump_field(out, ESPHOME_PSTR("key"), this->key);
|
||||
dump_field(out, ESPHOME_PSTR("name"), this->name);
|
||||
#ifdef USE_ENTITY_ICON
|
||||
dump_field(out, ESPHOME_PSTR("icon"), this->icon);
|
||||
#endif
|
||||
dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default);
|
||||
dump_field(out, ESPHOME_PSTR("entity_category"), static_cast<enums::EntityCategory>(this->entity_category));
|
||||
#ifdef USE_DEVICES
|
||||
dump_field(out, ESPHOME_PSTR("device_id"), this->device_id);
|
||||
#endif
|
||||
dump_field(out, ESPHOME_PSTR("capabilities"), this->capabilities);
|
||||
dump_field(out, ESPHOME_PSTR("frequency_min"), this->frequency_min);
|
||||
dump_field(out, ESPHOME_PSTR("frequency_max"), this->frequency_max);
|
||||
dump_field(out, ESPHOME_PSTR("supported_modulations"), this->supported_modulations);
|
||||
return out.c_str();
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
const char *SerialProxyConfigureRequest::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyConfigureRequest"));
|
||||
|
||||
@@ -21,6 +21,7 @@ void APIServerConnectionBase::log_receive_message_(const LogString *name) {
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_API
|
||||
void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {
|
||||
// Check authentication/connection requirements
|
||||
switch (msg_type) {
|
||||
@@ -625,7 +626,7 @@ void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const ui
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_IR_RF
|
||||
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
|
||||
case InfraredRFTransmitRawTimingsRequest::MESSAGE_TYPE: {
|
||||
InfraredRFTransmitRawTimingsRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
@@ -706,5 +707,6 @@ void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const ui
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif // USE_API
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
@@ -211,7 +211,7 @@ class APIServerConnectionBase {
|
||||
void on_z_wave_proxy_request(const ZWaveProxyRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_IR_RF
|
||||
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
|
||||
void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &value){};
|
||||
#endif
|
||||
|
||||
|
||||
@@ -30,6 +30,11 @@ APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-c
|
||||
|
||||
APIServer::APIServer() { global_api_server = this; }
|
||||
|
||||
// Custom deleter defined here so `delete` sees the complete APIConnection type.
|
||||
// This prevents libc++ from emitting an "incomplete type" error when other
|
||||
// translation units only have the forward declaration of APIConnection.
|
||||
void APIServer::APIConnectionDeleter::operator()(APIConnection *p) const { delete p; }
|
||||
|
||||
void APIServer::socket_failed_(const LogString *msg) {
|
||||
ESP_LOGW(TAG, "Socket %s: errno %d", LOG_STR_ARG(msg), errno);
|
||||
this->destroy_socket_();
|
||||
@@ -118,7 +123,7 @@ void APIServer::loop() {
|
||||
this->accept_new_connections_();
|
||||
}
|
||||
|
||||
if (this->clients_.empty()) {
|
||||
if (this->api_connection_count_ == 0) {
|
||||
// Check reboot timeout - done in loop to avoid scheduler heap churn
|
||||
// (cancelled scheduler items sit in heap memory until their scheduled time)
|
||||
if (this->reboot_timeout_ != 0) {
|
||||
@@ -135,15 +140,15 @@ void APIServer::loop() {
|
||||
// Check network connectivity once for all clients
|
||||
if (!network::is_connected()) {
|
||||
// Network is down - disconnect all clients
|
||||
for (auto &client : this->clients_) {
|
||||
for (auto &client : this->active_clients()) {
|
||||
client->on_fatal_error();
|
||||
client->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("Network down; disconnect"));
|
||||
}
|
||||
// Continue to process and clean up the clients below
|
||||
}
|
||||
|
||||
size_t client_index = 0;
|
||||
while (client_index < this->clients_.size()) {
|
||||
uint8_t client_index = 0;
|
||||
while (client_index < this->api_connection_count_) {
|
||||
auto &client = this->clients_[client_index];
|
||||
|
||||
// Common case: process active client
|
||||
@@ -161,7 +166,7 @@ void APIServer::loop() {
|
||||
}
|
||||
}
|
||||
|
||||
void APIServer::remove_client_(size_t client_index) {
|
||||
void APIServer::remove_client_(uint8_t client_index) {
|
||||
auto &client = this->clients_[client_index];
|
||||
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
@@ -179,14 +184,17 @@ void APIServer::remove_client_(size_t client_index) {
|
||||
// Close socket now (was deferred from on_fatal_error to allow getpeername)
|
||||
client->helper_->close();
|
||||
|
||||
// Swap with the last element and pop (avoids expensive vector shifts)
|
||||
if (client_index < this->clients_.size() - 1) {
|
||||
std::swap(this->clients_[client_index], this->clients_.back());
|
||||
// Swap-and-reset: move the removed client to the trailing slot and null it out so slots
|
||||
// [api_connection_count_, N) remain nullptr.
|
||||
const uint8_t last_index = this->api_connection_count_ - 1;
|
||||
if (client_index < last_index) {
|
||||
std::swap(this->clients_[client_index], this->clients_[last_index]);
|
||||
}
|
||||
this->clients_.pop_back();
|
||||
this->clients_[last_index].reset();
|
||||
this->api_connection_count_--;
|
||||
|
||||
// Last client disconnected - set warning and start tracking for reboot timeout
|
||||
if (this->clients_.empty() && this->reboot_timeout_ != 0) {
|
||||
if (this->api_connection_count_ == 0 && this->reboot_timeout_ != 0) {
|
||||
this->status_set_warning(LOG_STR("waiting for client connection"));
|
||||
this->last_connected_ = App.get_loop_component_start_time();
|
||||
}
|
||||
@@ -210,8 +218,8 @@ void __attribute__((flatten)) APIServer::accept_new_connections_() {
|
||||
sock->getpeername_to(peername);
|
||||
|
||||
// Check if we're at the connection limit
|
||||
if (this->clients_.size() >= this->max_connections_) {
|
||||
ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, peername);
|
||||
if (this->api_connection_count_ >= MAX_API_CONNECTIONS) {
|
||||
ESP_LOGW(TAG, "Max connections (%d), rejecting %s", MAX_API_CONNECTIONS, peername);
|
||||
// Immediately close - socket destructor will handle cleanup
|
||||
sock.reset();
|
||||
continue;
|
||||
@@ -220,11 +228,11 @@ void __attribute__((flatten)) APIServer::accept_new_connections_() {
|
||||
ESP_LOGD(TAG, "Accept %s", peername);
|
||||
|
||||
auto *conn = new APIConnection(std::move(sock), this);
|
||||
this->clients_.emplace_back(conn);
|
||||
this->clients_[this->api_connection_count_++].reset(conn);
|
||||
conn->start();
|
||||
|
||||
// First client connected - clear warning and update timestamp
|
||||
if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) {
|
||||
if (this->api_connection_count_ == 1 && this->reboot_timeout_ != 0) {
|
||||
this->status_clear_warning();
|
||||
this->last_connected_ = App.get_loop_component_start_time();
|
||||
}
|
||||
@@ -237,7 +245,7 @@ void APIServer::dump_config() {
|
||||
" Address: %s:%u\n"
|
||||
" Listen backlog: %u\n"
|
||||
" Max connections: %u",
|
||||
network::get_use_address(), this->port_, this->listen_backlog_, this->max_connections_);
|
||||
network::get_use_address(), this->port_, this->listen_backlog_, MAX_API_CONNECTIONS);
|
||||
#ifdef USE_API_NOISE
|
||||
ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_.has_psk()));
|
||||
if (!this->noise_ctx_.has_psk()) {
|
||||
@@ -255,7 +263,7 @@ void APIServer::handle_disconnect(APIConnection *conn) {}
|
||||
void APIServer::on_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \
|
||||
if (obj->is_internal()) \
|
||||
return; \
|
||||
for (auto &c : this->clients_) { \
|
||||
for (auto &c : this->active_clients()) { \
|
||||
if (c->flags_.state_subscription) \
|
||||
c->send_##entity_name##_state(obj); \
|
||||
} \
|
||||
@@ -337,7 +345,7 @@ API_DISPATCH_UPDATE(water_heater::WaterHeater, water_heater)
|
||||
void APIServer::on_event(event::Event *obj) {
|
||||
if (obj->is_internal())
|
||||
return;
|
||||
for (auto &c : this->clients_) {
|
||||
for (auto &c : this->active_clients()) {
|
||||
if (c->flags_.state_subscription)
|
||||
c->send_event(obj);
|
||||
}
|
||||
@@ -349,7 +357,7 @@ void APIServer::on_event(event::Event *obj) {
|
||||
void APIServer::on_update(update::UpdateEntity *obj) {
|
||||
if (obj->is_internal())
|
||||
return;
|
||||
for (auto &c : this->clients_) {
|
||||
for (auto &c : this->active_clients()) {
|
||||
if (c->flags_.state_subscription)
|
||||
c->send_update_state(obj);
|
||||
}
|
||||
@@ -360,12 +368,12 @@ void APIServer::on_update(update::UpdateEntity *obj) {
|
||||
void APIServer::on_zwave_proxy_request(const ZWaveProxyRequest &msg) {
|
||||
// We could add code to manage a second subscription type, but, since this message type is
|
||||
// very infrequent and small, we simply send it to all clients
|
||||
for (auto &c : this->clients_)
|
||||
for (auto &c : this->active_clients())
|
||||
c->send_message(msg);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_IR_RF
|
||||
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
|
||||
void APIServer::send_infrared_rf_receive_event([[maybe_unused]] uint32_t device_id, uint32_t key,
|
||||
const std::vector<int32_t> *timings) {
|
||||
InfraredRFReceiveEvent resp{};
|
||||
@@ -375,7 +383,7 @@ void APIServer::send_infrared_rf_receive_event([[maybe_unused]] uint32_t device_
|
||||
resp.key = key;
|
||||
resp.timings = timings;
|
||||
|
||||
for (auto &c : this->clients_)
|
||||
for (auto &c : this->active_clients())
|
||||
c->send_infrared_rf_receive_event(resp);
|
||||
}
|
||||
#endif
|
||||
@@ -392,7 +400,7 @@ void APIServer::set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = bat
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call) {
|
||||
for (auto &client : this->clients_) {
|
||||
for (auto &client : this->active_clients()) {
|
||||
client->send_homeassistant_action(call);
|
||||
}
|
||||
}
|
||||
@@ -532,7 +540,7 @@ bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString
|
||||
return;
|
||||
}
|
||||
ESP_LOGW(TAG, "Disconnecting all clients to reset PSK");
|
||||
for (auto &c : this->clients_) {
|
||||
for (auto &c : this->active_clients()) {
|
||||
DisconnectRequest req;
|
||||
c->send_message(req);
|
||||
}
|
||||
@@ -583,7 +591,7 @@ bool APIServer::clear_noise_psk(bool make_active) {
|
||||
|
||||
#ifdef USE_HOMEASSISTANT_TIME
|
||||
void APIServer::request_time() {
|
||||
for (auto &client : this->clients_) {
|
||||
for (auto &client : this->active_clients()) {
|
||||
if (!client->flags_.remove && client->is_authenticated()) {
|
||||
client->send_time_request();
|
||||
return; // Only request from one client to avoid clock conflicts
|
||||
@@ -593,8 +601,8 @@ void APIServer::request_time() {
|
||||
#endif
|
||||
|
||||
bool APIServer::is_connected_with_state_subscription() const {
|
||||
for (const auto &client : this->clients_) {
|
||||
if (client->flags_.state_subscription) {
|
||||
for (uint8_t i = 0; i < this->api_connection_count_; i++) {
|
||||
if (this->clients_[i]->flags_.state_subscription) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -609,7 +617,7 @@ void APIServer::on_log(uint8_t level, const char *tag, const char *message, size
|
||||
// we would be filling a buffer we are trying to clear
|
||||
return;
|
||||
}
|
||||
for (auto &c : this->clients_) {
|
||||
for (auto &c : this->active_clients()) {
|
||||
if (!c->flags_.remove && c->get_log_subscription_level() >= level)
|
||||
c->try_send_log_message(level, tag, message, message_len);
|
||||
}
|
||||
@@ -618,7 +626,7 @@ void APIServer::on_log(uint8_t level, const char *tag, const char *message, size
|
||||
|
||||
#ifdef USE_CAMERA
|
||||
void APIServer::on_camera_image(const std::shared_ptr<camera::CameraImage> &image) {
|
||||
for (auto &c : this->clients_) {
|
||||
for (auto &c : this->active_clients()) {
|
||||
if (!c->flags_.remove)
|
||||
c->set_camera_state(image);
|
||||
}
|
||||
@@ -635,7 +643,7 @@ void APIServer::on_shutdown() {
|
||||
this->batch_delay_ = 5;
|
||||
|
||||
// Send disconnect requests to all connected clients
|
||||
for (auto &c : this->clients_) {
|
||||
for (auto &c : this->active_clients()) {
|
||||
DisconnectRequest req;
|
||||
if (!c->send_message(req)) {
|
||||
// If we can't send the disconnect request directly (tx_buffer full),
|
||||
@@ -653,7 +661,7 @@ bool APIServer::teardown() {
|
||||
this->loop();
|
||||
|
||||
// Return true only when all clients have been torn down
|
||||
return this->clients_.empty();
|
||||
return this->api_connection_count_ == 0;
|
||||
}
|
||||
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
#include "esphome/components/camera/camera.h"
|
||||
#endif
|
||||
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::api {
|
||||
@@ -63,7 +65,6 @@ class APIServer final : public Component,
|
||||
void set_batch_delay(uint16_t batch_delay);
|
||||
uint16_t get_batch_delay() const { return batch_delay_; }
|
||||
void set_listen_backlog(uint8_t listen_backlog) { this->listen_backlog_ = listen_backlog; }
|
||||
void set_max_connections(uint8_t max_connections) { this->max_connections_ = max_connections; }
|
||||
|
||||
// Get reference to shared buffer for API connections
|
||||
APIBuffer &get_shared_buffer_ref() { return shared_write_buffer_; }
|
||||
@@ -182,13 +183,36 @@ class APIServer final : public Component,
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
void on_zwave_proxy_request(const ZWaveProxyRequest &msg);
|
||||
#endif
|
||||
#ifdef USE_IR_RF
|
||||
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
|
||||
void send_infrared_rf_receive_event(uint32_t device_id, uint32_t key, const std::vector<int32_t> *timings);
|
||||
#endif
|
||||
|
||||
bool is_connected() const { return !this->clients_.empty(); }
|
||||
bool is_connected() const { return this->api_connection_count_ != 0; }
|
||||
bool is_connected_with_state_subscription() const;
|
||||
|
||||
// Range-for view over the populated slice [0, api_connection_count_). Read-only with respect
|
||||
// to ownership — callers get `const unique_ptr&` so they can invoke non-const methods on the
|
||||
// APIConnection but cannot reset/move the slot and break the count invariant.
|
||||
// Custom deleter is defined out-of-line in api_server.cpp so libc++ does not
|
||||
// eagerly instantiate `delete static_cast<APIConnection *>(p)` here, where
|
||||
// only the forward declaration of APIConnection is visible (incomplete type).
|
||||
struct APIConnectionDeleter {
|
||||
void operator()(APIConnection *p) const;
|
||||
};
|
||||
using APIConnectionPtr = std::unique_ptr<APIConnection, APIConnectionDeleter>;
|
||||
class ActiveClientsView {
|
||||
const APIConnectionPtr *begin_;
|
||||
const APIConnectionPtr *end_;
|
||||
|
||||
public:
|
||||
ActiveClientsView(const APIConnectionPtr *b, const APIConnectionPtr *e) : begin_(b), end_(e) {}
|
||||
const APIConnectionPtr *begin() const { return this->begin_; }
|
||||
const APIConnectionPtr *end() const { return this->end_; }
|
||||
};
|
||||
ActiveClientsView active_clients() const {
|
||||
return {this->clients_.data(), this->clients_.data() + this->api_connection_count_};
|
||||
}
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
struct HomeAssistantStateSubscription {
|
||||
const char *entity_id; // Pointer to flash (internal) or heap (external)
|
||||
@@ -234,8 +258,8 @@ class APIServer final : public Component,
|
||||
protected:
|
||||
// Accept incoming socket connections. Only called when socket has pending connections.
|
||||
void __attribute__((noinline)) accept_new_connections_();
|
||||
// Remove a disconnected client by index. Swaps with last element and pops.
|
||||
void __attribute__((noinline)) remove_client_(size_t client_index);
|
||||
// Remove a disconnected client by index. Swaps with the last populated slot and resets it.
|
||||
void __attribute__((noinline)) remove_client_(uint8_t client_index);
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg,
|
||||
@@ -273,8 +297,9 @@ class APIServer final : public Component,
|
||||
uint32_t reboot_timeout_{300000};
|
||||
uint32_t last_connected_{0};
|
||||
|
||||
// Slots [0, api_connection_count_) are populated; trailing slots are always nullptr.
|
||||
std::array<APIConnectionPtr, MAX_API_CONNECTIONS> clients_{};
|
||||
// Vectors and strings (12 bytes each on 32-bit)
|
||||
std::vector<std::unique_ptr<APIConnection>> clients_;
|
||||
// Shared proto write buffer for all connections.
|
||||
// Not pre-allocated: all send paths call prepare_first_message_buffer() which
|
||||
// reserves the exact needed size. Pre-allocating here would cause heap fragmentation
|
||||
@@ -309,10 +334,10 @@ class APIServer final : public Component,
|
||||
uint16_t port_{6053};
|
||||
uint16_t batch_delay_{100};
|
||||
// Connection limits - these defaults will be overridden by config values
|
||||
// from cv.SplitDefault in __init__.py which sets platform-specific defaults
|
||||
// from cv.SplitDefault in __init__.py which sets platform-specific defaults.
|
||||
uint8_t listen_backlog_{4};
|
||||
uint8_t max_connections_{8};
|
||||
bool shutting_down_ = false;
|
||||
uint8_t api_connection_count_{0};
|
||||
// 7 bytes used, 1 byte padding
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
|
||||
@@ -93,7 +93,24 @@ async def async_run_logs(
|
||||
config, raw_line, backtrace_state=backtrace_state
|
||||
)
|
||||
|
||||
stop = await async_run(cli, on_log, name=name, subscribe_states=subscribe_states)
|
||||
# Safe to fall back to plaintext here only for this diagnostics use
|
||||
# case: the stream is one-way from device to client, and this code
|
||||
# never accepts commands or acts on any message the device sends.
|
||||
# An on-path attacker could still both inject fabricated log lines
|
||||
# and passively read the device's log output (and any state data
|
||||
# delivered when subscribe_states is enabled), so this does lose
|
||||
# confidentiality as well as authentication/integrity. That tradeoff
|
||||
# is acceptable for operator-visible logs, which aioesphomeapi also
|
||||
# warns may come from an unverified device. Never mirror this opt-in
|
||||
# for any connection that sends data to the device or uses Home
|
||||
# Assistant actions.
|
||||
stop = await async_run(
|
||||
cli,
|
||||
on_log,
|
||||
name=name,
|
||||
subscribe_states=subscribe_states,
|
||||
allow_plaintext_fallback=True,
|
||||
)
|
||||
try:
|
||||
await asyncio.Event().wait()
|
||||
finally:
|
||||
|
||||
@@ -79,6 +79,9 @@ LIST_ENTITIES_HANDLER(water_heater, water_heater::WaterHeater, ListEntitiesWater
|
||||
#ifdef USE_INFRARED
|
||||
LIST_ENTITIES_HANDLER(infrared, infrared::Infrared, ListEntitiesInfraredResponse)
|
||||
#endif
|
||||
#ifdef USE_RADIO_FREQUENCY
|
||||
LIST_ENTITIES_HANDLER(radio_frequency, radio_frequency::RadioFrequency, ListEntitiesRadioFrequencyResponse)
|
||||
#endif
|
||||
#ifdef USE_EVENT
|
||||
LIST_ENTITIES_HANDLER(event, event::Event, ListEntitiesEventResponse)
|
||||
#endif
|
||||
|
||||
@@ -87,6 +87,9 @@ class ListEntitiesIterator final : public ComponentIterator {
|
||||
#ifdef USE_INFRARED
|
||||
bool on_infrared(infrared::Infrared *entity) override;
|
||||
#endif
|
||||
#ifdef USE_RADIO_FREQUENCY
|
||||
bool on_radio_frequency(radio_frequency::RadioFrequency *entity) override;
|
||||
#endif
|
||||
#ifdef USE_EVENT
|
||||
bool on_event(event::Event *entity) override;
|
||||
#endif
|
||||
|
||||
@@ -342,6 +342,32 @@ class ProtoEncode {
|
||||
}
|
||||
encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value);
|
||||
}
|
||||
/// Encode a 48-bit MAC address (stored in a uint64) as varint.
|
||||
/// Real MAC addresses occupy the full 48 bits (OUI in upper 24), so the
|
||||
/// fast path -- any non-zero bit in the top 6 of 48 -- emits exactly 7 bytes
|
||||
/// with no per-byte branch. Falls back to the general loop otherwise.
|
||||
/// Caller must guarantee value fits in 48 bits (checked in debug builds).
|
||||
static inline void ESPHOME_ALWAYS_INLINE encode_varint_raw_48bit(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint64_t value) {
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
assert(value < (1ULL << (MAC_ADDRESS_SIZE * 8)) && "encode_varint_raw_48bit: value exceeds 48 bits");
|
||||
#endif
|
||||
// 7-byte varint holds 49 bits (7 * 7), so a 48-bit value needs all 7 bytes
|
||||
// whenever bit 42 or higher is set (i.e. value >= 1 << (48 - 6)).
|
||||
if (value >= (1ULL << (MAC_ADDRESS_SIZE * 8 - 6))) [[likely]] {
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 7);
|
||||
pos[0] = static_cast<uint8_t>(value | 0x80);
|
||||
pos[1] = static_cast<uint8_t>((value >> 7) | 0x80);
|
||||
pos[2] = static_cast<uint8_t>((value >> 14) | 0x80);
|
||||
pos[3] = static_cast<uint8_t>((value >> 21) | 0x80);
|
||||
pos[4] = static_cast<uint8_t>((value >> 28) | 0x80);
|
||||
pos[5] = static_cast<uint8_t>((value >> 35) | 0x80);
|
||||
pos[6] = static_cast<uint8_t>(value >> 42);
|
||||
pos += 7;
|
||||
return;
|
||||
}
|
||||
encode_varint_raw_64(pos PROTO_ENCODE_DEBUG_ARG, value);
|
||||
}
|
||||
static inline void ESPHOME_ALWAYS_INLINE encode_field_raw(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, uint32_t type) {
|
||||
encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, (field_id << 3) | type);
|
||||
@@ -352,6 +378,12 @@ class ProtoEncode {
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
|
||||
*pos++ = b;
|
||||
}
|
||||
/// Reserve one byte for later backpatch (e.g., sub-message length).
|
||||
/// Advances pos past the reserved byte without writing a value.
|
||||
static inline void ESPHOME_ALWAYS_INLINE reserve_byte(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM) {
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
|
||||
pos++;
|
||||
}
|
||||
/// Write raw bytes to the buffer (no tag, no length prefix).
|
||||
static inline void ESPHOME_ALWAYS_INLINE encode_raw(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
const void *data, size_t len) {
|
||||
@@ -645,6 +677,17 @@ class ProtoSize {
|
||||
static constexpr uint32_t VARINT_THRESHOLD_3_BYTE = 1 << 21; // 2097152
|
||||
static constexpr uint32_t VARINT_THRESHOLD_4_BYTE = 1 << 28; // 268435456
|
||||
|
||||
// Varint encoded length for a 16-bit value (1, 2, or 3 bytes).
|
||||
// Fully inline — no slow path call for values >= 128.
|
||||
static constexpr inline uint8_t ESPHOME_ALWAYS_INLINE varint16(uint16_t value) {
|
||||
return value < VARINT_THRESHOLD_1_BYTE ? 1 : (value < VARINT_THRESHOLD_2_BYTE ? 2 : 3);
|
||||
}
|
||||
|
||||
// Varint encoded length for an 8-bit value (1 or 2 bytes).
|
||||
static constexpr inline uint8_t ESPHOME_ALWAYS_INLINE varint8(uint8_t value) {
|
||||
return value < VARINT_THRESHOLD_1_BYTE ? 1 : 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates the size in bytes needed to encode a uint32_t value as a varint
|
||||
*
|
||||
@@ -800,6 +843,14 @@ class ProtoSize {
|
||||
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_uint64_force(uint32_t field_id_size, uint64_t value) {
|
||||
return field_id_size + varint(value);
|
||||
}
|
||||
/// 48-bit MAC address variant: matches encode_varint_raw_48bit's fast path.
|
||||
/// When any of the top 6 of 48 bits is set the encoded varint is 7 bytes;
|
||||
/// otherwise fall back to the general size calculation.
|
||||
/// Caller must guarantee value fits in 48 bits (encoder asserts in debug).
|
||||
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_uint64_48bit_force(uint32_t field_id_size,
|
||||
uint64_t value) {
|
||||
return field_id_size + (value >= (1ULL << (MAC_ADDRESS_SIZE * 8 - 6)) ? 7 : varint(value));
|
||||
}
|
||||
static constexpr uint32_t calc_length(uint32_t field_id_size, size_t len) {
|
||||
return len ? field_id_size + varint(static_cast<uint32_t>(len)) + static_cast<uint32_t>(len) : 0;
|
||||
}
|
||||
|
||||
@@ -82,6 +82,9 @@ class InitialStateIterator final : public ComponentIterator {
|
||||
#ifdef USE_INFRARED
|
||||
bool on_infrared(infrared::Infrared *infrared) override { return true; };
|
||||
#endif
|
||||
#ifdef USE_RADIO_FREQUENCY
|
||||
bool on_radio_frequency(radio_frequency::RadioFrequency *radio_frequency) override { return true; };
|
||||
#endif
|
||||
#ifdef USE_EVENT
|
||||
bool on_event(event::Event *event) override { return true; };
|
||||
#endif
|
||||
|
||||
@@ -83,7 +83,7 @@ def angle_to_position(value, min=-360, max=360):
|
||||
value = angle(min=min, max=max)(value)
|
||||
return (RESOLUTION + round(value * ANGLE_TO_POSITION)) % RESOLUTION
|
||||
except cv.Invalid as e:
|
||||
raise cv.Invalid(f"When using angle, {e.error_message}")
|
||||
raise cv.Invalid(f"When using angle, {e.error_message}") from e
|
||||
|
||||
|
||||
def percent_to_position(value):
|
||||
@@ -164,7 +164,7 @@ def has_valid_range_config():
|
||||
except cv.Invalid as e:
|
||||
raise cv.Invalid(
|
||||
f"The range between start and end position is invalid. It was was {range} but {e.error_message}"
|
||||
)
|
||||
) from e
|
||||
|
||||
return validator
|
||||
|
||||
|
||||
@@ -169,45 +169,43 @@ async def at581x_settings_to_code(config, action_id, template_arg, args):
|
||||
|
||||
# Radar configuration
|
||||
if frontend_reset := config.get(CONF_HW_FRONTEND_RESET):
|
||||
template_ = await cg.templatable(frontend_reset, args, int)
|
||||
template_ = await cg.templatable(frontend_reset, args, cg.int8)
|
||||
cg.add(var.set_hw_frontend_reset(template_))
|
||||
|
||||
if freq := config.get(CONF_FREQUENCY):
|
||||
if cg.is_template(freq):
|
||||
template_ = await cg.templatable(freq, args, cg.int32)
|
||||
else:
|
||||
template_ = int(freq / 1000000)
|
||||
if not cg.is_template(freq):
|
||||
freq = int(freq / 1000000)
|
||||
template_ = await cg.templatable(freq, args, cg.int_)
|
||||
cg.add(var.set_frequency(template_))
|
||||
|
||||
if (sens_dist := config.get(CONF_SENSING_DISTANCE)) is not None:
|
||||
template_ = await cg.templatable(sens_dist, args, int)
|
||||
template_ = await cg.templatable(sens_dist, args, cg.int_)
|
||||
cg.add(var.set_sensing_distance(template_))
|
||||
|
||||
if selfcheck := config.get(CONF_POWERON_SELFCHECK_TIME):
|
||||
template_ = await cg.templatable(selfcheck, args, cg.int32)
|
||||
template_ = await cg.templatable(selfcheck, args, cg.int_)
|
||||
cg.add(var.set_poweron_selfcheck_time(template_))
|
||||
|
||||
if protect := config.get(CONF_PROTECT_TIME):
|
||||
template_ = await cg.templatable(protect, args, cg.int32)
|
||||
template_ = await cg.templatable(protect, args, cg.int_)
|
||||
cg.add(var.set_protect_time(template_))
|
||||
|
||||
if trig_base := config.get(CONF_TRIGGER_BASE):
|
||||
template_ = await cg.templatable(trig_base, args, cg.int32)
|
||||
template_ = await cg.templatable(trig_base, args, cg.int_)
|
||||
cg.add(var.set_trigger_base(template_))
|
||||
|
||||
if trig_keep := config.get(CONF_TRIGGER_KEEP):
|
||||
template_ = await cg.templatable(trig_keep, args, cg.int32)
|
||||
template_ = await cg.templatable(trig_keep, args, cg.int_)
|
||||
cg.add(var.set_trigger_keep(template_))
|
||||
|
||||
if (stage_gain := config.get(CONF_STAGE_GAIN)) is not None:
|
||||
template_ = await cg.templatable(stage_gain, args, int)
|
||||
template_ = await cg.templatable(stage_gain, args, cg.int_)
|
||||
cg.add(var.set_stage_gain(template_))
|
||||
|
||||
if power := config.get(CONF_POWER_CONSUMPTION):
|
||||
if cg.is_template(power):
|
||||
template_ = await cg.templatable(power, args, cg.int32)
|
||||
else:
|
||||
template_ = int(power * 1000000)
|
||||
if not cg.is_template(power):
|
||||
power = int(power * 1000000)
|
||||
template_ = await cg.templatable(power, args, cg.int_)
|
||||
cg.add(var.set_power_consumption(template_))
|
||||
|
||||
return var
|
||||
|
||||
@@ -111,14 +111,14 @@ class ATM90E32Component : public PollingComponent,
|
||||
#endif
|
||||
float get_reference_voltage(uint8_t phase) {
|
||||
#ifdef USE_NUMBER
|
||||
return (phase >= 0 && phase < 3 && ref_voltages_[phase]) ? ref_voltages_[phase]->state : 120.0; // Default voltage
|
||||
return (phase < 3 && ref_voltages_[phase]) ? ref_voltages_[phase]->state : 120.0; // Default voltage
|
||||
#else
|
||||
return 120.0; // Default voltage
|
||||
#endif
|
||||
}
|
||||
float get_reference_current(uint8_t phase) {
|
||||
#ifdef USE_NUMBER
|
||||
return (phase >= 0 && phase < 3 && ref_currents_[phase]) ? ref_currents_[phase]->state : 5.0f; // Default current
|
||||
return (phase < 3 && ref_currents_[phase]) ? ref_currents_[phase]->state : 5.0f; // Default current
|
||||
#else
|
||||
return 5.0f; // Default current
|
||||
#endif
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.esp32 import add_idf_component, include_builtin_idf_component
|
||||
from esphome.components.esp32 import (
|
||||
add_idf_component,
|
||||
add_idf_sdkconfig_option,
|
||||
include_builtin_idf_component,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_BITS_PER_SAMPLE, CONF_NUM_CHANNELS, CONF_SAMPLE_RATE
|
||||
from esphome.core import CORE
|
||||
@@ -27,6 +31,7 @@ class AudioData:
|
||||
flac_support: bool = False
|
||||
mp3_support: bool = False
|
||||
opus_support: bool = False
|
||||
micro_decoder_support: bool = False
|
||||
|
||||
|
||||
def _get_data() -> AudioData:
|
||||
@@ -50,6 +55,11 @@ def request_opus_support() -> None:
|
||||
_get_data().opus_support = True
|
||||
|
||||
|
||||
def request_micro_decoder_support() -> None:
|
||||
"""Request micro-decoder library support for audio decoding."""
|
||||
_get_data().micro_decoder_support = True
|
||||
|
||||
|
||||
CONF_MIN_BITS_PER_SAMPLE = "min_bits_per_sample"
|
||||
CONF_MAX_BITS_PER_SAMPLE = "max_bits_per_sample"
|
||||
CONF_MIN_CHANNELS = "min_channels"
|
||||
@@ -208,6 +218,19 @@ async def to_code(config):
|
||||
)
|
||||
|
||||
data = _get_data()
|
||||
|
||||
if data.micro_decoder_support:
|
||||
add_idf_component(name="esphome/micro-decoder", ref="0.2.0")
|
||||
|
||||
# All codecs are enabled by default in micro-decoder, so disable the ones that aren't requested to save flash
|
||||
if not data.flac_support:
|
||||
add_idf_sdkconfig_option("CONFIG_MICRO_DECODER_CODEC_FLAC", False)
|
||||
if not data.mp3_support:
|
||||
add_idf_sdkconfig_option("CONFIG_MICRO_DECODER_CODEC_MP3", False)
|
||||
if not data.opus_support:
|
||||
add_idf_sdkconfig_option("CONFIG_MICRO_DECODER_CODEC_OPUS", False)
|
||||
|
||||
# Legacy audio_decoder.cpp support defines and components
|
||||
if data.flac_support:
|
||||
cg.add_define("USE_AUDIO_FLAC_SUPPORT")
|
||||
add_idf_component(name="esphome/micro-flac", ref="0.1.1")
|
||||
|
||||
@@ -32,7 +32,7 @@ async def audio_adc_set_mic_gain_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
template_ = await cg.templatable(config.get(CONF_MIC_GAIN), args, float)
|
||||
template_ = await cg.templatable(config.get(CONF_MIC_GAIN), args, cg.float_)
|
||||
cg.add(var.set_mic_gain(template_))
|
||||
|
||||
return var
|
||||
|
||||
@@ -52,7 +52,7 @@ async def audio_dac_set_volume_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
template_ = await cg.templatable(config.get(CONF_VOLUME), args, float)
|
||||
template_ = await cg.templatable(config.get(CONF_VOLUME), args, cg.float_)
|
||||
cg.add(var.set_volume(template_))
|
||||
|
||||
return var
|
||||
|
||||
@@ -116,7 +116,7 @@ def read_audio_file_and_type(file_config: ConfigType) -> tuple[bytes, MockObj]:
|
||||
raise cv.Invalid(
|
||||
f"Unable to determine audio file type of '{path}'. "
|
||||
f"Try re-encoding the file into a supported format. Details: {e}"
|
||||
)
|
||||
) from e
|
||||
|
||||
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["NONE"]
|
||||
if file_type == "wav":
|
||||
|
||||
0
esphome/components/audio_http/__init__.py
Normal file
0
esphome/components/audio_http/__init__.py
Normal file
163
esphome/components/audio_http/audio_http_media_source.cpp
Normal file
163
esphome/components/audio_http/audio_http_media_source.cpp
Normal file
@@ -0,0 +1,163 @@
|
||||
#include "audio_http_media_source.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace esphome::audio_http {
|
||||
|
||||
static const char *const TAG = "audio_http_media_source";
|
||||
|
||||
// Decoder task / buffer tuning. Kept here as constants so the header stays free of magic numbers.
|
||||
static constexpr size_t DEFAULT_TRANSFER_BUFFER_SIZE = 8 * 1024; // Staging buffer between HTTP reader and decoder
|
||||
static constexpr uint32_t HTTP_TIMEOUT_MS = 5000; // HTTP connect/read timeout
|
||||
static constexpr uint32_t AUDIO_WRITE_TIMEOUT_MS = 50; // Max blocking time per on_audio_write() call
|
||||
static constexpr uint32_t READER_WRITE_TIMEOUT_MS = 50; // Max blocking time when writing into the ring buffer
|
||||
static constexpr uint8_t READER_TASK_PRIORITY = 2;
|
||||
static constexpr uint8_t DECODER_TASK_PRIORITY = 2;
|
||||
static constexpr size_t READER_TASK_STACK_SIZE = 4096;
|
||||
static constexpr size_t DECODER_TASK_STACK_SIZE = 5120;
|
||||
static constexpr uint32_t PAUSE_POLL_DELAY_MS = 20;
|
||||
static constexpr const char *const HTTP_URI_PREFIX = "http://";
|
||||
static constexpr const char *const HTTPS_URI_PREFIX = "https://";
|
||||
|
||||
void AudioHTTPMediaSource::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Audio HTTP Media Source:\n"
|
||||
" Buffer Size: %zu bytes\n"
|
||||
" Decoder Task Stack in PSRAM: %s",
|
||||
this->buffer_size_, YESNO(this->decoder_task_stack_in_psram_));
|
||||
}
|
||||
|
||||
void AudioHTTPMediaSource::setup() {
|
||||
this->disable_loop();
|
||||
|
||||
micro_decoder::DecoderConfig config;
|
||||
config.ring_buffer_size = this->buffer_size_;
|
||||
// Keep the transfer buffer smaller than the ring buffer so the reader can top up the ring
|
||||
// while the decoder is still draining it, instead of oscillating between empty and full.
|
||||
config.transfer_buffer_size = std::min(DEFAULT_TRANSFER_BUFFER_SIZE, this->buffer_size_ / 2);
|
||||
config.http_timeout_ms = HTTP_TIMEOUT_MS;
|
||||
config.audio_write_timeout_ms = AUDIO_WRITE_TIMEOUT_MS;
|
||||
config.reader_write_timeout_ms = READER_WRITE_TIMEOUT_MS;
|
||||
config.reader_priority = READER_TASK_PRIORITY;
|
||||
config.decoder_priority = DECODER_TASK_PRIORITY;
|
||||
config.reader_stack_size = READER_TASK_STACK_SIZE;
|
||||
config.decoder_stack_size = DECODER_TASK_STACK_SIZE;
|
||||
config.decoder_stack_in_psram = this->decoder_task_stack_in_psram_;
|
||||
|
||||
this->decoder_ = std::make_unique<micro_decoder::DecoderSource>(config);
|
||||
if (this->decoder_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to allocate decoder");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
this->decoder_->set_listener(this); // We inherit from micro_decoder::DecoderListener
|
||||
}
|
||||
|
||||
void AudioHTTPMediaSource::loop() { this->decoder_->loop(); }
|
||||
|
||||
bool AudioHTTPMediaSource::can_handle(const std::string &uri) const {
|
||||
return uri.starts_with(HTTP_URI_PREFIX) || uri.starts_with(HTTPS_URI_PREFIX);
|
||||
}
|
||||
|
||||
// Called from the orchestrator's main loop, so no synchronization needed with loop()
|
||||
bool AudioHTTPMediaSource::play_uri(const std::string &uri) {
|
||||
if (!this->is_ready() || this->is_failed() || this->status_has_error() || !this->has_listener()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if source is already playing
|
||||
if (this->get_state() != media_source::MediaSourceState::IDLE) {
|
||||
ESP_LOGE(TAG, "Cannot play '%s': source is busy", uri.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate URI starts with "http://" or "https://"
|
||||
if (!uri.starts_with(HTTP_URI_PREFIX) && !uri.starts_with(HTTPS_URI_PREFIX)) {
|
||||
ESP_LOGE(TAG, "Invalid URI: '%s'", uri.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this->decoder_->play_url(uri)) {
|
||||
this->pause_.store(false, std::memory_order_relaxed);
|
||||
this->enable_loop();
|
||||
return true;
|
||||
}
|
||||
|
||||
ESP_LOGE(TAG, "Failed to start playback of '%s'", uri.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Called from the orchestrator's main loop, so no synchronization needed with loop()
|
||||
void AudioHTTPMediaSource::handle_command(media_source::MediaSourceCommand command) {
|
||||
switch (command) {
|
||||
case media_source::MediaSourceCommand::STOP:
|
||||
this->decoder_->stop();
|
||||
break;
|
||||
case media_source::MediaSourceCommand::PAUSE:
|
||||
// Only valid while actively playing; ignoring from IDLE/ERROR/PAUSED prevents the state
|
||||
// machine from getting stuck in PAUSED when no playback is active (which would block the
|
||||
// next play_uri() call via its IDLE-state precondition).
|
||||
if (this->get_state() != media_source::MediaSourceState::PLAYING)
|
||||
break;
|
||||
// PAUSE does not stop the decoder task. Instead, on_audio_write() returns 0 and temporarily
|
||||
// yields, which fills the ring buffer and applies back pressure that effectively pauses both
|
||||
// the decoder and HTTP reader tasks.
|
||||
this->set_state_(media_source::MediaSourceState::PAUSED);
|
||||
this->pause_.store(true, std::memory_order_relaxed);
|
||||
break;
|
||||
case media_source::MediaSourceCommand::PLAY:
|
||||
// Only resume from PAUSED; don't fabricate a PLAYING state from IDLE/ERROR.
|
||||
if (this->get_state() != media_source::MediaSourceState::PAUSED)
|
||||
break;
|
||||
this->set_state_(media_source::MediaSourceState::PLAYING);
|
||||
this->pause_.store(false, std::memory_order_relaxed);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Called from the decoder task. Forwards to the orchestrator's listener, which is responsible for
|
||||
// being thread-safe with respect to its own audio writer.
|
||||
size_t AudioHTTPMediaSource::on_audio_write(const uint8_t *data, size_t length, uint32_t timeout_ms) {
|
||||
if (this->pause_.load(std::memory_order_relaxed)) {
|
||||
vTaskDelay(pdMS_TO_TICKS(PAUSE_POLL_DELAY_MS));
|
||||
return 0;
|
||||
}
|
||||
return this->write_output(data, length, timeout_ms, this->stream_info_);
|
||||
}
|
||||
|
||||
// Called from the decoder task before the first on_audio_write().
|
||||
void AudioHTTPMediaSource::on_stream_info(const micro_decoder::AudioStreamInfo &info) {
|
||||
this->stream_info_ = audio::AudioStreamInfo(info.get_bits_per_sample(), info.get_channels(), info.get_sample_rate());
|
||||
}
|
||||
|
||||
// microDecoder invokes on_state_change() from inside decoder_->loop(), so this runs on the main
|
||||
// loop thread and it's safe to call set_state_() directly.
|
||||
void AudioHTTPMediaSource::on_state_change(micro_decoder::DecoderState state) {
|
||||
switch (state) {
|
||||
case micro_decoder::DecoderState::IDLE:
|
||||
this->set_state_(media_source::MediaSourceState::IDLE);
|
||||
this->disable_loop();
|
||||
break;
|
||||
case micro_decoder::DecoderState::PLAYING:
|
||||
this->set_state_(media_source::MediaSourceState::PLAYING);
|
||||
break;
|
||||
case micro_decoder::DecoderState::FAILED:
|
||||
this->set_state_(media_source::MediaSourceState::ERROR);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::audio_http
|
||||
|
||||
#endif // USE_ESP32
|
||||
59
esphome/components/audio_http/audio_http_media_source.h
Normal file
59
esphome/components/audio_http/audio_http_media_source.h
Normal file
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "esphome/components/audio/audio.h"
|
||||
#include "esphome/components/media_source/media_source.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
#include <micro_decoder/decoder_source.h>
|
||||
#include <micro_decoder/types.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace esphome::audio_http {
|
||||
|
||||
// Inherits from two unrelated listener-style interfaces:
|
||||
// - media_source::MediaSource: this source reports state and writes audio *to* an orchestrator
|
||||
// (the orchestrator calls set_listener() on us with a MediaSourceListener*).
|
||||
// - micro_decoder::DecoderListener: the underlying decoder calls back *into* us with decoded
|
||||
// audio and state changes (we call decoder_->set_listener(this) in setup()).
|
||||
// The two set_listener() methods live on different base classes and serve opposite directions.
|
||||
class AudioHTTPMediaSource : public Component, public media_source::MediaSource, public micro_decoder::DecoderListener {
|
||||
public:
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
|
||||
void set_buffer_size(size_t buffer_size) { this->buffer_size_ = buffer_size; }
|
||||
void set_task_stack_in_psram(bool task_stack_in_psram) { this->decoder_task_stack_in_psram_ = task_stack_in_psram; }
|
||||
|
||||
// MediaSource interface implementation
|
||||
bool play_uri(const std::string &uri) override;
|
||||
void handle_command(media_source::MediaSourceCommand command) override;
|
||||
bool can_handle(const std::string &uri) const override;
|
||||
|
||||
// DecoderListener interface implementation
|
||||
size_t on_audio_write(const uint8_t *data, size_t length, uint32_t timeout_ms) override;
|
||||
void on_stream_info(const micro_decoder::AudioStreamInfo &info) override;
|
||||
void on_state_change(micro_decoder::DecoderState state) override;
|
||||
|
||||
protected:
|
||||
std::unique_ptr<micro_decoder::DecoderSource> decoder_;
|
||||
audio::AudioStreamInfo stream_info_;
|
||||
|
||||
size_t buffer_size_{50000};
|
||||
|
||||
// Written from the main loop in handle_command(), read from the decoder task in
|
||||
// on_audio_write(). Must be atomic to avoid a data race.
|
||||
std::atomic<bool> pause_{false};
|
||||
bool decoder_task_stack_in_psram_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::audio_http
|
||||
|
||||
#endif // USE_ESP32
|
||||
59
esphome/components/audio_http/media_source.py
Normal file
59
esphome/components/audio_http/media_source.py
Normal file
@@ -0,0 +1,59 @@
|
||||
from typing import Any
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import audio, esp32, media_source, psram
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_BUFFER_SIZE, CONF_ID, CONF_TASK_STACK_IN_PSRAM
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@kahrendt"]
|
||||
AUTO_LOAD = ["audio"]
|
||||
|
||||
audio_http_ns = cg.esphome_ns.namespace("audio_http")
|
||||
AudioHTTPMediaSource = audio_http_ns.class_(
|
||||
"AudioHTTPMediaSource", cg.Component, media_source.MediaSource
|
||||
)
|
||||
|
||||
|
||||
def _request_micro_decoder(config: ConfigType) -> ConfigType:
|
||||
audio.request_micro_decoder_support()
|
||||
return config
|
||||
|
||||
|
||||
def _validate_task_stack_in_psram(value: Any) -> bool:
|
||||
# Only require the psram component when actually enabling PSRAM stacks; validating
|
||||
# the boolean first means `false` doesn't trigger the requires_component check.
|
||||
if value := cv.boolean(value):
|
||||
return cv.requires_component(psram.DOMAIN)(value)
|
||||
return value
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
media_source.media_source_schema(
|
||||
AudioHTTPMediaSource,
|
||||
)
|
||||
.extend(
|
||||
{
|
||||
cv.Optional(CONF_BUFFER_SIZE, default=50000): cv.int_range(
|
||||
min=5000, max=1000000
|
||||
),
|
||||
cv.Optional(CONF_TASK_STACK_IN_PSRAM): _validate_task_stack_in_psram,
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA),
|
||||
cv.only_on_esp32,
|
||||
_request_micro_decoder,
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await media_source.register_media_source(var, config)
|
||||
|
||||
if config.get(CONF_TASK_STACK_IN_PSRAM):
|
||||
cg.add(var.set_task_stack_in_psram(True))
|
||||
esp32.add_idf_sdkconfig_option(
|
||||
"CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True
|
||||
)
|
||||
cg.add(var.set_buffer_size(config[CONF_BUFFER_SIZE]))
|
||||
@@ -61,6 +61,15 @@ void BedJetClimate::dump_config() {
|
||||
}
|
||||
|
||||
void BedJetClimate::setup() {
|
||||
// Set custom modes once during setup — stored on Climate base class, wired via get_traits()
|
||||
this->set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES);
|
||||
this->set_supported_custom_presets({
|
||||
this->heating_mode_ == HEAT_MODE_EXTENDED ? "LTD HT" : "EXT HT",
|
||||
"M1",
|
||||
"M2",
|
||||
"M3",
|
||||
});
|
||||
|
||||
// restore set points
|
||||
auto restore = this->restore_state_();
|
||||
if (restore.has_value()) {
|
||||
|
||||
@@ -42,21 +42,14 @@ class BedJetClimate : public climate::Climate, public BedJetClient, public Polli
|
||||
climate::CLIMATE_MODE_DRY,
|
||||
});
|
||||
|
||||
// It would be better if we had a slider for the fan modes.
|
||||
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({
|
||||
this->heating_mode_ == HEAT_MODE_EXTENDED ? "LTD HT" : "EXT HT",
|
||||
"M1",
|
||||
"M2",
|
||||
"M3",
|
||||
});
|
||||
// Custom fan modes and presets are set once in setup(), stored on Climate base class,
|
||||
// and wired automatically via get_traits()
|
||||
traits.set_visual_min_temperature(19.0);
|
||||
traits.set_visual_max_temperature(43.0);
|
||||
traits.set_visual_temperature_step(1.0);
|
||||
|
||||
@@ -154,7 +154,7 @@ void BH1750Sensor::loop() {
|
||||
break;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "'%s': Illuminance=%.1flx", this->get_name().c_str(), lx);
|
||||
ESP_LOGV(TAG, "'%s': Illuminance=%.1flx", this->get_name().c_str(), lx);
|
||||
this->status_clear_warning();
|
||||
this->publish_state(lx);
|
||||
this->state_ = IDLE;
|
||||
|
||||
@@ -62,6 +62,7 @@ from esphome.const import (
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import (
|
||||
entity_duplicate_validator,
|
||||
queue_entity_register,
|
||||
setup_device_class,
|
||||
setup_entity,
|
||||
)
|
||||
@@ -332,8 +333,9 @@ def parse_multi_click_timing_str(value):
|
||||
try:
|
||||
state = cv.boolean(parts[0])
|
||||
except cv.Invalid:
|
||||
# pylint: disable=raise-missing-from
|
||||
raise cv.Invalid(f"First word must either be ON or OFF, not {parts[0]}")
|
||||
raise cv.Invalid(
|
||||
f"First word must either be ON or OFF, not {parts[0]}"
|
||||
) from None
|
||||
|
||||
if parts[1] != "for":
|
||||
raise cv.Invalid(f"Second word must be 'for', got {parts[1]}")
|
||||
@@ -350,7 +352,9 @@ def parse_multi_click_timing_str(value):
|
||||
try:
|
||||
length = cv.positive_time_period_milliseconds(parts[4])
|
||||
except cv.Invalid as err:
|
||||
raise cv.Invalid(f"Multi Click Grammar Parsing length failed: {err}")
|
||||
raise cv.Invalid(
|
||||
f"Multi Click Grammar Parsing length failed: {err}"
|
||||
) from err
|
||||
return {CONF_STATE: state, key: str(length)}
|
||||
|
||||
if parts[3] != "to":
|
||||
@@ -359,12 +363,16 @@ def parse_multi_click_timing_str(value):
|
||||
try:
|
||||
min_length = cv.positive_time_period_milliseconds(parts[2])
|
||||
except cv.Invalid as err:
|
||||
raise cv.Invalid(f"Multi Click Grammar Parsing minimum length failed: {err}")
|
||||
raise cv.Invalid(
|
||||
f"Multi Click Grammar Parsing minimum length failed: {err}"
|
||||
) from err
|
||||
|
||||
try:
|
||||
max_length = cv.positive_time_period_milliseconds(parts[4])
|
||||
except cv.Invalid as err:
|
||||
raise cv.Invalid(f"Multi Click Grammar Parsing minimum length failed: {err}")
|
||||
raise cv.Invalid(
|
||||
f"Multi Click Grammar Parsing maximum length failed: {err}"
|
||||
) from err
|
||||
|
||||
return {
|
||||
CONF_STATE: state,
|
||||
@@ -617,7 +625,7 @@ async def setup_binary_sensor_core_(var, config):
|
||||
async def register_binary_sensor(var, config):
|
||||
if not CORE.has_id(config[CONF_ID]):
|
||||
var = cg.Pvariable(config[CONF_ID], var)
|
||||
cg.add(cg.App.register_binary_sensor(var))
|
||||
queue_entity_register("binary_sensor", config)
|
||||
CORE.register_platform_component("binary_sensor", var)
|
||||
await setup_binary_sensor_core_(var, config)
|
||||
|
||||
|
||||
@@ -50,29 +50,31 @@ void MultiClickTriggerBase::on_state_(bool state) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (*this->at_index_ == this->timing_count_) {
|
||||
// at_index_ has a value here (the !has_value() branch above returns).
|
||||
size_t at_index = *this->at_index_;
|
||||
if (at_index == this->timing_count_) {
|
||||
this->trigger_();
|
||||
return;
|
||||
}
|
||||
|
||||
MultiClickTriggerEvent evt = this->timing_[*this->at_index_];
|
||||
MultiClickTriggerEvent evt = this->timing_[at_index];
|
||||
|
||||
if (evt.max_length != 4294967294UL) {
|
||||
ESP_LOGV(TAG, "A i=%zu min=%" PRIu32 " max=%" PRIu32, *this->at_index_, evt.min_length, evt.max_length); // NOLINT
|
||||
ESP_LOGV(TAG, "A i=%zu min=%" PRIu32 " max=%" PRIu32, at_index, evt.min_length, evt.max_length); // NOLINT
|
||||
this->schedule_is_valid_(evt.min_length);
|
||||
this->schedule_is_not_valid_(evt.max_length);
|
||||
} else if (*this->at_index_ + 1 != this->timing_count_) {
|
||||
ESP_LOGV(TAG, "B i=%zu min=%" PRIu32, *this->at_index_, evt.min_length); // NOLINT
|
||||
} else if (at_index + 1 != this->timing_count_) {
|
||||
ESP_LOGV(TAG, "B i=%zu min=%" PRIu32, at_index, evt.min_length); // NOLINT
|
||||
this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID);
|
||||
this->schedule_is_valid_(evt.min_length);
|
||||
} else {
|
||||
ESP_LOGV(TAG, "C i=%zu min=%" PRIu32, *this->at_index_, evt.min_length); // NOLINT
|
||||
ESP_LOGV(TAG, "C i=%zu min=%" PRIu32, at_index, evt.min_length); // NOLINT
|
||||
this->is_valid_ = false;
|
||||
this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID);
|
||||
this->set_timeout(MULTICLICK_TRIGGER_ID, evt.min_length, [this]() { this->trigger_(); });
|
||||
}
|
||||
|
||||
*this->at_index_ = *this->at_index_ + 1;
|
||||
this->at_index_ = at_index + 1;
|
||||
}
|
||||
void MultiClickTriggerBase::schedule_cooldown_() {
|
||||
ESP_LOGV(TAG, "Multi Click: Invalid length of press, starting cooldown of %" PRIu32 " ms", this->invalid_cooldown_);
|
||||
|
||||
@@ -65,3 +65,8 @@ async def to_code(config):
|
||||
@pins.PIN_SCHEMA_REGISTRY.register("bk72xx", PIN_SCHEMA)
|
||||
async def pin_to_code(config):
|
||||
return await libretiny.gpio.component_pin_to_code(config)
|
||||
|
||||
|
||||
# Called by writer.py; delegates to the shared libretiny implementation.
|
||||
def copy_files() -> None:
|
||||
libretiny.copy_files()
|
||||
|
||||
@@ -16,6 +16,7 @@ from esphome.components.libretiny.const import (
|
||||
FAMILY_BK7231N,
|
||||
FAMILY_BK7231Q,
|
||||
FAMILY_BK7231T,
|
||||
FAMILY_BK7238,
|
||||
FAMILY_BK7251,
|
||||
)
|
||||
|
||||
@@ -24,16 +25,32 @@ BK72XX_BOARDS = {
|
||||
"name": "WB2L_M1 Wi-Fi Module",
|
||||
"family": FAMILY_BK7231N,
|
||||
},
|
||||
"xh-wb3s": {
|
||||
"name": "NiceMCU XH-WB3S",
|
||||
"family": FAMILY_BK7238,
|
||||
},
|
||||
"cbu": {
|
||||
"name": "CBU Wi-Fi Module",
|
||||
"family": FAMILY_BK7231N,
|
||||
},
|
||||
"t1-u": {
|
||||
"name": "T1-U Wi-Fi Module",
|
||||
"family": FAMILY_BK7238,
|
||||
},
|
||||
"generic-bk7238-tuya": {
|
||||
"name": "Generic - BK7238 (Tuya T1)",
|
||||
"family": FAMILY_BK7238,
|
||||
},
|
||||
"t1-m": {
|
||||
"name": "T1-M Wi-Fi Module",
|
||||
"family": FAMILY_BK7238,
|
||||
},
|
||||
"generic-bk7231t-qfn32-tuya": {
|
||||
"name": "Generic - BK7231T (Tuya QFN32)",
|
||||
"name": "Generic - BK7231T (Tuya)",
|
||||
"family": FAMILY_BK7231T,
|
||||
},
|
||||
"generic-bk7231n-qfn32-tuya": {
|
||||
"name": "Generic - BK7231N (Tuya QFN32)",
|
||||
"name": "Generic - BK7231N (Tuya)",
|
||||
"family": FAMILY_BK7231N,
|
||||
},
|
||||
"cb1s": {
|
||||
@@ -64,6 +81,10 @@ BK72XX_BOARDS = {
|
||||
"name": "Generic - BK7252",
|
||||
"family": FAMILY_BK7251,
|
||||
},
|
||||
"t1-3s": {
|
||||
"name": "T1-3S Wi-Fi Module",
|
||||
"family": FAMILY_BK7238,
|
||||
},
|
||||
"wb2l": {
|
||||
"name": "WB2L Wi-Fi Module",
|
||||
"family": FAMILY_BK7231T,
|
||||
@@ -80,6 +101,10 @@ BK72XX_BOARDS = {
|
||||
"name": "CB2S Wi-Fi Module",
|
||||
"family": FAMILY_BK7231N,
|
||||
},
|
||||
"generic-bk7238": {
|
||||
"name": "Generic - BK7238",
|
||||
"family": FAMILY_BK7238,
|
||||
},
|
||||
"wa2": {
|
||||
"name": "WA2 Wi-Fi Module",
|
||||
"family": FAMILY_BK7231Q,
|
||||
@@ -100,6 +125,10 @@ BK72XX_BOARDS = {
|
||||
"name": "WB3L Wi-Fi Module",
|
||||
"family": FAMILY_BK7231T,
|
||||
},
|
||||
"t1-2s": {
|
||||
"name": "T1-2S Wi-Fi Module",
|
||||
"family": FAMILY_BK7238,
|
||||
},
|
||||
"wb2s": {
|
||||
"name": "WB2S Wi-Fi Module",
|
||||
"family": FAMILY_BK7231T,
|
||||
@@ -158,6 +187,83 @@ BK72XX_BOARD_PINS = {
|
||||
"D12": 22,
|
||||
"A0": 23,
|
||||
},
|
||||
"xh-wb3s": {
|
||||
"SPI0_CS": 15,
|
||||
"SPI0_MISO": 17,
|
||||
"SPI0_MOSI": 16,
|
||||
"SPI0_SCK": 14,
|
||||
"WIRE2_SCL_0": 15,
|
||||
"WIRE2_SCL_1": 24,
|
||||
"WIRE2_SDA_0": 17,
|
||||
"WIRE2_SDA_1": 26,
|
||||
"SERIAL1_RX": 10,
|
||||
"SERIAL1_TX": 11,
|
||||
"SERIAL2_RX": 1,
|
||||
"SERIAL2_TX": 0,
|
||||
"ADC1": 26,
|
||||
"ADC2": 24,
|
||||
"ADC3": 20,
|
||||
"ADC4": 28,
|
||||
"ADC5": 1,
|
||||
"ADC6": 10,
|
||||
"CS": 15,
|
||||
"MISO": 17,
|
||||
"MOSI": 16,
|
||||
"P0": 0,
|
||||
"P1": 1,
|
||||
"P6": 6,
|
||||
"P7": 7,
|
||||
"P8": 8,
|
||||
"P9": 9,
|
||||
"P10": 10,
|
||||
"P11": 11,
|
||||
"P14": 14,
|
||||
"P15": 15,
|
||||
"P16": 16,
|
||||
"P17": 17,
|
||||
"P20": 20,
|
||||
"P21": 21,
|
||||
"P22": 22,
|
||||
"P23": 23,
|
||||
"P24": 24,
|
||||
"P26": 26,
|
||||
"P28": 28,
|
||||
"PWM0": 6,
|
||||
"PWM1": 7,
|
||||
"PWM2": 8,
|
||||
"PWM3": 9,
|
||||
"PWM4": 24,
|
||||
"PWM5": 26,
|
||||
"RX1": 10,
|
||||
"RX2": 1,
|
||||
"SCK": 14,
|
||||
"TX1": 11,
|
||||
"TX2": 0,
|
||||
"D0": 7,
|
||||
"D1": 23,
|
||||
"D2": 14,
|
||||
"D3": 26,
|
||||
"D4": 24,
|
||||
"D5": 6,
|
||||
"D6": 9,
|
||||
"D7": 0,
|
||||
"D8": 1,
|
||||
"D9": 8,
|
||||
"D10": 10,
|
||||
"D11": 11,
|
||||
"D12": 16,
|
||||
"D13": 20,
|
||||
"D14": 21,
|
||||
"D15": 22,
|
||||
"D16": 15,
|
||||
"D17": 17,
|
||||
"A0": 28,
|
||||
"A1": 26,
|
||||
"A2": 24,
|
||||
"A3": 1,
|
||||
"A4": 10,
|
||||
"A5": 20,
|
||||
},
|
||||
"cbu": {
|
||||
"SPI0_CS": 15,
|
||||
"SPI0_MISO": 17,
|
||||
@@ -230,6 +336,204 @@ BK72XX_BOARD_PINS = {
|
||||
"D18": 21,
|
||||
"A0": 23,
|
||||
},
|
||||
"t1-u": {
|
||||
"SPI0_CS": 15,
|
||||
"SPI0_MISO": 17,
|
||||
"SPI0_MOSI": 16,
|
||||
"SPI0_SCK": 14,
|
||||
"WIRE2_SCL_0": 15,
|
||||
"WIRE2_SCL_1": 24,
|
||||
"WIRE2_SDA_0": 17,
|
||||
"WIRE2_SDA_1": 26,
|
||||
"SERIAL1_RX": 10,
|
||||
"SERIAL1_TX": 11,
|
||||
"SERIAL2_RX": 1,
|
||||
"SERIAL2_TX": 0,
|
||||
"ADC1": 26,
|
||||
"ADC2": 24,
|
||||
"ADC3": 20,
|
||||
"ADC4": 28,
|
||||
"ADC5": 1,
|
||||
"ADC6": 10,
|
||||
"CS": 15,
|
||||
"MISO": 17,
|
||||
"MOSI": 16,
|
||||
"P0": 0,
|
||||
"P1": 1,
|
||||
"P6": 6,
|
||||
"P8": 8,
|
||||
"P9": 9,
|
||||
"P10": 10,
|
||||
"P11": 11,
|
||||
"P14": 14,
|
||||
"P15": 15,
|
||||
"P16": 16,
|
||||
"P17": 17,
|
||||
"P20": 20,
|
||||
"P21": 21,
|
||||
"P22": 22,
|
||||
"P23": 23,
|
||||
"P24": 24,
|
||||
"P26": 26,
|
||||
"P28": 28,
|
||||
"PWM0": 6,
|
||||
"PWM2": 8,
|
||||
"PWM3": 9,
|
||||
"PWM4": 24,
|
||||
"PWM5": 26,
|
||||
"RX1": 10,
|
||||
"RX2": 1,
|
||||
"SCK": 14,
|
||||
"TX1": 11,
|
||||
"TX2": 0,
|
||||
"D0": 14,
|
||||
"D1": 16,
|
||||
"D2": 23,
|
||||
"D3": 22,
|
||||
"D4": 20,
|
||||
"D5": 1,
|
||||
"D6": 0,
|
||||
"D7": 24,
|
||||
"D8": 9,
|
||||
"D9": 26,
|
||||
"D10": 6,
|
||||
"D11": 8,
|
||||
"D12": 11,
|
||||
"D13": 10,
|
||||
"D14": 28,
|
||||
"D15": 21,
|
||||
"D16": 17,
|
||||
"D17": 15,
|
||||
"A0": 20,
|
||||
"A1": 1,
|
||||
"A2": 24,
|
||||
"A3": 26,
|
||||
"A4": 10,
|
||||
"A5": 28,
|
||||
},
|
||||
"generic-bk7238-tuya": {
|
||||
"SPI0_CS": 15,
|
||||
"SPI0_MISO": 17,
|
||||
"SPI0_MOSI": 16,
|
||||
"SPI0_SCK": 14,
|
||||
"WIRE2_SCL_0": 15,
|
||||
"WIRE2_SCL_1": 24,
|
||||
"WIRE2_SDA_0": 17,
|
||||
"WIRE2_SDA_1": 26,
|
||||
"SERIAL1_RX": 10,
|
||||
"SERIAL1_TX": 11,
|
||||
"SERIAL2_RX": 1,
|
||||
"SERIAL2_TX": 0,
|
||||
"ADC1": 26,
|
||||
"ADC2": 24,
|
||||
"ADC3": 20,
|
||||
"ADC4": 28,
|
||||
"ADC5": 1,
|
||||
"ADC6": 10,
|
||||
"CS": 15,
|
||||
"MISO": 17,
|
||||
"MOSI": 16,
|
||||
"P0": 0,
|
||||
"P1": 1,
|
||||
"P6": 6,
|
||||
"P7": 7,
|
||||
"P8": 8,
|
||||
"P9": 9,
|
||||
"P10": 10,
|
||||
"P11": 11,
|
||||
"P14": 14,
|
||||
"P15": 15,
|
||||
"P16": 16,
|
||||
"P17": 17,
|
||||
"P20": 20,
|
||||
"P21": 21,
|
||||
"P22": 22,
|
||||
"P23": 23,
|
||||
"P24": 24,
|
||||
"P26": 26,
|
||||
"P28": 28,
|
||||
"PWM0": 6,
|
||||
"PWM1": 7,
|
||||
"PWM2": 8,
|
||||
"PWM3": 9,
|
||||
"PWM4": 24,
|
||||
"PWM5": 26,
|
||||
"RX1": 10,
|
||||
"RX2": 1,
|
||||
"SCK": 14,
|
||||
"TX1": 11,
|
||||
"TX2": 0,
|
||||
"D0": 0,
|
||||
"D1": 1,
|
||||
"D2": 6,
|
||||
"D3": 7,
|
||||
"D4": 8,
|
||||
"D5": 9,
|
||||
"D6": 10,
|
||||
"D7": 11,
|
||||
"D8": 14,
|
||||
"D9": 15,
|
||||
"D10": 16,
|
||||
"D11": 17,
|
||||
"D12": 20,
|
||||
"D13": 21,
|
||||
"D14": 22,
|
||||
"D15": 23,
|
||||
"D16": 24,
|
||||
"D17": 26,
|
||||
"D18": 28,
|
||||
"A0": 1,
|
||||
"A1": 10,
|
||||
"A2": 20,
|
||||
"A3": 24,
|
||||
"A4": 26,
|
||||
"A5": 28,
|
||||
},
|
||||
"t1-m": {
|
||||
"WIRE2_SCL": 24,
|
||||
"WIRE2_SDA": 26,
|
||||
"SERIAL1_RX": 10,
|
||||
"SERIAL1_TX": 11,
|
||||
"SERIAL2_RX": 1,
|
||||
"SERIAL2_TX": 0,
|
||||
"ADC1": 26,
|
||||
"ADC2": 24,
|
||||
"ADC5": 1,
|
||||
"ADC6": 10,
|
||||
"P0": 0,
|
||||
"P1": 1,
|
||||
"P6": 6,
|
||||
"P8": 8,
|
||||
"P9": 9,
|
||||
"P10": 10,
|
||||
"P11": 11,
|
||||
"P24": 24,
|
||||
"P26": 26,
|
||||
"PWM0": 6,
|
||||
"PWM2": 8,
|
||||
"PWM3": 9,
|
||||
"PWM4": 24,
|
||||
"PWM5": 26,
|
||||
"RX1": 10,
|
||||
"RX2": 1,
|
||||
"SCL2": 24,
|
||||
"SDA2": 26,
|
||||
"TX1": 11,
|
||||
"TX2": 0,
|
||||
"D0": 26,
|
||||
"D1": 6,
|
||||
"D2": 8,
|
||||
"D3": 1,
|
||||
"D4": 10,
|
||||
"D5": 11,
|
||||
"D6": 9,
|
||||
"D7": 24,
|
||||
"D11": 0,
|
||||
"A0": 26,
|
||||
"A1": 10,
|
||||
"A2": 1,
|
||||
"A3": 24,
|
||||
},
|
||||
"generic-bk7231t-qfn32-tuya": {
|
||||
"SPI0_CS": 15,
|
||||
"SPI0_MISO": 17,
|
||||
@@ -781,6 +1085,75 @@ BK72XX_BOARD_PINS = {
|
||||
"A6": 12,
|
||||
"A7": 13,
|
||||
},
|
||||
"t1-3s": {
|
||||
"SPI0_CS": 15,
|
||||
"SPI0_MISO": 17,
|
||||
"SPI0_MOSI": 16,
|
||||
"SPI0_SCK": 14,
|
||||
"WIRE2_SCL_0": 15,
|
||||
"WIRE2_SCL_1": 24,
|
||||
"WIRE2_SDA_0": 17,
|
||||
"WIRE2_SDA_1": 26,
|
||||
"SERIAL1_RX": 10,
|
||||
"SERIAL1_TX": 11,
|
||||
"SERIAL2_RX": 1,
|
||||
"SERIAL2_TX": 0,
|
||||
"ADC1": 26,
|
||||
"ADC2": 24,
|
||||
"ADC3": 20,
|
||||
"ADC5": 1,
|
||||
"ADC6": 10,
|
||||
"CS": 15,
|
||||
"MISO": 17,
|
||||
"MOSI": 16,
|
||||
"P0": 0,
|
||||
"P1": 1,
|
||||
"P6": 6,
|
||||
"P8": 8,
|
||||
"P9": 9,
|
||||
"P10": 10,
|
||||
"P11": 11,
|
||||
"P14": 14,
|
||||
"P15": 15,
|
||||
"P16": 16,
|
||||
"P17": 17,
|
||||
"P20": 20,
|
||||
"P22": 22,
|
||||
"P23": 23,
|
||||
"P24": 24,
|
||||
"P26": 26,
|
||||
"PWM0": 6,
|
||||
"PWM2": 8,
|
||||
"PWM3": 9,
|
||||
"PWM4": 24,
|
||||
"PWM5": 26,
|
||||
"RX1": 10,
|
||||
"RX2": 1,
|
||||
"SCK": 14,
|
||||
"TX1": 11,
|
||||
"TX2": 0,
|
||||
"D0": 20,
|
||||
"D1": 22,
|
||||
"D2": 6,
|
||||
"D3": 8,
|
||||
"D4": 9,
|
||||
"D5": 23,
|
||||
"D6": 0,
|
||||
"D7": 1,
|
||||
"D8": 24,
|
||||
"D9": 26,
|
||||
"D10": 10,
|
||||
"D11": 11,
|
||||
"D12": 17,
|
||||
"D13": 16,
|
||||
"D14": 15,
|
||||
"D15": 14,
|
||||
"A0": 20,
|
||||
"A1": 1,
|
||||
"A2": 24,
|
||||
"A3": 26,
|
||||
"A4": 10,
|
||||
},
|
||||
"wb2l": {
|
||||
"WIRE1_SCL": 20,
|
||||
"WIRE1_SDA": 21,
|
||||
@@ -965,6 +1338,84 @@ BK72XX_BOARD_PINS = {
|
||||
"D10": 21,
|
||||
"A0": 23,
|
||||
},
|
||||
"generic-bk7238": {
|
||||
"SPI0_CS": 15,
|
||||
"SPI0_MISO": 17,
|
||||
"SPI0_MOSI": 16,
|
||||
"SPI0_SCK": 14,
|
||||
"WIRE2_SCL_0": 15,
|
||||
"WIRE2_SCL_1": 24,
|
||||
"WIRE2_SDA_0": 17,
|
||||
"WIRE2_SDA_1": 26,
|
||||
"SERIAL1_RX": 10,
|
||||
"SERIAL1_TX": 11,
|
||||
"SERIAL2_RX": 1,
|
||||
"SERIAL2_TX": 0,
|
||||
"ADC1": 26,
|
||||
"ADC2": 24,
|
||||
"ADC3": 20,
|
||||
"ADC4": 28,
|
||||
"ADC5": 1,
|
||||
"ADC6": 10,
|
||||
"CS": 15,
|
||||
"MISO": 17,
|
||||
"MOSI": 16,
|
||||
"P0": 0,
|
||||
"P1": 1,
|
||||
"P6": 6,
|
||||
"P7": 7,
|
||||
"P8": 8,
|
||||
"P9": 9,
|
||||
"P10": 10,
|
||||
"P11": 11,
|
||||
"P14": 14,
|
||||
"P15": 15,
|
||||
"P16": 16,
|
||||
"P17": 17,
|
||||
"P20": 20,
|
||||
"P21": 21,
|
||||
"P22": 22,
|
||||
"P23": 23,
|
||||
"P24": 24,
|
||||
"P26": 26,
|
||||
"P28": 28,
|
||||
"PWM0": 6,
|
||||
"PWM1": 7,
|
||||
"PWM2": 8,
|
||||
"PWM3": 9,
|
||||
"PWM4": 24,
|
||||
"PWM5": 26,
|
||||
"RX1": 10,
|
||||
"RX2": 1,
|
||||
"SCK": 14,
|
||||
"TX1": 11,
|
||||
"TX2": 0,
|
||||
"D0": 0,
|
||||
"D1": 1,
|
||||
"D2": 6,
|
||||
"D3": 7,
|
||||
"D4": 8,
|
||||
"D5": 9,
|
||||
"D6": 10,
|
||||
"D7": 11,
|
||||
"D8": 14,
|
||||
"D9": 15,
|
||||
"D10": 16,
|
||||
"D11": 17,
|
||||
"D12": 20,
|
||||
"D13": 21,
|
||||
"D14": 22,
|
||||
"D15": 23,
|
||||
"D16": 24,
|
||||
"D17": 26,
|
||||
"D18": 28,
|
||||
"A0": 1,
|
||||
"A1": 10,
|
||||
"A2": 20,
|
||||
"A3": 24,
|
||||
"A4": 26,
|
||||
"A5": 28,
|
||||
},
|
||||
"wa2": {
|
||||
"WIRE1_SCL": 20,
|
||||
"WIRE1_SDA": 21,
|
||||
@@ -1235,6 +1686,51 @@ BK72XX_BOARD_PINS = {
|
||||
"D15": 1,
|
||||
"A0": 23,
|
||||
},
|
||||
"t1-2s": {
|
||||
"WIRE2_SCL": 24,
|
||||
"WIRE2_SDA": 26,
|
||||
"SERIAL1_RX": 10,
|
||||
"SERIAL1_TX": 11,
|
||||
"SERIAL2_RX": 1,
|
||||
"SERIAL2_TX": 0,
|
||||
"ADC1": 26,
|
||||
"ADC2": 24,
|
||||
"ADC5": 1,
|
||||
"ADC6": 10,
|
||||
"P0": 0,
|
||||
"P1": 1,
|
||||
"P6": 6,
|
||||
"P8": 8,
|
||||
"P9": 9,
|
||||
"P10": 10,
|
||||
"P11": 11,
|
||||
"P24": 24,
|
||||
"P26": 26,
|
||||
"PWM0": 6,
|
||||
"PWM2": 8,
|
||||
"PWM3": 9,
|
||||
"PWM4": 24,
|
||||
"PWM5": 26,
|
||||
"RX1": 10,
|
||||
"RX2": 1,
|
||||
"SCL2": 24,
|
||||
"SDA2": 26,
|
||||
"TX1": 11,
|
||||
"TX2": 0,
|
||||
"D0": 26,
|
||||
"D1": 6,
|
||||
"D2": 8,
|
||||
"D3": 1,
|
||||
"D4": 10,
|
||||
"D5": 11,
|
||||
"D6": 9,
|
||||
"D7": 24,
|
||||
"D11": 0,
|
||||
"A0": 26,
|
||||
"A1": 10,
|
||||
"A2": 1,
|
||||
"A3": 24,
|
||||
},
|
||||
"wb2s": {
|
||||
"WIRE1_SCL": 20,
|
||||
"WIRE1_SDA": 21,
|
||||
|
||||
@@ -20,58 +20,77 @@ constexpr uint8_t bl0906_checksum(const uint8_t address, const DataPacket *data)
|
||||
}
|
||||
|
||||
void BL0906::loop() {
|
||||
if (this->current_channel_ == UINT8_MAX) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (this->available())
|
||||
this->flush();
|
||||
|
||||
if (this->current_channel_ == 0) {
|
||||
if (this->current_stage_ == STAGE_IDLE) {
|
||||
// Woken up between cycles to drain the action queue. Go back to sleep.
|
||||
this->handle_actions_();
|
||||
this->disable_loop();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->current_stage_ == STAGE_TEMP) {
|
||||
// Temperature
|
||||
this->read_data_(BL0906_TEMPERATURE, BL0906_TREF, this->temperature_sensor_);
|
||||
} else if (this->current_channel_ == 1) {
|
||||
} else if (this->current_stage_ == STAGE_CHANNEL_1) {
|
||||
this->read_data_(BL0906_I_1_RMS, BL0906_IREF, this->current_1_sensor_);
|
||||
this->read_data_(BL0906_WATT_1, BL0906_PREF, this->power_1_sensor_);
|
||||
this->read_data_(BL0906_CF_1_CNT, BL0906_EREF, this->energy_1_sensor_);
|
||||
} else if (this->current_channel_ == 2) {
|
||||
} else if (this->current_stage_ == STAGE_CHANNEL_2) {
|
||||
this->read_data_(BL0906_I_2_RMS, BL0906_IREF, this->current_2_sensor_);
|
||||
this->read_data_(BL0906_WATT_2, BL0906_PREF, this->power_2_sensor_);
|
||||
this->read_data_(BL0906_CF_2_CNT, BL0906_EREF, this->energy_2_sensor_);
|
||||
} else if (this->current_channel_ == 3) {
|
||||
} else if (this->current_stage_ == STAGE_CHANNEL_3) {
|
||||
this->read_data_(BL0906_I_3_RMS, BL0906_IREF, this->current_3_sensor_);
|
||||
this->read_data_(BL0906_WATT_3, BL0906_PREF, this->power_3_sensor_);
|
||||
this->read_data_(BL0906_CF_3_CNT, BL0906_EREF, this->energy_3_sensor_);
|
||||
} else if (this->current_channel_ == 4) {
|
||||
} else if (this->current_stage_ == STAGE_CHANNEL_4) {
|
||||
this->read_data_(BL0906_I_4_RMS, BL0906_IREF, this->current_4_sensor_);
|
||||
this->read_data_(BL0906_WATT_4, BL0906_PREF, this->power_4_sensor_);
|
||||
this->read_data_(BL0906_CF_4_CNT, BL0906_EREF, this->energy_4_sensor_);
|
||||
} else if (this->current_channel_ == 5) {
|
||||
} else if (this->current_stage_ == STAGE_CHANNEL_5) {
|
||||
this->read_data_(BL0906_I_5_RMS, BL0906_IREF, this->current_5_sensor_);
|
||||
this->read_data_(BL0906_WATT_5, BL0906_PREF, this->power_5_sensor_);
|
||||
this->read_data_(BL0906_CF_5_CNT, BL0906_EREF, this->energy_5_sensor_);
|
||||
} else if (this->current_channel_ == 6) {
|
||||
} else if (this->current_stage_ == STAGE_CHANNEL_6) {
|
||||
this->read_data_(BL0906_I_6_RMS, BL0906_IREF, this->current_6_sensor_);
|
||||
this->read_data_(BL0906_WATT_6, BL0906_PREF, this->power_6_sensor_);
|
||||
this->read_data_(BL0906_CF_6_CNT, BL0906_EREF, this->energy_6_sensor_);
|
||||
} else if (this->current_channel_ == UINT8_MAX - 2) {
|
||||
} else if (this->current_stage_ == STAGE_FREQ) {
|
||||
// Frequency
|
||||
this->read_data_(BL0906_FREQUENCY, BL0906_FREF, frequency_sensor_);
|
||||
this->read_data_(BL0906_FREQUENCY, BL0906_FREF, this->frequency_sensor_);
|
||||
// Voltage
|
||||
this->read_data_(BL0906_V_RMS, BL0906_UREF, voltage_sensor_);
|
||||
} else if (this->current_channel_ == UINT8_MAX - 1) {
|
||||
this->read_data_(BL0906_V_RMS, BL0906_UREF, this->voltage_sensor_);
|
||||
} else if (this->current_stage_ == STAGE_POWER) {
|
||||
// Total power
|
||||
this->read_data_(BL0906_WATT_SUM, BL0906_WATT, this->total_power_sensor_);
|
||||
// Total Energy
|
||||
this->read_data_(BL0906_CF_SUM_CNT, BL0906_CF, this->total_energy_sensor_);
|
||||
} else {
|
||||
this->current_channel_ = UINT8_MAX - 2; // Go to frequency and voltage
|
||||
return;
|
||||
}
|
||||
this->current_channel_++;
|
||||
this->advance_stage_();
|
||||
this->handle_actions_();
|
||||
}
|
||||
|
||||
void BL0906::advance_stage_() {
|
||||
switch (this->current_stage_) {
|
||||
case STAGE_CHANNEL_6:
|
||||
this->current_stage_ = STAGE_FREQ;
|
||||
break;
|
||||
case STAGE_FREQ:
|
||||
this->current_stage_ = STAGE_POWER;
|
||||
break;
|
||||
case STAGE_POWER:
|
||||
// Cycle complete; sleep until the next update().
|
||||
this->current_stage_ = STAGE_IDLE;
|
||||
this->disable_loop();
|
||||
break;
|
||||
default:
|
||||
this->current_stage_ = static_cast<BL0906Stage>(this->current_stage_ + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void BL0906::setup() {
|
||||
while (this->available())
|
||||
this->flush();
|
||||
@@ -85,12 +104,20 @@ void BL0906::setup() {
|
||||
this->bias_correction_(BL0906_RMSOS_6, 0.01200, 0); // Calibration current_6
|
||||
|
||||
this->write_array(USR_WRPROT_ONLYREAD, sizeof(USR_WRPROT_ONLYREAD));
|
||||
|
||||
// Loop stays idle until the first update() or enqueued action.
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
void BL0906::update() { this->current_channel_ = 0; }
|
||||
void BL0906::update() {
|
||||
this->current_stage_ = STAGE_TEMP;
|
||||
this->enable_loop();
|
||||
}
|
||||
|
||||
size_t BL0906::enqueue_action_(ActionCallbackFuncPtr function) {
|
||||
this->action_queue_.push_back(function);
|
||||
// Ensure the queue is serviced even if the read cycle has already completed.
|
||||
this->enable_loop();
|
||||
return this->action_queue_.size();
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,22 @@
|
||||
namespace esphome {
|
||||
namespace bl0906 {
|
||||
|
||||
// Stage values for the read state machine. After STAGE_CHANNEL_6 the state machine
|
||||
// jumps to the two sentinel stages below, then to STAGE_IDLE which marks the cycle
|
||||
// as complete and disables the loop.
|
||||
enum BL0906Stage : uint8_t {
|
||||
STAGE_TEMP = 0, // chip temperature
|
||||
STAGE_CHANNEL_1 = 1, // per-phase current + power + energy
|
||||
STAGE_CHANNEL_2 = 2,
|
||||
STAGE_CHANNEL_3 = 3,
|
||||
STAGE_CHANNEL_4 = 4,
|
||||
STAGE_CHANNEL_5 = 5,
|
||||
STAGE_CHANNEL_6 = 6,
|
||||
STAGE_FREQ = UINT8_MAX - 2, // frequency + voltage
|
||||
STAGE_POWER = UINT8_MAX - 1, // total power + total energy
|
||||
STAGE_IDLE = UINT8_MAX, // cycle complete
|
||||
};
|
||||
|
||||
struct DataPacket { // NOLINT(altera-struct-pack-align)
|
||||
uint8_t l{0};
|
||||
uint8_t m{0};
|
||||
@@ -79,7 +95,8 @@ class BL0906 : public PollingComponent, public uart::UARTDevice {
|
||||
|
||||
void bias_correction_(uint8_t address, float measurements, float correction);
|
||||
|
||||
uint8_t current_channel_{0};
|
||||
BL0906Stage current_stage_{STAGE_IDLE};
|
||||
void advance_stage_();
|
||||
size_t enqueue_action_(ActionCallbackFuncPtr function);
|
||||
void handle_actions_();
|
||||
|
||||
|
||||
@@ -30,19 +30,6 @@ void BluetoothProxy::setup() {
|
||||
this->configured_scan_active_ = this->parent_->get_scan_active();
|
||||
|
||||
this->parent_->add_scanner_state_listener(this);
|
||||
|
||||
this->set_interval(100, [this]() {
|
||||
if (api::global_api_server->is_connected() && this->api_connection_ != nullptr) {
|
||||
this->flush_pending_advertisements_();
|
||||
return;
|
||||
}
|
||||
for (uint8_t i = 0; i < this->connection_count_; i++) {
|
||||
auto *connection = this->connections_[i];
|
||||
if (connection->get_address() != 0 && !connection->disconnect_pending()) {
|
||||
connection->disconnect();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void BluetoothProxy::on_scanner_state(esp32_ble_tracker::ScannerState state) {
|
||||
@@ -133,6 +120,25 @@ void BluetoothProxy::dump_config() {
|
||||
YESNO(this->active_), this->connection_count_);
|
||||
}
|
||||
|
||||
void BluetoothProxy::loop() {
|
||||
// Run advertisement flush / connection cleanup every 100ms
|
||||
uint32_t now = App.get_loop_component_start_time();
|
||||
if (now - this->last_advertisement_flush_time_ < 100)
|
||||
return;
|
||||
this->last_advertisement_flush_time_ = now;
|
||||
|
||||
if (api::global_api_server->is_connected() && this->api_connection_ != nullptr) {
|
||||
this->flush_pending_advertisements_();
|
||||
return;
|
||||
}
|
||||
for (uint8_t i = 0; i < this->connection_count_; i++) {
|
||||
auto *connection = this->connections_[i];
|
||||
if (connection->get_address() != 0 && !connection->disconnect_pending()) {
|
||||
connection->disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
esp32_ble_tracker::AdvertisementParserType BluetoothProxy::get_advertisement_parser_type() {
|
||||
return esp32_ble_tracker::AdvertisementParserType::RAW_ADVERTISEMENTS;
|
||||
}
|
||||
@@ -201,7 +207,6 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest
|
||||
connection->set_connection_type(espbt::ConnectionType::V3_WITHOUT_CACHE);
|
||||
this->log_connection_info_(connection, "v3 without cache");
|
||||
}
|
||||
uint64_to_bd_addr(msg.address, connection->remote_bda_);
|
||||
connection->set_remote_addr_type(static_cast<esp_ble_addr_type_t>(msg.address_type));
|
||||
connection->set_state(espbt::ClientState::DISCOVERED);
|
||||
this->send_connections_free();
|
||||
|
||||
@@ -65,6 +65,7 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener,
|
||||
bool parse_devices(const esp32_ble::BLEScanResult *scan_results, size_t count) override;
|
||||
void dump_config() override;
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override;
|
||||
|
||||
void register_connection(BluetoothConnection *connection) {
|
||||
@@ -176,6 +177,9 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener,
|
||||
// BLE advertisement batching
|
||||
api::BluetoothLERawAdvertisementsResponse response_;
|
||||
|
||||
// Group 3: 4-byte types
|
||||
uint32_t last_advertisement_flush_time_{0};
|
||||
|
||||
// Pre-allocated response message - always ready to send
|
||||
api::BluetoothConnectionsFreeResponse connections_free_response_;
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ void BM8563::read_time() {
|
||||
rtc_time.day_of_week, rtc_time.hour, rtc_time.minute, rtc_time.second);
|
||||
|
||||
rtc_time.recalc_timestamp_utc(false);
|
||||
if (!rtc_time.is_valid()) {
|
||||
if (!rtc_time.is_valid(/*check_day_of_week=*/true, /*check_day_of_year=*/false)) {
|
||||
ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -78,43 +78,43 @@ void BME680Component::setup() {
|
||||
}
|
||||
|
||||
// Read calibration
|
||||
uint8_t cal1[25];
|
||||
if (!this->read_bytes(BME680_REGISTER_COEFF1, cal1, 25)) {
|
||||
uint8_t coeff1[25];
|
||||
if (!this->read_bytes(BME680_REGISTER_COEFF1, coeff1, 25)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
uint8_t cal2[16];
|
||||
if (!this->read_bytes(BME680_REGISTER_COEFF2, cal2, 16)) {
|
||||
uint8_t coeff2[16];
|
||||
if (!this->read_bytes(BME680_REGISTER_COEFF2, coeff2, 16)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
this->calibration_.t1 = cal2[9] << 8 | cal2[8];
|
||||
this->calibration_.t2 = cal1[2] << 8 | cal1[1];
|
||||
this->calibration_.t3 = cal1[3];
|
||||
this->calibration_.t1 = coeff2[9] << 8 | coeff2[8];
|
||||
this->calibration_.t2 = coeff1[2] << 8 | coeff1[1];
|
||||
this->calibration_.t3 = coeff1[3];
|
||||
|
||||
this->calibration_.h1 = cal2[2] << 4 | (cal2[1] & 0x0F);
|
||||
this->calibration_.h2 = cal2[0] << 4 | cal2[1] >> 4;
|
||||
this->calibration_.h3 = cal2[3];
|
||||
this->calibration_.h4 = cal2[4];
|
||||
this->calibration_.h5 = cal2[5];
|
||||
this->calibration_.h6 = cal2[6];
|
||||
this->calibration_.h7 = cal2[7];
|
||||
this->calibration_.h1 = coeff2[2] << 4 | (coeff2[1] & 0x0F);
|
||||
this->calibration_.h2 = coeff2[0] << 4 | coeff2[1] >> 4;
|
||||
this->calibration_.h3 = coeff2[3];
|
||||
this->calibration_.h4 = coeff2[4];
|
||||
this->calibration_.h5 = coeff2[5];
|
||||
this->calibration_.h6 = coeff2[6];
|
||||
this->calibration_.h7 = coeff2[7];
|
||||
|
||||
this->calibration_.p1 = cal1[6] << 8 | cal1[5];
|
||||
this->calibration_.p2 = cal1[8] << 8 | cal1[7];
|
||||
this->calibration_.p3 = cal1[9];
|
||||
this->calibration_.p4 = cal1[12] << 8 | cal1[11];
|
||||
this->calibration_.p5 = cal1[14] << 8 | cal1[13];
|
||||
this->calibration_.p6 = cal1[16];
|
||||
this->calibration_.p7 = cal1[15];
|
||||
this->calibration_.p8 = cal1[20] << 8 | cal1[19];
|
||||
this->calibration_.p9 = cal1[22] << 8 | cal1[21];
|
||||
this->calibration_.p10 = cal1[23];
|
||||
this->calibration_.p1 = coeff1[6] << 8 | coeff1[5];
|
||||
this->calibration_.p2 = coeff1[8] << 8 | coeff1[7];
|
||||
this->calibration_.p3 = coeff1[9];
|
||||
this->calibration_.p4 = coeff1[12] << 8 | coeff1[11];
|
||||
this->calibration_.p5 = coeff1[14] << 8 | coeff1[13];
|
||||
this->calibration_.p6 = coeff1[16];
|
||||
this->calibration_.p7 = coeff1[15];
|
||||
this->calibration_.p8 = coeff1[20] << 8 | coeff1[19];
|
||||
this->calibration_.p9 = coeff1[22] << 8 | coeff1[21];
|
||||
this->calibration_.p10 = coeff1[23];
|
||||
|
||||
this->calibration_.gh1 = cal2[14];
|
||||
this->calibration_.gh2 = cal2[12] << 8 | cal2[13];
|
||||
this->calibration_.gh3 = cal2[15];
|
||||
this->calibration_.gh1 = coeff2[14];
|
||||
this->calibration_.gh2 = coeff2[12] << 8 | coeff2[13];
|
||||
this->calibration_.gh3 = coeff2[15];
|
||||
|
||||
uint8_t temp_var = 0;
|
||||
if (!this->read_byte(0x02, &temp_var)) {
|
||||
|
||||
@@ -6,6 +6,7 @@ from esphome.const import CONF_ID, CONF_SAMPLE_RATE, CONF_TEMPERATURE_OFFSET, Fr
|
||||
CODEOWNERS = ["@trvrnrth"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
AUTO_LOAD = ["sensor", "text_sensor"]
|
||||
CONFLICTS_WITH = ["bme68x_bsec2"]
|
||||
MULTI_CONF = True
|
||||
|
||||
CONF_BME680_BSEC_ID = "bme680_bsec_id"
|
||||
|
||||
@@ -13,10 +13,12 @@ from esphome.const import (
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@neffs", "@kbx81"]
|
||||
CONFLICTS_WITH = ["bme680_bsec"]
|
||||
|
||||
DOMAIN = "bme68x_bsec2"
|
||||
|
||||
BSEC2_LIBRARY_VERSION = "1.10.2610"
|
||||
BME68x_LIBRARY_VERSION = "v1.3.40408"
|
||||
|
||||
CONF_ALGORITHM_OUTPUT = "algorithm_output"
|
||||
CONF_BME68X_BSEC2_ID = "bme68x_bsec2_id"
|
||||
@@ -170,7 +172,9 @@ async def to_code_base(config):
|
||||
with open(path, encoding="utf-8") as f:
|
||||
bsec2_iaq_config = f.read()
|
||||
except Exception as e:
|
||||
raise core.EsphomeError(f"Could not open binary configuration file {path}: {e}")
|
||||
raise core.EsphomeError(
|
||||
f"Could not open binary configuration file {path}: {e}"
|
||||
) from e
|
||||
|
||||
# Convert retrieved BSEC2 config to an array of ints
|
||||
rhs = [int(x) for x in bsec2_iaq_config.split(",")]
|
||||
@@ -184,16 +188,31 @@ async def to_code_base(config):
|
||||
if core.CORE.using_arduino:
|
||||
cg.add_library("Wire", None)
|
||||
cg.add_library("SPI", None)
|
||||
cg.add_library(
|
||||
"BME68x Sensor library",
|
||||
None,
|
||||
"https://github.com/boschsensortec/Bosch-BME68x-Library#v1.3.40408",
|
||||
)
|
||||
cg.add_library(
|
||||
"BSEC2 Software Library",
|
||||
None,
|
||||
f"https://github.com/boschsensortec/Bosch-BSEC2-Library.git#{BSEC2_LIBRARY_VERSION}",
|
||||
)
|
||||
|
||||
if core.CORE.is_esp32:
|
||||
from esphome.components.esp32 import add_idf_component
|
||||
|
||||
add_idf_component(
|
||||
name="boschsensortec/Bosch-BME68x-Library",
|
||||
repo="https://github.com/esphome-libs/Bosch-BME68x-Library",
|
||||
ref=BME68x_LIBRARY_VERSION,
|
||||
)
|
||||
add_idf_component(
|
||||
name="boschsensortec/Bosch-BSEC2-Library",
|
||||
repo="https://github.com/esphome-libs/Bosch-BSEC2-Library",
|
||||
ref=BSEC2_LIBRARY_VERSION,
|
||||
)
|
||||
else:
|
||||
cg.add_library(
|
||||
"BME68x Sensor library",
|
||||
None,
|
||||
f"https://github.com/boschsensortec/Bosch-BME68x-Library#{BME68x_LIBRARY_VERSION}",
|
||||
)
|
||||
cg.add_library(
|
||||
"BSEC2 Software Library",
|
||||
None,
|
||||
f"https://github.com/boschsensortec/Bosch-BSEC2-Library.git#{BSEC2_LIBRARY_VERSION}",
|
||||
)
|
||||
|
||||
cg.add_define("USE_BSEC2")
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ from esphome.const import (
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import (
|
||||
entity_duplicate_validator,
|
||||
queue_entity_register,
|
||||
setup_device_class,
|
||||
setup_entity,
|
||||
)
|
||||
@@ -101,7 +102,7 @@ async def setup_button_core_(var, config):
|
||||
async def register_button(var, config):
|
||||
if not CORE.has_id(config[CONF_ID]):
|
||||
var = cg.Pvariable(config[CONF_ID], var)
|
||||
cg.add(cg.App.register_button(var))
|
||||
queue_entity_register("button", config)
|
||||
CORE.register_platform_component("button", var)
|
||||
await setup_button_core_(var, config)
|
||||
|
||||
|
||||
@@ -162,7 +162,6 @@ async def canbus_action_to_code(config, action_id, template_arg, args):
|
||||
await cg.register_parented(var, config[CONF_CANBUS_ID])
|
||||
|
||||
if (can_id := config.get(CONF_CAN_ID)) is not None:
|
||||
can_id = await cg.templatable(can_id, args, cg.uint32)
|
||||
cg.add(var.set_can_id(can_id))
|
||||
cg.add(var.set_use_extended_id(config[CONF_USE_EXTENDED_ID]))
|
||||
|
||||
|
||||
@@ -102,8 +102,34 @@ CC1101Component::CC1101Component() {
|
||||
memset(this->pa_table_, 0, sizeof(this->pa_table_));
|
||||
}
|
||||
|
||||
void IRAM_ATTR CC1101Component::gpio_intr(CC1101Component *arg) { arg->enable_loop_soon_any_context(); }
|
||||
|
||||
void CC1101Component::setup() {
|
||||
this->spi_setup();
|
||||
|
||||
if (this->gdo0_pin_ != nullptr) {
|
||||
this->gdo0_pin_->setup();
|
||||
}
|
||||
|
||||
this->configure();
|
||||
if (this->is_failed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Defer pin mode setup until after all components have completed setup()
|
||||
// This handles the case where remote_transmitter runs after CC1101 and changes pin mode
|
||||
if (this->gdo0_pin_ != nullptr) {
|
||||
this->defer([this]() {
|
||||
this->gdo0_pin_->pin_mode(gpio::FLAG_INPUT);
|
||||
if (this->state_.PKT_FORMAT == static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO)) {
|
||||
this->gdo0_pin_->attach_interrupt(&CC1101Component::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::configure() {
|
||||
// Manual reset sequence per CC1101 datasheet section 19.1.2
|
||||
this->cs_->digital_write(true);
|
||||
delayMicroseconds(1);
|
||||
this->cs_->digital_write(false);
|
||||
@@ -126,11 +152,6 @@ void CC1101Component::setup() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup GDO0 pin if configured
|
||||
if (this->gdo0_pin_ != nullptr) {
|
||||
this->gdo0_pin_->setup();
|
||||
}
|
||||
|
||||
this->initialized_ = true;
|
||||
|
||||
for (uint8_t i = 0; i <= static_cast<uint8_t>(Register::TEST0); i++) {
|
||||
@@ -140,16 +161,11 @@ void CC1101Component::setup() {
|
||||
this->write_(static_cast<Register>(i));
|
||||
}
|
||||
this->set_output_power(this->output_power_requested_);
|
||||
|
||||
if (!this->enter_rx_()) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// Defer pin mode setup until after all components have completed setup()
|
||||
// This handles the case where remote_transmitter runs after CC1101 and changes pin mode
|
||||
if (this->gdo0_pin_ != nullptr) {
|
||||
this->defer([this]() { this->gdo0_pin_->pin_mode(gpio::FLAG_INPUT); });
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::call_listeners_(const std::vector<uint8_t> &packet, float freq_offset, float rssi, uint8_t lqi) {
|
||||
@@ -160,6 +176,7 @@ void CC1101Component::call_listeners_(const std::vector<uint8_t> &packet, float
|
||||
}
|
||||
|
||||
void CC1101Component::loop() {
|
||||
this->disable_loop();
|
||||
if (this->state_.PKT_FORMAT != static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO) || this->gdo0_pin_ == nullptr ||
|
||||
!this->gdo0_pin_->digital_read()) {
|
||||
return;
|
||||
@@ -240,6 +257,7 @@ void CC1101Component::begin_tx() {
|
||||
this->write_(Register::PKTCTRL0, 0x32);
|
||||
ESP_LOGV(TAG, "Beginning TX sequence");
|
||||
if (this->gdo0_pin_ != nullptr) {
|
||||
this->gdo0_pin_->detach_interrupt();
|
||||
this->gdo0_pin_->pin_mode(gpio::FLAG_OUTPUT);
|
||||
}
|
||||
// Transition through IDLE to bypass CCA (Clear Channel Assessment) which can
|
||||
@@ -264,7 +282,7 @@ void CC1101Component::begin_rx() {
|
||||
|
||||
void CC1101Component::reset() {
|
||||
this->strobe_(Command::RES);
|
||||
this->setup();
|
||||
this->configure();
|
||||
}
|
||||
|
||||
void CC1101Component::set_idle() {
|
||||
@@ -669,6 +687,13 @@ void CC1101Component::set_packet_mode(bool value) {
|
||||
this->state_.GDO0_CFG = 0x0D;
|
||||
}
|
||||
if (this->initialized_) {
|
||||
if (this->gdo0_pin_ != nullptr) {
|
||||
if (value) {
|
||||
this->gdo0_pin_->attach_interrupt(&CC1101Component::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE);
|
||||
} else {
|
||||
this->gdo0_pin_->detach_interrupt();
|
||||
}
|
||||
}
|
||||
this->write_(Register::PKTCTRL0);
|
||||
this->write_(Register::PKTCTRL1);
|
||||
this->write_(Register::IOCFG0);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user