mirror of
https://github.com/esphome/esphome.git
synced 2026-02-16 06:19:32 -07:00
Compare commits
269 Commits
pipsolar_t
...
json_web_s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa870483f1 | ||
|
|
840ad30880 | ||
|
|
cfe121b38b | ||
|
|
5fbd9d5b14 | ||
|
|
2b1783ce61 | ||
|
|
904072ce79 | ||
|
|
0a4b98d74a | ||
|
|
823b5ac1ab | ||
|
|
b8017de724 | ||
|
|
ca96604582 | ||
|
|
d18d378f06 | ||
|
|
83e3752544 | ||
|
|
0490b2d450 | ||
|
|
55ff740e4e | ||
|
|
aba8a83cba | ||
|
|
a23809d5db | ||
|
|
32fc3ea6f5 | ||
|
|
deb8ffd348 | ||
|
|
6de2049076 | ||
|
|
cd43f8474e | ||
|
|
ecc0b366b3 | ||
|
|
6a17db8857 | ||
|
|
0843ec6ae8 | ||
|
|
74c84c8747 | ||
|
|
3e9a6c582e | ||
|
|
084113926c | ||
|
|
a5f60750c2 | ||
|
|
a382383d83 | ||
|
|
03cfd87b16 | ||
|
|
6d8294c2d3 | ||
|
|
6a3205f4db | ||
|
|
6f22509883 | ||
|
|
455ade0dca | ||
|
|
87fcfc9d76 | ||
|
|
d86048cc2d | ||
|
|
e1355de4cb | ||
|
|
7385c4cf3d | ||
|
|
3bd6ec4ec7 | ||
|
|
051604f284 | ||
|
|
10dfd95ff2 | ||
|
|
22e0a8ce2e | ||
|
|
b4f63fd992 | ||
|
|
ded835ab63 | ||
|
|
73a249c075 | ||
|
|
fe6f27c526 | ||
|
|
f73c539ea7 | ||
|
|
f87aa384d0 | ||
|
|
f9687a2a31 | ||
|
|
f084d320fc | ||
|
|
f93382445e | ||
|
|
463363a08d | ||
|
|
a0790f926e | ||
|
|
ca59ab8f37 | ||
|
|
b2474c6de9 | ||
|
|
3aaf10b6a8 | ||
|
|
33f545a8e3 | ||
|
|
d056e1040b | ||
|
|
75a78b2bf3 | ||
|
|
cd6314dc96 | ||
|
|
f91bffff9a | ||
|
|
5cbe9af485 | ||
|
|
a7fbecb25c | ||
|
|
bf92d94863 | ||
|
|
9c3817f544 | ||
|
|
ee9e3315b6 | ||
|
|
67dea1e538 | ||
|
|
003b9c6c3f | ||
|
|
2f1a345905 | ||
|
|
7ef933abec | ||
|
|
4ddd40bcfb | ||
|
|
8ae901b3f1 | ||
|
|
bc49174920 | ||
|
|
123ee02d39 | ||
|
|
0cc8055757 | ||
|
|
27a212c14d | ||
|
|
65dc182526 | ||
|
|
dd91039ff1 | ||
|
|
1c9a9c7536 | ||
|
|
011407ea8b | ||
|
|
1141e83a7c | ||
|
|
214ce95cf3 | ||
|
|
3a7b83ba93 | ||
|
|
cc2f3d85dc | ||
|
|
723f67d5e2 | ||
|
|
70e45706d9 | ||
|
|
56a2a2269f | ||
|
|
d6841ba33a | ||
|
|
10cbd0164a | ||
|
|
d285706b41 | ||
|
|
ef469c20df | ||
|
|
6870d3dc50 | ||
|
|
9cc39621a6 | ||
|
|
c4f7d09553 | ||
|
|
ab1661ef22 | ||
|
|
ccbf17d5ab | ||
|
|
bac96086be | ||
|
|
c32e4bc65b | ||
|
|
993765d732 | ||
|
|
8d84fe0113 | ||
|
|
58746b737f | ||
|
|
f93e843972 | ||
|
|
60968d311b | ||
|
|
30584e2e96 | ||
|
|
468ae39a9e | ||
|
|
beb9c8d328 | ||
|
|
cdda3fb7cc | ||
|
|
bba00a3906 | ||
|
|
42e50ca178 | ||
|
|
165e362a1b | ||
|
|
e4763f8e71 | ||
|
|
9fddd0659e | ||
|
|
faea546a0e | ||
|
|
069db2e128 | ||
|
|
5f2203b915 | ||
|
|
5c67e04fef | ||
|
|
0cdcacc7fc | ||
|
|
cfb61bc50a | ||
|
|
547c985672 | ||
|
|
44e624d7a7 | ||
|
|
5779e3e6e4 | ||
|
|
3184717607 | ||
|
|
e8972c65c8 | ||
|
|
71cda05073 | ||
|
|
3dbebb728d | ||
|
|
f938de16af | ||
|
|
ec791063b3 | ||
|
|
fb984cd052 | ||
|
|
85181779d1 | ||
|
|
95b23702e4 | ||
|
|
95eebcd74f | ||
|
|
3c3d5c2fca | ||
|
|
811ac81320 | ||
|
|
f01bd68a4b | ||
|
|
5433c0f707 | ||
|
|
b06cce9eeb | ||
|
|
65bcfee035 | ||
|
|
9261b9ecaa | ||
|
|
6725e6c01e | ||
|
|
effbcece49 | ||
|
|
98a926f37f | ||
|
|
110c173eac | ||
|
|
6008abae62 | ||
|
|
04e102f344 | ||
|
|
bb67b1ca1e | ||
|
|
6d7956a062 | ||
|
|
afbbdd1492 | ||
|
|
b06568c132 | ||
|
|
3c5fc638d5 | ||
|
|
ddb762f8f5 | ||
|
|
4ac7fe84b4 | ||
|
|
d6a41ed51e | ||
|
|
8d1379a275 | ||
|
|
5bbf9153ca | ||
|
|
a1c4d56268 | ||
|
|
a9ce3df04c | ||
|
|
99aa83564e | ||
|
|
aa5092bdc2 | ||
|
|
645832a070 | ||
|
|
19c1d3aee7 | ||
|
|
ce5ec7a78f | ||
|
|
ebf589560d | ||
|
|
8dd1aec606 | ||
|
|
9d967b01c8 | ||
|
|
11e0d536e4 | ||
|
|
673f46f761 | ||
|
|
4abae8d445 | ||
|
|
e62368e058 | ||
|
|
5345c96ff3 | ||
|
|
333ace25c9 | ||
|
|
6014bba3d1 | ||
|
|
5f2394ef80 | ||
|
|
29555c0ddc | ||
|
|
37eaf10f75 | ||
|
|
0b60fd0c8c | ||
|
|
fc16ad806a | ||
|
|
7e43abd86f | ||
|
|
7a2734fae9 | ||
|
|
346f3d38d5 | ||
|
|
fbde91358c | ||
|
|
54d6825323 | ||
|
|
307c3e1061 | ||
|
|
df74d307c8 | ||
|
|
acdc7bd892 | ||
|
|
1095bde2db | ||
|
|
258b73d7f6 | ||
|
|
31608543c2 | ||
|
|
41a060668c | ||
|
|
6bad697fc6 | ||
|
|
3ca5e5e4e4 | ||
|
|
cd4cb8b3ec | ||
|
|
1f3a0490a7 | ||
|
|
b08d871add | ||
|
|
15f0986a59 | ||
|
|
90edf32acf | ||
|
|
3c0f43db9e | ||
|
|
6edecd3d45 | ||
|
|
055c00f1ac | ||
|
|
7dc40881e2 | ||
|
|
b04373687e | ||
|
|
b89c127f62 | ||
|
|
47dc5d0a1f | ||
|
|
21886dd3ac | ||
|
|
85a5a26519 | ||
|
|
79ccacd6d6 | ||
|
|
e2319ba651 | ||
|
|
ed4ebffa74 | ||
|
|
c213de4861 | ||
|
|
6cf320fd60 | ||
|
|
aeea340bc6 | ||
|
|
d0e50ed030 | ||
|
|
280d460025 | ||
|
|
ea70faf642 | ||
|
|
5d7b38b261 | ||
|
|
e88093ca60 | ||
|
|
b48d4ab785 | ||
|
|
8ade9dfc10 | ||
|
|
4e0e7796de | ||
|
|
62b6c9bf7c | ||
|
|
b5fe271d6b | ||
|
|
5d787e2512 | ||
|
|
8998ef0bc3 | ||
|
|
8ec31dd769 | ||
|
|
0193464f92 | ||
|
|
1996bc425f | ||
|
|
a0d3d54d69 | ||
|
|
ee264d0fd4 | ||
|
|
892e9b006f | ||
|
|
f8bd4ef57d | ||
|
|
bfcc0e26a3 | ||
|
|
86a1b4cf69 | ||
|
|
d8a28f6fba | ||
|
|
e80a940222 | ||
|
|
e99dbe05f7 | ||
|
|
f453a8d9a1 | ||
|
|
126190d26a | ||
|
|
e40201a98d | ||
|
|
8142f5db44 | ||
|
|
98ccab87a7 | ||
|
|
b9e72a8774 | ||
|
|
d9fc625c6a | ||
|
|
dfbf79d6d6 | ||
|
|
ea0fac96cb | ||
|
|
3182222d60 | ||
|
|
d8849b16f2 | ||
|
|
635983f163 | ||
|
|
6cbe672004 | ||
|
|
226867b05c | ||
|
|
67871a1683 | ||
|
|
f60c03e350 | ||
|
|
eb66429144 | ||
|
|
0f3bac5dd6 | ||
|
|
5b92d0b89e | ||
|
|
052b05df56 | ||
|
|
7b0db659d1 | ||
|
|
2f7270cf8f | ||
|
|
b44727aee6 | ||
|
|
1a55254258 | ||
|
|
baf2b0e3c9 | ||
|
|
680e92a226 | ||
|
|
db0b32bfc9 | ||
|
|
21794e28e5 | ||
|
|
728236270c | ||
|
|
01cdc4ed58 | ||
|
|
d6a0c8ffbb | ||
|
|
4cc0f874f7 | ||
|
|
ed58b9372f | ||
|
|
ee2a81923b | ||
|
|
0a1e7ee50b | ||
|
|
4d4283bcfa |
@@ -1 +1 @@
|
|||||||
d272a88e8ca28ae9340a9a03295a566432a52cb696501908f57764475bf7ca65
|
cf3d341206b4184ec8b7fe85141aef4fe4696aa720c3f8a06d4e57930574bdab
|
||||||
|
|||||||
4
.github/actions/restore-python/action.yml
vendored
4
.github/actions/restore-python/action.yml
vendored
@@ -17,12 +17,12 @@ runs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Set up Python ${{ inputs.python-version }}
|
- name: Set up Python ${{ inputs.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ inputs.python-version }}
|
python-version: ${{ inputs.python-version }}
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
# yamllint disable-line rule:line-length
|
# yamllint disable-line rule:line-length
|
||||||
|
|||||||
38
.github/scripts/auto-label-pr/constants.js
vendored
Normal file
38
.github/scripts/auto-label-pr/constants.js
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
// Constants and markers for PR auto-labeling
|
||||||
|
module.exports = {
|
||||||
|
BOT_COMMENT_MARKER: '<!-- auto-label-pr-bot -->',
|
||||||
|
CODEOWNERS_MARKER: '<!-- codeowners-request -->',
|
||||||
|
TOO_BIG_MARKER: '<!-- too-big-request -->',
|
||||||
|
DEPRECATED_COMPONENT_MARKER: '<!-- deprecated-component-request -->',
|
||||||
|
|
||||||
|
MANAGED_LABELS: [
|
||||||
|
'new-component',
|
||||||
|
'new-platform',
|
||||||
|
'new-target-platform',
|
||||||
|
'merging-to-release',
|
||||||
|
'merging-to-beta',
|
||||||
|
'chained-pr',
|
||||||
|
'core',
|
||||||
|
'small-pr',
|
||||||
|
'dashboard',
|
||||||
|
'github-actions',
|
||||||
|
'by-code-owner',
|
||||||
|
'has-tests',
|
||||||
|
'needs-tests',
|
||||||
|
'needs-docs',
|
||||||
|
'needs-codeowners',
|
||||||
|
'too-big',
|
||||||
|
'labeller-recheck',
|
||||||
|
'bugfix',
|
||||||
|
'new-feature',
|
||||||
|
'breaking-change',
|
||||||
|
'developer-breaking-change',
|
||||||
|
'code-quality',
|
||||||
|
'deprecated-component'
|
||||||
|
],
|
||||||
|
|
||||||
|
DOCS_PR_PATTERNS: [
|
||||||
|
/https:\/\/github\.com\/esphome\/esphome-docs\/pull\/\d+/,
|
||||||
|
/esphome\/esphome-docs#\d+/
|
||||||
|
]
|
||||||
|
};
|
||||||
373
.github/scripts/auto-label-pr/detectors.js
vendored
Normal file
373
.github/scripts/auto-label-pr/detectors.js
vendored
Normal file
@@ -0,0 +1,373 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const { DOCS_PR_PATTERNS } = require('./constants');
|
||||||
|
|
||||||
|
// Strategy: Merge branch detection
|
||||||
|
async function detectMergeBranch(context) {
|
||||||
|
const labels = new Set();
|
||||||
|
const baseRef = context.payload.pull_request.base.ref;
|
||||||
|
|
||||||
|
if (baseRef === 'release') {
|
||||||
|
labels.add('merging-to-release');
|
||||||
|
} else if (baseRef === 'beta') {
|
||||||
|
labels.add('merging-to-beta');
|
||||||
|
} else if (baseRef !== 'dev') {
|
||||||
|
labels.add('chained-pr');
|
||||||
|
}
|
||||||
|
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy: Component and platform labeling
|
||||||
|
async function detectComponentPlatforms(changedFiles, apiData) {
|
||||||
|
const labels = new Set();
|
||||||
|
const componentRegex = /^esphome\/components\/([^\/]+)\//;
|
||||||
|
const targetPlatformRegex = new RegExp(`^esphome\/components\/(${apiData.targetPlatforms.join('|')})/`);
|
||||||
|
|
||||||
|
for (const file of changedFiles) {
|
||||||
|
const componentMatch = file.match(componentRegex);
|
||||||
|
if (componentMatch) {
|
||||||
|
labels.add(`component: ${componentMatch[1]}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const platformMatch = file.match(targetPlatformRegex);
|
||||||
|
if (platformMatch) {
|
||||||
|
labels.add(`platform: ${platformMatch[1]}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy: New component detection
|
||||||
|
async function detectNewComponents(prFiles) {
|
||||||
|
const labels = new Set();
|
||||||
|
const addedFiles = prFiles.filter(file => file.status === 'added').map(file => file.filename);
|
||||||
|
|
||||||
|
for (const file of addedFiles) {
|
||||||
|
const componentMatch = file.match(/^esphome\/components\/([^\/]+)\/__init__\.py$/);
|
||||||
|
if (componentMatch) {
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync(file, 'utf8');
|
||||||
|
if (content.includes('IS_TARGET_PLATFORM = True')) {
|
||||||
|
labels.add('new-target-platform');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`Failed to read content of ${file}:`, error.message);
|
||||||
|
}
|
||||||
|
labels.add('new-component');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy: New platform detection
|
||||||
|
async function detectNewPlatforms(prFiles, apiData) {
|
||||||
|
const labels = new Set();
|
||||||
|
const addedFiles = prFiles.filter(file => file.status === 'added').map(file => file.filename);
|
||||||
|
|
||||||
|
for (const file of addedFiles) {
|
||||||
|
const platformFileMatch = file.match(/^esphome\/components\/([^\/]+)\/([^\/]+)\.py$/);
|
||||||
|
if (platformFileMatch) {
|
||||||
|
const [, component, platform] = platformFileMatch;
|
||||||
|
if (apiData.platformComponents.includes(platform)) {
|
||||||
|
labels.add('new-platform');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const platformDirMatch = file.match(/^esphome\/components\/([^\/]+)\/([^\/]+)\/__init__\.py$/);
|
||||||
|
if (platformDirMatch) {
|
||||||
|
const [, component, platform] = platformDirMatch;
|
||||||
|
if (apiData.platformComponents.includes(platform)) {
|
||||||
|
labels.add('new-platform');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy: Core files detection
|
||||||
|
async function detectCoreChanges(changedFiles) {
|
||||||
|
const labels = new Set();
|
||||||
|
const coreFiles = changedFiles.filter(file =>
|
||||||
|
file.startsWith('esphome/core/') ||
|
||||||
|
(file.startsWith('esphome/') && file.split('/').length === 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (coreFiles.length > 0) {
|
||||||
|
labels.add('core');
|
||||||
|
}
|
||||||
|
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy: PR size detection
|
||||||
|
async function detectPRSize(prFiles, totalAdditions, totalDeletions, totalChanges, isMegaPR, SMALL_PR_THRESHOLD, TOO_BIG_THRESHOLD) {
|
||||||
|
const labels = new Set();
|
||||||
|
|
||||||
|
if (totalChanges <= SMALL_PR_THRESHOLD) {
|
||||||
|
labels.add('small-pr');
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
|
||||||
|
const testAdditions = prFiles
|
||||||
|
.filter(file => file.filename.startsWith('tests/'))
|
||||||
|
.reduce((sum, file) => sum + (file.additions || 0), 0);
|
||||||
|
const testDeletions = prFiles
|
||||||
|
.filter(file => file.filename.startsWith('tests/'))
|
||||||
|
.reduce((sum, file) => sum + (file.deletions || 0), 0);
|
||||||
|
|
||||||
|
const nonTestChanges = (totalAdditions - testAdditions) - (totalDeletions - testDeletions);
|
||||||
|
|
||||||
|
// Don't add too-big if mega-pr label is already present
|
||||||
|
if (nonTestChanges > TOO_BIG_THRESHOLD && !isMegaPR) {
|
||||||
|
labels.add('too-big');
|
||||||
|
}
|
||||||
|
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy: Dashboard changes
|
||||||
|
async function detectDashboardChanges(changedFiles) {
|
||||||
|
const labels = new Set();
|
||||||
|
const dashboardFiles = changedFiles.filter(file =>
|
||||||
|
file.startsWith('esphome/dashboard/') ||
|
||||||
|
file.startsWith('esphome/components/dashboard_import/')
|
||||||
|
);
|
||||||
|
|
||||||
|
if (dashboardFiles.length > 0) {
|
||||||
|
labels.add('dashboard');
|
||||||
|
}
|
||||||
|
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy: GitHub Actions changes
|
||||||
|
async function detectGitHubActionsChanges(changedFiles) {
|
||||||
|
const labels = new Set();
|
||||||
|
const githubActionsFiles = changedFiles.filter(file =>
|
||||||
|
file.startsWith('.github/workflows/')
|
||||||
|
);
|
||||||
|
|
||||||
|
if (githubActionsFiles.length > 0) {
|
||||||
|
labels.add('github-actions');
|
||||||
|
}
|
||||||
|
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy: Code owner detection
|
||||||
|
async function detectCodeOwner(github, context, changedFiles) {
|
||||||
|
const labels = new Set();
|
||||||
|
const { owner, repo } = context.repo;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data: codeownersFile } = await github.rest.repos.getContent({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
path: 'CODEOWNERS',
|
||||||
|
});
|
||||||
|
|
||||||
|
const codeownersContent = Buffer.from(codeownersFile.content, 'base64').toString('utf8');
|
||||||
|
const prAuthor = context.payload.pull_request.user.login;
|
||||||
|
|
||||||
|
const codeownersLines = codeownersContent.split('\n')
|
||||||
|
.map(line => line.trim())
|
||||||
|
.filter(line => line && !line.startsWith('#'));
|
||||||
|
|
||||||
|
const codeownersRegexes = codeownersLines.map(line => {
|
||||||
|
const parts = line.split(/\s+/);
|
||||||
|
const pattern = parts[0];
|
||||||
|
const owners = parts.slice(1);
|
||||||
|
|
||||||
|
let regex;
|
||||||
|
if (pattern.endsWith('*')) {
|
||||||
|
const dir = pattern.slice(0, -1);
|
||||||
|
regex = new RegExp(`^${dir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`);
|
||||||
|
} else if (pattern.includes('*')) {
|
||||||
|
// First escape all regex special chars except *, then replace * with .*
|
||||||
|
const regexPattern = pattern
|
||||||
|
.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
|
||||||
|
.replace(/\*/g, '.*');
|
||||||
|
regex = new RegExp(`^${regexPattern}$`);
|
||||||
|
} else {
|
||||||
|
regex = new RegExp(`^${pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { regex, owners };
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const file of changedFiles) {
|
||||||
|
for (const { regex, owners } of codeownersRegexes) {
|
||||||
|
if (regex.test(file) && owners.some(owner => owner === `@${prAuthor}`)) {
|
||||||
|
labels.add('by-code-owner');
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Failed to read or parse CODEOWNERS file:', error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy: Test detection
|
||||||
|
async function detectTests(changedFiles) {
|
||||||
|
const labels = new Set();
|
||||||
|
const testFiles = changedFiles.filter(file => file.startsWith('tests/'));
|
||||||
|
|
||||||
|
if (testFiles.length > 0) {
|
||||||
|
labels.add('has-tests');
|
||||||
|
}
|
||||||
|
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy: PR Template Checkbox detection
|
||||||
|
async function detectPRTemplateCheckboxes(context) {
|
||||||
|
const labels = new Set();
|
||||||
|
const prBody = context.payload.pull_request.body || '';
|
||||||
|
|
||||||
|
console.log('Checking PR template checkboxes...');
|
||||||
|
|
||||||
|
// Check for checked checkboxes in the "Types of changes" section
|
||||||
|
const checkboxPatterns = [
|
||||||
|
{ pattern: /- \[x\] Bugfix \(non-breaking change which fixes an issue\)/i, label: 'bugfix' },
|
||||||
|
{ pattern: /- \[x\] New feature \(non-breaking change which adds functionality\)/i, label: 'new-feature' },
|
||||||
|
{ pattern: /- \[x\] Breaking change \(fix or feature that would cause existing functionality to not work as expected\)/i, label: 'breaking-change' },
|
||||||
|
{ pattern: /- \[x\] Developer breaking change \(an API change that could break external components\)/i, label: 'developer-breaking-change' },
|
||||||
|
{ pattern: /- \[x\] Code quality improvements to existing code or addition of tests/i, label: 'code-quality' }
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const { pattern, label } of checkboxPatterns) {
|
||||||
|
if (pattern.test(prBody)) {
|
||||||
|
console.log(`Found checked checkbox for: ${label}`);
|
||||||
|
labels.add(label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy: Deprecated component detection
|
||||||
|
async function detectDeprecatedComponents(github, context, changedFiles) {
|
||||||
|
const labels = new Set();
|
||||||
|
const deprecatedInfo = [];
|
||||||
|
const { owner, repo } = context.repo;
|
||||||
|
|
||||||
|
// Compile regex once for better performance
|
||||||
|
const componentFileRegex = /^esphome\/components\/([^\/]+)\//;
|
||||||
|
|
||||||
|
// Get files that are modified or added in components directory
|
||||||
|
const componentFiles = changedFiles.filter(file => componentFileRegex.test(file));
|
||||||
|
|
||||||
|
if (componentFiles.length === 0) {
|
||||||
|
return { labels, deprecatedInfo };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract unique component names using the same regex
|
||||||
|
const components = new Set();
|
||||||
|
for (const file of componentFiles) {
|
||||||
|
const match = file.match(componentFileRegex);
|
||||||
|
if (match) {
|
||||||
|
components.add(match[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get PR head to fetch files from the PR branch
|
||||||
|
const prNumber = context.payload.pull_request.number;
|
||||||
|
|
||||||
|
// Check each component's __init__.py for DEPRECATED_COMPONENT constant
|
||||||
|
for (const component of components) {
|
||||||
|
const initFile = `esphome/components/${component}/__init__.py`;
|
||||||
|
try {
|
||||||
|
// Fetch file content from PR head using GitHub API
|
||||||
|
const { data: fileData } = await github.rest.repos.getContent({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
path: initFile,
|
||||||
|
ref: `refs/pull/${prNumber}/head`
|
||||||
|
});
|
||||||
|
|
||||||
|
// Decode base64 content
|
||||||
|
const content = Buffer.from(fileData.content, 'base64').toString('utf8');
|
||||||
|
|
||||||
|
// Look for DEPRECATED_COMPONENT = "message" or DEPRECATED_COMPONENT = 'message'
|
||||||
|
// Support single quotes, double quotes, and triple quotes (for multiline)
|
||||||
|
const doubleQuoteMatch = content.match(/DEPRECATED_COMPONENT\s*=\s*"""([\s\S]*?)"""/s) ||
|
||||||
|
content.match(/DEPRECATED_COMPONENT\s*=\s*"((?:[^"\\]|\\.)*)"/);
|
||||||
|
const singleQuoteMatch = content.match(/DEPRECATED_COMPONENT\s*=\s*'''([\s\S]*?)'''/s) ||
|
||||||
|
content.match(/DEPRECATED_COMPONENT\s*=\s*'((?:[^'\\]|\\.)*)'/);
|
||||||
|
const deprecatedMatch = doubleQuoteMatch || singleQuoteMatch;
|
||||||
|
|
||||||
|
if (deprecatedMatch) {
|
||||||
|
labels.add('deprecated-component');
|
||||||
|
deprecatedInfo.push({
|
||||||
|
component: component,
|
||||||
|
message: deprecatedMatch[1].trim()
|
||||||
|
});
|
||||||
|
console.log(`Found deprecated component: ${component}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Only log if it's not a simple "file not found" error (404)
|
||||||
|
if (error.status !== 404) {
|
||||||
|
console.log(`Error reading ${initFile}:`, error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { labels, deprecatedInfo };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy: Requirements detection
|
||||||
|
async function detectRequirements(allLabels, prFiles, context) {
|
||||||
|
const labels = new Set();
|
||||||
|
|
||||||
|
// Check for missing tests
|
||||||
|
if ((allLabels.has('new-component') || allLabels.has('new-platform') || allLabels.has('new-feature')) && !allLabels.has('has-tests')) {
|
||||||
|
labels.add('needs-tests');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for missing docs
|
||||||
|
if (allLabels.has('new-component') || allLabels.has('new-platform') || allLabels.has('new-feature')) {
|
||||||
|
const prBody = context.payload.pull_request.body || '';
|
||||||
|
const hasDocsLink = DOCS_PR_PATTERNS.some(pattern => pattern.test(prBody));
|
||||||
|
|
||||||
|
if (!hasDocsLink) {
|
||||||
|
labels.add('needs-docs');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for missing CODEOWNERS
|
||||||
|
if (allLabels.has('new-component')) {
|
||||||
|
const codeownersModified = prFiles.some(file =>
|
||||||
|
file.filename === 'CODEOWNERS' &&
|
||||||
|
(file.status === 'modified' || file.status === 'added') &&
|
||||||
|
(file.additions || 0) > 0
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!codeownersModified) {
|
||||||
|
labels.add('needs-codeowners');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
detectMergeBranch,
|
||||||
|
detectComponentPlatforms,
|
||||||
|
detectNewComponents,
|
||||||
|
detectNewPlatforms,
|
||||||
|
detectCoreChanges,
|
||||||
|
detectPRSize,
|
||||||
|
detectDashboardChanges,
|
||||||
|
detectGitHubActionsChanges,
|
||||||
|
detectCodeOwner,
|
||||||
|
detectTests,
|
||||||
|
detectPRTemplateCheckboxes,
|
||||||
|
detectDeprecatedComponents,
|
||||||
|
detectRequirements
|
||||||
|
};
|
||||||
187
.github/scripts/auto-label-pr/index.js
vendored
Normal file
187
.github/scripts/auto-label-pr/index.js
vendored
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
const { MANAGED_LABELS } = require('./constants');
|
||||||
|
const {
|
||||||
|
detectMergeBranch,
|
||||||
|
detectComponentPlatforms,
|
||||||
|
detectNewComponents,
|
||||||
|
detectNewPlatforms,
|
||||||
|
detectCoreChanges,
|
||||||
|
detectPRSize,
|
||||||
|
detectDashboardChanges,
|
||||||
|
detectGitHubActionsChanges,
|
||||||
|
detectCodeOwner,
|
||||||
|
detectTests,
|
||||||
|
detectPRTemplateCheckboxes,
|
||||||
|
detectDeprecatedComponents,
|
||||||
|
detectRequirements
|
||||||
|
} = require('./detectors');
|
||||||
|
const { handleReviews } = require('./reviews');
|
||||||
|
const { applyLabels, removeOldLabels } = require('./labels');
|
||||||
|
|
||||||
|
// Fetch API data
|
||||||
|
async function fetchApiData() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('https://data.esphome.io/components.json');
|
||||||
|
const componentsData = await response.json();
|
||||||
|
return {
|
||||||
|
targetPlatforms: componentsData.target_platforms || [],
|
||||||
|
platformComponents: componentsData.platform_components || []
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Failed to fetch components data from API:', error.message);
|
||||||
|
return { targetPlatforms: [], platformComponents: [] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = async ({ github, context }) => {
|
||||||
|
// Environment variables
|
||||||
|
const SMALL_PR_THRESHOLD = parseInt(process.env.SMALL_PR_THRESHOLD);
|
||||||
|
const MAX_LABELS = parseInt(process.env.MAX_LABELS);
|
||||||
|
const TOO_BIG_THRESHOLD = parseInt(process.env.TOO_BIG_THRESHOLD);
|
||||||
|
const COMPONENT_LABEL_THRESHOLD = parseInt(process.env.COMPONENT_LABEL_THRESHOLD);
|
||||||
|
|
||||||
|
// Global state
|
||||||
|
const { owner, repo } = context.repo;
|
||||||
|
const pr_number = context.issue.number;
|
||||||
|
|
||||||
|
// Get current labels and PR data
|
||||||
|
const { data: currentLabelsData } = await github.rest.issues.listLabelsOnIssue({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issue_number: pr_number
|
||||||
|
});
|
||||||
|
const currentLabels = currentLabelsData.map(label => label.name);
|
||||||
|
const managedLabels = currentLabels.filter(label =>
|
||||||
|
label.startsWith('component: ') || MANAGED_LABELS.includes(label)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check for mega-PR early - if present, skip most automatic labeling
|
||||||
|
const isMegaPR = currentLabels.includes('mega-pr');
|
||||||
|
|
||||||
|
// Get all PR files with automatic pagination
|
||||||
|
const prFiles = await github.paginate(
|
||||||
|
github.rest.pulls.listFiles,
|
||||||
|
{
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
pull_number: pr_number
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calculate data from PR files
|
||||||
|
const changedFiles = prFiles.map(file => file.filename);
|
||||||
|
const totalAdditions = prFiles.reduce((sum, file) => sum + (file.additions || 0), 0);
|
||||||
|
const totalDeletions = prFiles.reduce((sum, file) => sum + (file.deletions || 0), 0);
|
||||||
|
const totalChanges = totalAdditions + totalDeletions;
|
||||||
|
|
||||||
|
console.log('Current labels:', currentLabels.join(', '));
|
||||||
|
console.log('Changed files:', changedFiles.length);
|
||||||
|
console.log('Total changes:', totalChanges);
|
||||||
|
if (isMegaPR) {
|
||||||
|
console.log('Mega-PR detected - applying limited labeling logic');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch API data
|
||||||
|
const apiData = await fetchApiData();
|
||||||
|
const baseRef = context.payload.pull_request.base.ref;
|
||||||
|
|
||||||
|
// Early exit for release and beta branches only
|
||||||
|
if (baseRef === 'release' || baseRef === 'beta') {
|
||||||
|
const branchLabels = await detectMergeBranch(context);
|
||||||
|
const finalLabels = Array.from(branchLabels);
|
||||||
|
|
||||||
|
console.log('Computed labels (merge branch only):', finalLabels.join(', '));
|
||||||
|
|
||||||
|
// Apply labels
|
||||||
|
await applyLabels(github, context, finalLabels);
|
||||||
|
|
||||||
|
// Remove old managed labels
|
||||||
|
await removeOldLabels(github, context, managedLabels, finalLabels);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run all strategies
|
||||||
|
const [
|
||||||
|
branchLabels,
|
||||||
|
componentLabels,
|
||||||
|
newComponentLabels,
|
||||||
|
newPlatformLabels,
|
||||||
|
coreLabels,
|
||||||
|
sizeLabels,
|
||||||
|
dashboardLabels,
|
||||||
|
actionsLabels,
|
||||||
|
codeOwnerLabels,
|
||||||
|
testLabels,
|
||||||
|
checkboxLabels,
|
||||||
|
deprecatedResult
|
||||||
|
] = await Promise.all([
|
||||||
|
detectMergeBranch(context),
|
||||||
|
detectComponentPlatforms(changedFiles, apiData),
|
||||||
|
detectNewComponents(prFiles),
|
||||||
|
detectNewPlatforms(prFiles, apiData),
|
||||||
|
detectCoreChanges(changedFiles),
|
||||||
|
detectPRSize(prFiles, totalAdditions, totalDeletions, totalChanges, isMegaPR, SMALL_PR_THRESHOLD, TOO_BIG_THRESHOLD),
|
||||||
|
detectDashboardChanges(changedFiles),
|
||||||
|
detectGitHubActionsChanges(changedFiles),
|
||||||
|
detectCodeOwner(github, context, changedFiles),
|
||||||
|
detectTests(changedFiles),
|
||||||
|
detectPRTemplateCheckboxes(context),
|
||||||
|
detectDeprecatedComponents(github, context, changedFiles)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Extract deprecated component info
|
||||||
|
const deprecatedLabels = deprecatedResult.labels;
|
||||||
|
const deprecatedInfo = deprecatedResult.deprecatedInfo;
|
||||||
|
|
||||||
|
// Combine all labels
|
||||||
|
const allLabels = new Set([
|
||||||
|
...branchLabels,
|
||||||
|
...componentLabels,
|
||||||
|
...newComponentLabels,
|
||||||
|
...newPlatformLabels,
|
||||||
|
...coreLabels,
|
||||||
|
...sizeLabels,
|
||||||
|
...dashboardLabels,
|
||||||
|
...actionsLabels,
|
||||||
|
...codeOwnerLabels,
|
||||||
|
...testLabels,
|
||||||
|
...checkboxLabels,
|
||||||
|
...deprecatedLabels
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Detect requirements based on all other labels
|
||||||
|
const requirementLabels = await detectRequirements(allLabels, prFiles, context);
|
||||||
|
for (const label of requirementLabels) {
|
||||||
|
allLabels.add(label);
|
||||||
|
}
|
||||||
|
|
||||||
|
let finalLabels = Array.from(allLabels);
|
||||||
|
|
||||||
|
// For mega-PRs, exclude component labels if there are too many
|
||||||
|
if (isMegaPR) {
|
||||||
|
const componentLabels = finalLabels.filter(label => label.startsWith('component: '));
|
||||||
|
if (componentLabels.length > COMPONENT_LABEL_THRESHOLD) {
|
||||||
|
finalLabels = finalLabels.filter(label => !label.startsWith('component: '));
|
||||||
|
console.log(`Mega-PR detected - excluding ${componentLabels.length} component labels (threshold: ${COMPONENT_LABEL_THRESHOLD})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle too many labels (only for non-mega PRs)
|
||||||
|
const tooManyLabels = finalLabels.length > MAX_LABELS;
|
||||||
|
const originalLabelCount = finalLabels.length;
|
||||||
|
|
||||||
|
if (tooManyLabels && !isMegaPR && !finalLabels.includes('too-big')) {
|
||||||
|
finalLabels = ['too-big'];
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Computed labels:', finalLabels.join(', '));
|
||||||
|
|
||||||
|
// Handle reviews
|
||||||
|
await handleReviews(github, context, finalLabels, originalLabelCount, deprecatedInfo, prFiles, totalAdditions, totalDeletions, MAX_LABELS, TOO_BIG_THRESHOLD);
|
||||||
|
|
||||||
|
// Apply labels
|
||||||
|
await applyLabels(github, context, finalLabels);
|
||||||
|
|
||||||
|
// Remove old managed labels
|
||||||
|
await removeOldLabels(github, context, managedLabels, finalLabels);
|
||||||
|
};
|
||||||
41
.github/scripts/auto-label-pr/labels.js
vendored
Normal file
41
.github/scripts/auto-label-pr/labels.js
vendored
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
// Apply labels to PR
|
||||||
|
async function applyLabels(github, context, finalLabels) {
|
||||||
|
const { owner, repo } = context.repo;
|
||||||
|
const pr_number = context.issue.number;
|
||||||
|
|
||||||
|
if (finalLabels.length > 0) {
|
||||||
|
console.log(`Adding labels: ${finalLabels.join(', ')}`);
|
||||||
|
await github.rest.issues.addLabels({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issue_number: pr_number,
|
||||||
|
labels: finalLabels
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove old managed labels
|
||||||
|
async function removeOldLabels(github, context, managedLabels, finalLabels) {
|
||||||
|
const { owner, repo } = context.repo;
|
||||||
|
const pr_number = context.issue.number;
|
||||||
|
|
||||||
|
const labelsToRemove = managedLabels.filter(label => !finalLabels.includes(label));
|
||||||
|
for (const label of labelsToRemove) {
|
||||||
|
console.log(`Removing label: ${label}`);
|
||||||
|
try {
|
||||||
|
await github.rest.issues.removeLabel({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issue_number: pr_number,
|
||||||
|
name: label
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`Failed to remove label ${label}:`, error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
applyLabels,
|
||||||
|
removeOldLabels
|
||||||
|
};
|
||||||
141
.github/scripts/auto-label-pr/reviews.js
vendored
Normal file
141
.github/scripts/auto-label-pr/reviews.js
vendored
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
const {
|
||||||
|
BOT_COMMENT_MARKER,
|
||||||
|
CODEOWNERS_MARKER,
|
||||||
|
TOO_BIG_MARKER,
|
||||||
|
DEPRECATED_COMPONENT_MARKER
|
||||||
|
} = require('./constants');
|
||||||
|
|
||||||
|
// Generate review messages
|
||||||
|
function generateReviewMessages(finalLabels, originalLabelCount, deprecatedInfo, prFiles, totalAdditions, totalDeletions, prAuthor, MAX_LABELS, TOO_BIG_THRESHOLD) {
|
||||||
|
const messages = [];
|
||||||
|
|
||||||
|
// Deprecated component message
|
||||||
|
if (finalLabels.includes('deprecated-component') && deprecatedInfo && deprecatedInfo.length > 0) {
|
||||||
|
let message = `${DEPRECATED_COMPONENT_MARKER}\n### ⚠️ Deprecated Component\n\n`;
|
||||||
|
message += `Hey there @${prAuthor},\n`;
|
||||||
|
message += `This PR modifies one or more deprecated components. Please be aware:\n\n`;
|
||||||
|
|
||||||
|
for (const info of deprecatedInfo) {
|
||||||
|
message += `#### Component: \`${info.component}\`\n`;
|
||||||
|
message += `${info.message}\n\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
message += `Consider migrating to the recommended alternative if applicable.`;
|
||||||
|
|
||||||
|
messages.push(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Too big message
|
||||||
|
if (finalLabels.includes('too-big')) {
|
||||||
|
const testAdditions = prFiles
|
||||||
|
.filter(file => file.filename.startsWith('tests/'))
|
||||||
|
.reduce((sum, file) => sum + (file.additions || 0), 0);
|
||||||
|
const testDeletions = prFiles
|
||||||
|
.filter(file => file.filename.startsWith('tests/'))
|
||||||
|
.reduce((sum, file) => sum + (file.deletions || 0), 0);
|
||||||
|
const nonTestChanges = (totalAdditions - testAdditions) - (totalDeletions - testDeletions);
|
||||||
|
|
||||||
|
const tooManyLabels = originalLabelCount > MAX_LABELS;
|
||||||
|
const tooManyChanges = nonTestChanges > TOO_BIG_THRESHOLD;
|
||||||
|
|
||||||
|
let message = `${TOO_BIG_MARKER}\n### 📦 Pull Request Size\n\n`;
|
||||||
|
|
||||||
|
if (tooManyLabels && tooManyChanges) {
|
||||||
|
message += `This PR is too large with ${nonTestChanges} line changes (excluding tests) and affects ${originalLabelCount} different components/areas.`;
|
||||||
|
} else if (tooManyLabels) {
|
||||||
|
message += `This PR affects ${originalLabelCount} different components/areas.`;
|
||||||
|
} else {
|
||||||
|
message += `This PR is too large with ${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`;
|
||||||
|
|
||||||
|
messages.push(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// CODEOWNERS message
|
||||||
|
if (finalLabels.includes('needs-codeowners')) {
|
||||||
|
const message = `${CODEOWNERS_MARKER}\n### 👥 Code Ownership\n\n` +
|
||||||
|
`Hey there @${prAuthor},\n` +
|
||||||
|
`Thanks for submitting this pull request! Can you add yourself as a codeowner for this integration? ` +
|
||||||
|
`This way we can notify you if a bug report for this integration is reported.\n\n` +
|
||||||
|
`In \`__init__.py\` of the integration, please add:\n\n` +
|
||||||
|
`\`\`\`python\nCODEOWNERS = ["@${prAuthor}"]\n\`\`\`\n\n` +
|
||||||
|
`And run \`script/build_codeowners.py\``;
|
||||||
|
|
||||||
|
messages.push(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle reviews
|
||||||
|
async function handleReviews(github, context, finalLabels, originalLabelCount, deprecatedInfo, prFiles, totalAdditions, totalDeletions, MAX_LABELS, TOO_BIG_THRESHOLD) {
|
||||||
|
const { owner, repo } = context.repo;
|
||||||
|
const pr_number = context.issue.number;
|
||||||
|
const prAuthor = context.payload.pull_request.user.login;
|
||||||
|
|
||||||
|
const reviewMessages = generateReviewMessages(finalLabels, originalLabelCount, deprecatedInfo, prFiles, totalAdditions, totalDeletions, prAuthor, MAX_LABELS, TOO_BIG_THRESHOLD);
|
||||||
|
const hasReviewableLabels = finalLabels.some(label =>
|
||||||
|
['too-big', 'needs-codeowners', 'deprecated-component'].includes(label)
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: reviews } = await github.rest.pulls.listReviews({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
pull_number: pr_number
|
||||||
|
});
|
||||||
|
|
||||||
|
const botReviews = reviews.filter(review =>
|
||||||
|
review.user.type === 'Bot' &&
|
||||||
|
review.state === 'CHANGES_REQUESTED' &&
|
||||||
|
review.body && review.body.includes(BOT_COMMENT_MARKER)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasReviewableLabels) {
|
||||||
|
const reviewBody = `${BOT_COMMENT_MARKER}\n\n${reviewMessages.join('\n\n---\n\n')}`;
|
||||||
|
|
||||||
|
if (botReviews.length > 0) {
|
||||||
|
// Update existing review
|
||||||
|
await github.rest.pulls.updateReview({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
pull_number: pr_number,
|
||||||
|
review_id: botReviews[0].id,
|
||||||
|
body: reviewBody
|
||||||
|
});
|
||||||
|
console.log('Updated existing bot review');
|
||||||
|
} else {
|
||||||
|
// Create new review
|
||||||
|
await github.rest.pulls.createReview({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
pull_number: pr_number,
|
||||||
|
body: reviewBody,
|
||||||
|
event: 'REQUEST_CHANGES'
|
||||||
|
});
|
||||||
|
console.log('Created new bot review');
|
||||||
|
}
|
||||||
|
} else if (botReviews.length > 0) {
|
||||||
|
// Dismiss existing reviews
|
||||||
|
for (const review of botReviews) {
|
||||||
|
try {
|
||||||
|
await github.rest.pulls.dismissReview({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
pull_number: pr_number,
|
||||||
|
review_id: review.id,
|
||||||
|
message: 'Review dismissed: All requirements have been met'
|
||||||
|
});
|
||||||
|
console.log(`Dismissed bot review ${review.id}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`Failed to dismiss review ${review.id}:`, error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
handleReviews
|
||||||
|
};
|
||||||
634
.github/workflows/auto-label-pr.yml
vendored
634
.github/workflows/auto-label-pr.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
|||||||
if: github.event.action != 'labeled' || github.event.sender.type != 'Bot'
|
if: github.event.action != 'labeled' || github.event.sender.type != 'Bot'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Generate a token
|
- name: Generate a token
|
||||||
id: generate-token
|
id: generate-token
|
||||||
@@ -36,633 +36,5 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
github-token: ${{ steps.generate-token.outputs.token }}
|
github-token: ${{ steps.generate-token.outputs.token }}
|
||||||
script: |
|
script: |
|
||||||
const fs = require('fs');
|
const script = require('./.github/scripts/auto-label-pr/index.js');
|
||||||
|
await script({ github, context });
|
||||||
// Constants
|
|
||||||
const SMALL_PR_THRESHOLD = parseInt('${{ env.SMALL_PR_THRESHOLD }}');
|
|
||||||
const MAX_LABELS = parseInt('${{ env.MAX_LABELS }}');
|
|
||||||
const TOO_BIG_THRESHOLD = parseInt('${{ env.TOO_BIG_THRESHOLD }}');
|
|
||||||
const COMPONENT_LABEL_THRESHOLD = parseInt('${{ env.COMPONENT_LABEL_THRESHOLD }}');
|
|
||||||
const BOT_COMMENT_MARKER = '<!-- auto-label-pr-bot -->';
|
|
||||||
const CODEOWNERS_MARKER = '<!-- codeowners-request -->';
|
|
||||||
const TOO_BIG_MARKER = '<!-- too-big-request -->';
|
|
||||||
|
|
||||||
const MANAGED_LABELS = [
|
|
||||||
'new-component',
|
|
||||||
'new-platform',
|
|
||||||
'new-target-platform',
|
|
||||||
'merging-to-release',
|
|
||||||
'merging-to-beta',
|
|
||||||
'chained-pr',
|
|
||||||
'core',
|
|
||||||
'small-pr',
|
|
||||||
'dashboard',
|
|
||||||
'github-actions',
|
|
||||||
'by-code-owner',
|
|
||||||
'has-tests',
|
|
||||||
'needs-tests',
|
|
||||||
'needs-docs',
|
|
||||||
'needs-codeowners',
|
|
||||||
'too-big',
|
|
||||||
'labeller-recheck',
|
|
||||||
'bugfix',
|
|
||||||
'new-feature',
|
|
||||||
'breaking-change',
|
|
||||||
'developer-breaking-change',
|
|
||||||
'code-quality'
|
|
||||||
];
|
|
||||||
|
|
||||||
const DOCS_PR_PATTERNS = [
|
|
||||||
/https:\/\/github\.com\/esphome\/esphome-docs\/pull\/\d+/,
|
|
||||||
/esphome\/esphome-docs#\d+/
|
|
||||||
];
|
|
||||||
|
|
||||||
// Global state
|
|
||||||
const { owner, repo } = context.repo;
|
|
||||||
const pr_number = context.issue.number;
|
|
||||||
|
|
||||||
// Get current labels and PR data
|
|
||||||
const { data: currentLabelsData } = await github.rest.issues.listLabelsOnIssue({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
issue_number: pr_number
|
|
||||||
});
|
|
||||||
const currentLabels = currentLabelsData.map(label => label.name);
|
|
||||||
const managedLabels = currentLabels.filter(label =>
|
|
||||||
label.startsWith('component: ') || MANAGED_LABELS.includes(label)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check for mega-PR early - if present, skip most automatic labeling
|
|
||||||
const isMegaPR = currentLabels.includes('mega-pr');
|
|
||||||
|
|
||||||
// Get all PR files with automatic pagination
|
|
||||||
const prFiles = await github.paginate(
|
|
||||||
github.rest.pulls.listFiles,
|
|
||||||
{
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
pull_number: pr_number
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Calculate data from PR files
|
|
||||||
const changedFiles = prFiles.map(file => file.filename);
|
|
||||||
const totalAdditions = prFiles.reduce((sum, file) => sum + (file.additions || 0), 0);
|
|
||||||
const totalDeletions = prFiles.reduce((sum, file) => sum + (file.deletions || 0), 0);
|
|
||||||
const totalChanges = totalAdditions + totalDeletions;
|
|
||||||
|
|
||||||
console.log('Current labels:', currentLabels.join(', '));
|
|
||||||
console.log('Changed files:', changedFiles.length);
|
|
||||||
console.log('Total changes:', totalChanges);
|
|
||||||
if (isMegaPR) {
|
|
||||||
console.log('Mega-PR detected - applying limited labeling logic');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch API data
|
|
||||||
async function fetchApiData() {
|
|
||||||
try {
|
|
||||||
const response = await fetch('https://data.esphome.io/components.json');
|
|
||||||
const componentsData = await response.json();
|
|
||||||
return {
|
|
||||||
targetPlatforms: componentsData.target_platforms || [],
|
|
||||||
platformComponents: componentsData.platform_components || []
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Failed to fetch components data from API:', error.message);
|
|
||||||
return { targetPlatforms: [], platformComponents: [] };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strategy: Merge branch detection
|
|
||||||
async function detectMergeBranch() {
|
|
||||||
const labels = new Set();
|
|
||||||
const baseRef = context.payload.pull_request.base.ref;
|
|
||||||
|
|
||||||
if (baseRef === 'release') {
|
|
||||||
labels.add('merging-to-release');
|
|
||||||
} else if (baseRef === 'beta') {
|
|
||||||
labels.add('merging-to-beta');
|
|
||||||
} else if (baseRef !== 'dev') {
|
|
||||||
labels.add('chained-pr');
|
|
||||||
}
|
|
||||||
|
|
||||||
return labels;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strategy: Component and platform labeling
|
|
||||||
async function detectComponentPlatforms(apiData) {
|
|
||||||
const labels = new Set();
|
|
||||||
const componentRegex = /^esphome\/components\/([^\/]+)\//;
|
|
||||||
const targetPlatformRegex = new RegExp(`^esphome\/components\/(${apiData.targetPlatforms.join('|')})/`);
|
|
||||||
|
|
||||||
for (const file of changedFiles) {
|
|
||||||
const componentMatch = file.match(componentRegex);
|
|
||||||
if (componentMatch) {
|
|
||||||
labels.add(`component: ${componentMatch[1]}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const platformMatch = file.match(targetPlatformRegex);
|
|
||||||
if (platformMatch) {
|
|
||||||
labels.add(`platform: ${platformMatch[1]}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return labels;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strategy: New component detection
|
|
||||||
async function detectNewComponents() {
|
|
||||||
const labels = new Set();
|
|
||||||
const addedFiles = prFiles.filter(file => file.status === 'added').map(file => file.filename);
|
|
||||||
|
|
||||||
for (const file of addedFiles) {
|
|
||||||
const componentMatch = file.match(/^esphome\/components\/([^\/]+)\/__init__\.py$/);
|
|
||||||
if (componentMatch) {
|
|
||||||
try {
|
|
||||||
const content = fs.readFileSync(file, 'utf8');
|
|
||||||
if (content.includes('IS_TARGET_PLATFORM = True')) {
|
|
||||||
labels.add('new-target-platform');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`Failed to read content of ${file}:`, error.message);
|
|
||||||
}
|
|
||||||
labels.add('new-component');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return labels;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strategy: New platform detection
|
|
||||||
async function detectNewPlatforms(apiData) {
|
|
||||||
const labels = new Set();
|
|
||||||
const addedFiles = prFiles.filter(file => file.status === 'added').map(file => file.filename);
|
|
||||||
|
|
||||||
for (const file of addedFiles) {
|
|
||||||
const platformFileMatch = file.match(/^esphome\/components\/([^\/]+)\/([^\/]+)\.py$/);
|
|
||||||
if (platformFileMatch) {
|
|
||||||
const [, component, platform] = platformFileMatch;
|
|
||||||
if (apiData.platformComponents.includes(platform)) {
|
|
||||||
labels.add('new-platform');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const platformDirMatch = file.match(/^esphome\/components\/([^\/]+)\/([^\/]+)\/__init__\.py$/);
|
|
||||||
if (platformDirMatch) {
|
|
||||||
const [, component, platform] = platformDirMatch;
|
|
||||||
if (apiData.platformComponents.includes(platform)) {
|
|
||||||
labels.add('new-platform');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return labels;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strategy: Core files detection
|
|
||||||
async function detectCoreChanges() {
|
|
||||||
const labels = new Set();
|
|
||||||
const coreFiles = changedFiles.filter(file =>
|
|
||||||
file.startsWith('esphome/core/') ||
|
|
||||||
(file.startsWith('esphome/') && file.split('/').length === 2)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (coreFiles.length > 0) {
|
|
||||||
labels.add('core');
|
|
||||||
}
|
|
||||||
|
|
||||||
return labels;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strategy: PR size detection
|
|
||||||
async function detectPRSize() {
|
|
||||||
const labels = new Set();
|
|
||||||
|
|
||||||
if (totalChanges <= SMALL_PR_THRESHOLD) {
|
|
||||||
labels.add('small-pr');
|
|
||||||
return labels;
|
|
||||||
}
|
|
||||||
|
|
||||||
const testAdditions = prFiles
|
|
||||||
.filter(file => file.filename.startsWith('tests/'))
|
|
||||||
.reduce((sum, file) => sum + (file.additions || 0), 0);
|
|
||||||
const testDeletions = prFiles
|
|
||||||
.filter(file => file.filename.startsWith('tests/'))
|
|
||||||
.reduce((sum, file) => sum + (file.deletions || 0), 0);
|
|
||||||
|
|
||||||
const nonTestChanges = (totalAdditions - testAdditions) - (totalDeletions - testDeletions);
|
|
||||||
|
|
||||||
// Don't add too-big if mega-pr label is already present
|
|
||||||
if (nonTestChanges > TOO_BIG_THRESHOLD && !isMegaPR) {
|
|
||||||
labels.add('too-big');
|
|
||||||
}
|
|
||||||
|
|
||||||
return labels;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strategy: Dashboard changes
|
|
||||||
async function detectDashboardChanges() {
|
|
||||||
const labels = new Set();
|
|
||||||
const dashboardFiles = changedFiles.filter(file =>
|
|
||||||
file.startsWith('esphome/dashboard/') ||
|
|
||||||
file.startsWith('esphome/components/dashboard_import/')
|
|
||||||
);
|
|
||||||
|
|
||||||
if (dashboardFiles.length > 0) {
|
|
||||||
labels.add('dashboard');
|
|
||||||
}
|
|
||||||
|
|
||||||
return labels;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strategy: GitHub Actions changes
|
|
||||||
async function detectGitHubActionsChanges() {
|
|
||||||
const labels = new Set();
|
|
||||||
const githubActionsFiles = changedFiles.filter(file =>
|
|
||||||
file.startsWith('.github/workflows/')
|
|
||||||
);
|
|
||||||
|
|
||||||
if (githubActionsFiles.length > 0) {
|
|
||||||
labels.add('github-actions');
|
|
||||||
}
|
|
||||||
|
|
||||||
return labels;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strategy: Code owner detection
|
|
||||||
async function detectCodeOwner() {
|
|
||||||
const labels = new Set();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { data: codeownersFile } = await github.rest.repos.getContent({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
path: 'CODEOWNERS',
|
|
||||||
});
|
|
||||||
|
|
||||||
const codeownersContent = Buffer.from(codeownersFile.content, 'base64').toString('utf8');
|
|
||||||
const prAuthor = context.payload.pull_request.user.login;
|
|
||||||
|
|
||||||
const codeownersLines = codeownersContent.split('\n')
|
|
||||||
.map(line => line.trim())
|
|
||||||
.filter(line => line && !line.startsWith('#'));
|
|
||||||
|
|
||||||
const codeownersRegexes = codeownersLines.map(line => {
|
|
||||||
const parts = line.split(/\s+/);
|
|
||||||
const pattern = parts[0];
|
|
||||||
const owners = parts.slice(1);
|
|
||||||
|
|
||||||
let regex;
|
|
||||||
if (pattern.endsWith('*')) {
|
|
||||||
const dir = pattern.slice(0, -1);
|
|
||||||
regex = new RegExp(`^${dir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`);
|
|
||||||
} else if (pattern.includes('*')) {
|
|
||||||
// First escape all regex special chars except *, then replace * with .*
|
|
||||||
const regexPattern = pattern
|
|
||||||
.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
|
|
||||||
.replace(/\*/g, '.*');
|
|
||||||
regex = new RegExp(`^${regexPattern}$`);
|
|
||||||
} else {
|
|
||||||
regex = new RegExp(`^${pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { regex, owners };
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const file of changedFiles) {
|
|
||||||
for (const { regex, owners } of codeownersRegexes) {
|
|
||||||
if (regex.test(file) && owners.some(owner => owner === `@${prAuthor}`)) {
|
|
||||||
labels.add('by-code-owner');
|
|
||||||
return labels;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Failed to read or parse CODEOWNERS file:', error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
return labels;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strategy: Test detection
|
|
||||||
async function detectTests() {
|
|
||||||
const labels = new Set();
|
|
||||||
const testFiles = changedFiles.filter(file => file.startsWith('tests/'));
|
|
||||||
|
|
||||||
if (testFiles.length > 0) {
|
|
||||||
labels.add('has-tests');
|
|
||||||
}
|
|
||||||
|
|
||||||
return labels;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strategy: PR Template Checkbox detection
|
|
||||||
async function detectPRTemplateCheckboxes() {
|
|
||||||
const labels = new Set();
|
|
||||||
const prBody = context.payload.pull_request.body || '';
|
|
||||||
|
|
||||||
console.log('Checking PR template checkboxes...');
|
|
||||||
|
|
||||||
// Check for checked checkboxes in the "Types of changes" section
|
|
||||||
const checkboxPatterns = [
|
|
||||||
{ pattern: /- \[x\] Bugfix \(non-breaking change which fixes an issue\)/i, label: 'bugfix' },
|
|
||||||
{ pattern: /- \[x\] New feature \(non-breaking change which adds functionality\)/i, label: 'new-feature' },
|
|
||||||
{ pattern: /- \[x\] Breaking change \(fix or feature that would cause existing functionality to not work as expected\)/i, label: 'breaking-change' },
|
|
||||||
{ pattern: /- \[x\] Developer breaking change \(an API change that could break external components\)/i, label: 'developer-breaking-change' },
|
|
||||||
{ pattern: /- \[x\] Code quality improvements to existing code or addition of tests/i, label: 'code-quality' }
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const { pattern, label } of checkboxPatterns) {
|
|
||||||
if (pattern.test(prBody)) {
|
|
||||||
console.log(`Found checked checkbox for: ${label}`);
|
|
||||||
labels.add(label);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return labels;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strategy: Requirements detection
|
|
||||||
async function detectRequirements(allLabels) {
|
|
||||||
const labels = new Set();
|
|
||||||
|
|
||||||
// Check for missing tests
|
|
||||||
if ((allLabels.has('new-component') || allLabels.has('new-platform') || allLabels.has('new-feature')) && !allLabels.has('has-tests')) {
|
|
||||||
labels.add('needs-tests');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for missing docs
|
|
||||||
if (allLabels.has('new-component') || allLabels.has('new-platform') || allLabels.has('new-feature')) {
|
|
||||||
const prBody = context.payload.pull_request.body || '';
|
|
||||||
const hasDocsLink = DOCS_PR_PATTERNS.some(pattern => pattern.test(prBody));
|
|
||||||
|
|
||||||
if (!hasDocsLink) {
|
|
||||||
labels.add('needs-docs');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for missing CODEOWNERS
|
|
||||||
if (allLabels.has('new-component')) {
|
|
||||||
const codeownersModified = prFiles.some(file =>
|
|
||||||
file.filename === 'CODEOWNERS' &&
|
|
||||||
(file.status === 'modified' || file.status === 'added') &&
|
|
||||||
(file.additions || 0) > 0
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!codeownersModified) {
|
|
||||||
labels.add('needs-codeowners');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return labels;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate review messages
|
|
||||||
function generateReviewMessages(finalLabels, originalLabelCount) {
|
|
||||||
const messages = [];
|
|
||||||
const prAuthor = context.payload.pull_request.user.login;
|
|
||||||
|
|
||||||
// Too big message
|
|
||||||
if (finalLabels.includes('too-big')) {
|
|
||||||
const testAdditions = prFiles
|
|
||||||
.filter(file => file.filename.startsWith('tests/'))
|
|
||||||
.reduce((sum, file) => sum + (file.additions || 0), 0);
|
|
||||||
const testDeletions = prFiles
|
|
||||||
.filter(file => file.filename.startsWith('tests/'))
|
|
||||||
.reduce((sum, file) => sum + (file.deletions || 0), 0);
|
|
||||||
const nonTestChanges = (totalAdditions - testAdditions) - (totalDeletions - testDeletions);
|
|
||||||
|
|
||||||
const tooManyLabels = originalLabelCount > MAX_LABELS;
|
|
||||||
const tooManyChanges = nonTestChanges > TOO_BIG_THRESHOLD;
|
|
||||||
|
|
||||||
let message = `${TOO_BIG_MARKER}\n### 📦 Pull Request Size\n\n`;
|
|
||||||
|
|
||||||
if (tooManyLabels && tooManyChanges) {
|
|
||||||
message += `This PR is too large with ${nonTestChanges} line changes (excluding tests) and affects ${originalLabelCount} different components/areas.`;
|
|
||||||
} else if (tooManyLabels) {
|
|
||||||
message += `This PR affects ${originalLabelCount} different components/areas.`;
|
|
||||||
} else {
|
|
||||||
message += `This PR is too large with ${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`;
|
|
||||||
|
|
||||||
messages.push(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// CODEOWNERS message
|
|
||||||
if (finalLabels.includes('needs-codeowners')) {
|
|
||||||
const message = `${CODEOWNERS_MARKER}\n### 👥 Code Ownership\n\n` +
|
|
||||||
`Hey there @${prAuthor},\n` +
|
|
||||||
`Thanks for submitting this pull request! Can you add yourself as a codeowner for this integration? ` +
|
|
||||||
`This way we can notify you if a bug report for this integration is reported.\n\n` +
|
|
||||||
`In \`__init__.py\` of the integration, please add:\n\n` +
|
|
||||||
`\`\`\`python\nCODEOWNERS = ["@${prAuthor}"]\n\`\`\`\n\n` +
|
|
||||||
`And run \`script/build_codeowners.py\``;
|
|
||||||
|
|
||||||
messages.push(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
return messages;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle reviews
|
|
||||||
async function handleReviews(finalLabels, originalLabelCount) {
|
|
||||||
const reviewMessages = generateReviewMessages(finalLabels, originalLabelCount);
|
|
||||||
const hasReviewableLabels = finalLabels.some(label =>
|
|
||||||
['too-big', 'needs-codeowners'].includes(label)
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: reviews } = await github.rest.pulls.listReviews({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
pull_number: pr_number
|
|
||||||
});
|
|
||||||
|
|
||||||
const botReviews = reviews.filter(review =>
|
|
||||||
review.user.type === 'Bot' &&
|
|
||||||
review.state === 'CHANGES_REQUESTED' &&
|
|
||||||
review.body && review.body.includes(BOT_COMMENT_MARKER)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (hasReviewableLabels) {
|
|
||||||
const reviewBody = `${BOT_COMMENT_MARKER}\n\n${reviewMessages.join('\n\n---\n\n')}`;
|
|
||||||
|
|
||||||
if (botReviews.length > 0) {
|
|
||||||
// Update existing review
|
|
||||||
await github.rest.pulls.updateReview({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
pull_number: pr_number,
|
|
||||||
review_id: botReviews[0].id,
|
|
||||||
body: reviewBody
|
|
||||||
});
|
|
||||||
console.log('Updated existing bot review');
|
|
||||||
} else {
|
|
||||||
// Create new review
|
|
||||||
await github.rest.pulls.createReview({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
pull_number: pr_number,
|
|
||||||
body: reviewBody,
|
|
||||||
event: 'REQUEST_CHANGES'
|
|
||||||
});
|
|
||||||
console.log('Created new bot review');
|
|
||||||
}
|
|
||||||
} else if (botReviews.length > 0) {
|
|
||||||
// Dismiss existing reviews
|
|
||||||
for (const review of botReviews) {
|
|
||||||
try {
|
|
||||||
await github.rest.pulls.dismissReview({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
pull_number: pr_number,
|
|
||||||
review_id: review.id,
|
|
||||||
message: 'Review dismissed: All requirements have been met'
|
|
||||||
});
|
|
||||||
console.log(`Dismissed bot review ${review.id}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`Failed to dismiss review ${review.id}:`, error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Main execution
|
|
||||||
const apiData = await fetchApiData();
|
|
||||||
const baseRef = context.payload.pull_request.base.ref;
|
|
||||||
|
|
||||||
// Early exit for release and beta branches only
|
|
||||||
if (baseRef === 'release' || baseRef === 'beta') {
|
|
||||||
const branchLabels = await detectMergeBranch();
|
|
||||||
const finalLabels = Array.from(branchLabels);
|
|
||||||
|
|
||||||
console.log('Computed labels (merge branch only):', finalLabels.join(', '));
|
|
||||||
|
|
||||||
// Apply labels
|
|
||||||
if (finalLabels.length > 0) {
|
|
||||||
await github.rest.issues.addLabels({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
issue_number: pr_number,
|
|
||||||
labels: finalLabels
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove old managed labels
|
|
||||||
const labelsToRemove = managedLabels.filter(label => !finalLabels.includes(label));
|
|
||||||
for (const label of labelsToRemove) {
|
|
||||||
try {
|
|
||||||
await github.rest.issues.removeLabel({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
issue_number: pr_number,
|
|
||||||
name: label
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`Failed to remove label ${label}:`, error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run all strategies
|
|
||||||
const [
|
|
||||||
branchLabels,
|
|
||||||
componentLabels,
|
|
||||||
newComponentLabels,
|
|
||||||
newPlatformLabels,
|
|
||||||
coreLabels,
|
|
||||||
sizeLabels,
|
|
||||||
dashboardLabels,
|
|
||||||
actionsLabels,
|
|
||||||
codeOwnerLabels,
|
|
||||||
testLabels,
|
|
||||||
checkboxLabels
|
|
||||||
] = await Promise.all([
|
|
||||||
detectMergeBranch(),
|
|
||||||
detectComponentPlatforms(apiData),
|
|
||||||
detectNewComponents(),
|
|
||||||
detectNewPlatforms(apiData),
|
|
||||||
detectCoreChanges(),
|
|
||||||
detectPRSize(),
|
|
||||||
detectDashboardChanges(),
|
|
||||||
detectGitHubActionsChanges(),
|
|
||||||
detectCodeOwner(),
|
|
||||||
detectTests(),
|
|
||||||
detectPRTemplateCheckboxes()
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Combine all labels
|
|
||||||
const allLabels = new Set([
|
|
||||||
...branchLabels,
|
|
||||||
...componentLabels,
|
|
||||||
...newComponentLabels,
|
|
||||||
...newPlatformLabels,
|
|
||||||
...coreLabels,
|
|
||||||
...sizeLabels,
|
|
||||||
...dashboardLabels,
|
|
||||||
...actionsLabels,
|
|
||||||
...codeOwnerLabels,
|
|
||||||
...testLabels,
|
|
||||||
...checkboxLabels
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Detect requirements based on all other labels
|
|
||||||
const requirementLabels = await detectRequirements(allLabels);
|
|
||||||
for (const label of requirementLabels) {
|
|
||||||
allLabels.add(label);
|
|
||||||
}
|
|
||||||
|
|
||||||
let finalLabels = Array.from(allLabels);
|
|
||||||
|
|
||||||
// For mega-PRs, exclude component labels if there are too many
|
|
||||||
if (isMegaPR) {
|
|
||||||
const componentLabels = finalLabels.filter(label => label.startsWith('component: '));
|
|
||||||
if (componentLabels.length > COMPONENT_LABEL_THRESHOLD) {
|
|
||||||
finalLabels = finalLabels.filter(label => !label.startsWith('component: '));
|
|
||||||
console.log(`Mega-PR detected - excluding ${componentLabels.length} component labels (threshold: ${COMPONENT_LABEL_THRESHOLD})`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle too many labels (only for non-mega PRs)
|
|
||||||
const tooManyLabels = finalLabels.length > MAX_LABELS;
|
|
||||||
const originalLabelCount = finalLabels.length;
|
|
||||||
|
|
||||||
if (tooManyLabels && !isMegaPR && !finalLabels.includes('too-big')) {
|
|
||||||
finalLabels = ['too-big'];
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Computed labels:', finalLabels.join(', '));
|
|
||||||
|
|
||||||
// Handle reviews
|
|
||||||
await handleReviews(finalLabels, originalLabelCount);
|
|
||||||
|
|
||||||
// Apply labels
|
|
||||||
if (finalLabels.length > 0) {
|
|
||||||
console.log(`Adding labels: ${finalLabels.join(', ')}`);
|
|
||||||
await github.rest.issues.addLabels({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
issue_number: pr_number,
|
|
||||||
labels: finalLabels
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove old managed labels
|
|
||||||
const labelsToRemove = managedLabels.filter(label => !finalLabels.includes(label));
|
|
||||||
for (const label of labelsToRemove) {
|
|
||||||
console.log(`Removing label: ${label}`);
|
|
||||||
try {
|
|
||||||
await github.rest.issues.removeLabel({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
issue_number: pr_number,
|
|
||||||
name: label
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`Failed to remove label ${label}:`, error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
4
.github/workflows/ci-api-proto.yml
vendored
4
.github/workflows/ci-api-proto.yml
vendored
@@ -21,9 +21,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
|
|
||||||
|
|||||||
4
.github/workflows/ci-clang-tidy-hash.yml
vendored
4
.github/workflows/ci-clang-tidy-hash.yml
vendored
@@ -21,10 +21,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
|
|
||||||
|
|||||||
4
.github/workflows/ci-docker.yml
vendored
4
.github/workflows/ci-docker.yml
vendored
@@ -43,9 +43,9 @@ jobs:
|
|||||||
- "docker"
|
- "docker"
|
||||||
# - "lint"
|
# - "lint"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Check out code from base repository
|
- name: Check out code from base repository
|
||||||
if: steps.pr.outputs.skip != 'true'
|
if: steps.pr.outputs.skip != 'true'
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
# Always check out from the base repository (esphome/esphome), never from forks
|
# Always check out from the base repository (esphome/esphome), never from forks
|
||||||
# Use the PR's target branch to ensure we run trusted code from the main repo
|
# Use the PR's target branch to ensure we run trusted code from the main repo
|
||||||
|
|||||||
64
.github/workflows/ci.yml
vendored
64
.github/workflows/ci.yml
vendored
@@ -36,18 +36,18 @@ jobs:
|
|||||||
cache-key: ${{ steps.cache-key.outputs.key }}
|
cache-key: ${{ steps.cache-key.outputs.key }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Generate cache-key
|
- name: Generate cache-key
|
||||||
id: 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_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
# yamllint disable-line rule:line-length
|
# yamllint disable-line rule:line-length
|
||||||
@@ -70,7 +70,7 @@ jobs:
|
|||||||
if: needs.determine-jobs.outputs.python-linters == 'true'
|
if: needs.determine-jobs.outputs.python-linters == 'true'
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Restore Python
|
- name: Restore Python
|
||||||
uses: ./.github/actions/restore-python
|
uses: ./.github/actions/restore-python
|
||||||
with:
|
with:
|
||||||
@@ -91,7 +91,7 @@ jobs:
|
|||||||
- common
|
- common
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Restore Python
|
- name: Restore Python
|
||||||
uses: ./.github/actions/restore-python
|
uses: ./.github/actions/restore-python
|
||||||
with:
|
with:
|
||||||
@@ -132,7 +132,7 @@ jobs:
|
|||||||
- common
|
- common
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Restore Python
|
- name: Restore Python
|
||||||
id: restore-python
|
id: restore-python
|
||||||
uses: ./.github/actions/restore-python
|
uses: ./.github/actions/restore-python
|
||||||
@@ -157,7 +157,7 @@ jobs:
|
|||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
- name: Save Python virtual environment cache
|
- name: Save Python virtual environment cache
|
||||||
if: github.ref == 'refs/heads/dev'
|
if: github.ref == 'refs/heads/dev'
|
||||||
uses: actions/cache/save@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
|
key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
|
||||||
@@ -183,7 +183,7 @@ jobs:
|
|||||||
component-test-batches: ${{ steps.determine.outputs.component-test-batches }}
|
component-test-batches: ${{ steps.determine.outputs.component-test-batches }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
# Fetch enough history to find the merge base
|
# Fetch enough history to find the merge base
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
@@ -193,7 +193,7 @@ jobs:
|
|||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||||
- name: Restore components graph cache
|
- name: Restore components graph cache
|
||||||
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||||
with:
|
with:
|
||||||
path: .temp/components_graph.json
|
path: .temp/components_graph.json
|
||||||
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
|
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
|
||||||
@@ -223,7 +223,7 @@ jobs:
|
|||||||
echo "component-test-batches=$(echo "$output" | jq -c '.component_test_batches')" >> $GITHUB_OUTPUT
|
echo "component-test-batches=$(echo "$output" | jq -c '.component_test_batches')" >> $GITHUB_OUTPUT
|
||||||
- name: Save components graph cache
|
- name: Save components graph cache
|
||||||
if: github.ref == 'refs/heads/dev'
|
if: github.ref == 'refs/heads/dev'
|
||||||
uses: actions/cache/save@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||||
with:
|
with:
|
||||||
path: .temp/components_graph.json
|
path: .temp/components_graph.json
|
||||||
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
|
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
|
||||||
@@ -237,15 +237,15 @@ jobs:
|
|||||||
if: needs.determine-jobs.outputs.integration-tests == 'true'
|
if: needs.determine-jobs.outputs.integration-tests == 'true'
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Set up Python 3.13
|
- name: Set up Python 3.13
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.13"
|
python-version: "3.13"
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
|
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
|
||||||
@@ -273,7 +273,7 @@ jobs:
|
|||||||
if: github.event_name == 'pull_request' && (needs.determine-jobs.outputs.cpp-unit-tests-run-all == 'true' || needs.determine-jobs.outputs.cpp-unit-tests-components != '[]')
|
if: github.event_name == 'pull_request' && (needs.determine-jobs.outputs.cpp-unit-tests-run-all == 'true' || needs.determine-jobs.outputs.cpp-unit-tests-components != '[]')
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Restore Python
|
- name: Restore Python
|
||||||
uses: ./.github/actions/restore-python
|
uses: ./.github/actions/restore-python
|
||||||
@@ -321,7 +321,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
# Need history for HEAD~1 to work for checking changed files
|
# Need history for HEAD~1 to work for checking changed files
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
@@ -334,14 +334,14 @@ jobs:
|
|||||||
|
|
||||||
- name: Cache platformio
|
- name: Cache platformio
|
||||||
if: github.ref == 'refs/heads/dev'
|
if: github.ref == 'refs/heads/dev'
|
||||||
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||||
with:
|
with:
|
||||||
path: ~/.platformio
|
path: ~/.platformio
|
||||||
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
||||||
|
|
||||||
- name: Cache platformio
|
- name: Cache platformio
|
||||||
if: github.ref != 'refs/heads/dev'
|
if: github.ref != 'refs/heads/dev'
|
||||||
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||||
with:
|
with:
|
||||||
path: ~/.platformio
|
path: ~/.platformio
|
||||||
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
||||||
@@ -400,7 +400,7 @@ jobs:
|
|||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
# Need history for HEAD~1 to work for checking changed files
|
# Need history for HEAD~1 to work for checking changed files
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
@@ -413,14 +413,14 @@ jobs:
|
|||||||
|
|
||||||
- name: Cache platformio
|
- name: Cache platformio
|
||||||
if: github.ref == 'refs/heads/dev'
|
if: github.ref == 'refs/heads/dev'
|
||||||
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||||
with:
|
with:
|
||||||
path: ~/.platformio
|
path: ~/.platformio
|
||||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||||
|
|
||||||
- name: Cache platformio
|
- name: Cache platformio
|
||||||
if: github.ref != 'refs/heads/dev'
|
if: github.ref != 'refs/heads/dev'
|
||||||
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||||
with:
|
with:
|
||||||
path: ~/.platformio
|
path: ~/.platformio
|
||||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||||
@@ -489,7 +489,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
# Need history for HEAD~1 to work for checking changed files
|
# Need history for HEAD~1 to work for checking changed files
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
@@ -502,14 +502,14 @@ jobs:
|
|||||||
|
|
||||||
- name: Cache platformio
|
- name: Cache platformio
|
||||||
if: github.ref == 'refs/heads/dev'
|
if: github.ref == 'refs/heads/dev'
|
||||||
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||||
with:
|
with:
|
||||||
path: ~/.platformio
|
path: ~/.platformio
|
||||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||||
|
|
||||||
- name: Cache platformio
|
- name: Cache platformio
|
||||||
if: github.ref != 'refs/heads/dev'
|
if: github.ref != 'refs/heads/dev'
|
||||||
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||||
with:
|
with:
|
||||||
path: ~/.platformio
|
path: ~/.platformio
|
||||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||||
@@ -577,7 +577,7 @@ jobs:
|
|||||||
version: 1.0
|
version: 1.0
|
||||||
|
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Restore Python
|
- name: Restore Python
|
||||||
uses: ./.github/actions/restore-python
|
uses: ./.github/actions/restore-python
|
||||||
with:
|
with:
|
||||||
@@ -662,7 +662,7 @@ jobs:
|
|||||||
if: github.event_name == 'pull_request' && !startsWith(github.base_ref, 'beta') && !startsWith(github.base_ref, 'release')
|
if: github.event_name == 'pull_request' && !startsWith(github.base_ref, 'beta') && !startsWith(github.base_ref, 'release')
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Restore Python
|
- name: Restore Python
|
||||||
uses: ./.github/actions/restore-python
|
uses: ./.github/actions/restore-python
|
||||||
with:
|
with:
|
||||||
@@ -688,7 +688,7 @@ jobs:
|
|||||||
skip: ${{ steps.check-script.outputs.skip }}
|
skip: ${{ steps.check-script.outputs.skip }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out target branch
|
- name: Check out target branch
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.base_ref }}
|
ref: ${{ github.base_ref }}
|
||||||
|
|
||||||
@@ -735,7 +735,7 @@ jobs:
|
|||||||
- name: Restore cached memory analysis
|
- name: Restore cached memory analysis
|
||||||
id: cache-memory-analysis
|
id: cache-memory-analysis
|
||||||
if: steps.check-script.outputs.skip != 'true'
|
if: steps.check-script.outputs.skip != 'true'
|
||||||
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||||
with:
|
with:
|
||||||
path: memory-analysis-target.json
|
path: memory-analysis-target.json
|
||||||
key: ${{ steps.cache-key.outputs.cache-key }}
|
key: ${{ steps.cache-key.outputs.cache-key }}
|
||||||
@@ -759,7 +759,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Cache platformio
|
- name: Cache platformio
|
||||||
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
|
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
|
||||||
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||||
with:
|
with:
|
||||||
path: ~/.platformio
|
path: ~/.platformio
|
||||||
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
|
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
|
||||||
@@ -800,7 +800,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Save memory analysis to cache
|
- name: Save memory analysis to cache
|
||||||
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success'
|
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success'
|
||||||
uses: actions/cache/save@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||||
with:
|
with:
|
||||||
path: memory-analysis-target.json
|
path: memory-analysis-target.json
|
||||||
key: ${{ steps.cache-key.outputs.cache-key }}
|
key: ${{ steps.cache-key.outputs.cache-key }}
|
||||||
@@ -840,14 +840,14 @@ jobs:
|
|||||||
flash_usage: ${{ steps.extract.outputs.flash_usage }}
|
flash_usage: ${{ steps.extract.outputs.flash_usage }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out PR branch
|
- name: Check out PR branch
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Restore Python
|
- name: Restore Python
|
||||||
uses: ./.github/actions/restore-python
|
uses: ./.github/actions/restore-python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||||
- name: Cache platformio
|
- name: Cache platformio
|
||||||
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||||
with:
|
with:
|
||||||
path: ~/.platformio
|
path: ~/.platformio
|
||||||
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
|
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
|
||||||
@@ -908,7 +908,7 @@ jobs:
|
|||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Restore Python
|
- name: Restore Python
|
||||||
uses: ./.github/actions/restore-python
|
uses: ./.github/actions/restore-python
|
||||||
with:
|
with:
|
||||||
|
|||||||
6
.github/workflows/codeql.yml
vendored
6
.github/workflows/codeql.yml
vendored
@@ -54,11 +54,11 @@ jobs:
|
|||||||
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
|
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
|
uses: github/codeql-action/init@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
build-mode: ${{ matrix.build-mode }}
|
build-mode: ${{ matrix.build-mode }}
|
||||||
@@ -86,6 +86,6 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
|
uses: github/codeql-action/analyze@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
|
||||||
with:
|
with:
|
||||||
category: "/language:${{matrix.language}}"
|
category: "/language:${{matrix.language}}"
|
||||||
|
|||||||
20
.github/workflows/release.yml
vendored
20
.github/workflows/release.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
|||||||
branch_build: ${{ steps.tag.outputs.branch_build }}
|
branch_build: ${{ steps.tag.outputs.branch_build }}
|
||||||
deploy_env: ${{ steps.tag.outputs.deploy_env }}
|
deploy_env: ${{ steps.tag.outputs.deploy_env }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Get tag
|
- name: Get tag
|
||||||
id: tag
|
id: tag
|
||||||
# yamllint disable rule:line-length
|
# yamllint disable rule:line-length
|
||||||
@@ -60,9 +60,9 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.x"
|
python-version: "3.x"
|
||||||
- name: Build
|
- name: Build
|
||||||
@@ -92,9 +92,9 @@ jobs:
|
|||||||
os: "ubuntu-24.04-arm"
|
os: "ubuntu-24.04-arm"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
|
|
||||||
@@ -102,12 +102,12 @@ jobs:
|
|||||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||||
|
|
||||||
- name: Log in to docker hub
|
- name: Log in to docker hub
|
||||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USER }}
|
username: ${{ secrets.DOCKER_USER }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
- name: Log in to the GitHub container registry
|
- name: Log in to the GitHub container registry
|
||||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -168,7 +168,7 @@ jobs:
|
|||||||
- ghcr
|
- ghcr
|
||||||
- dockerhub
|
- dockerhub
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Download digests
|
- name: Download digests
|
||||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||||
@@ -182,13 +182,13 @@ jobs:
|
|||||||
|
|
||||||
- name: Log in to docker hub
|
- name: Log in to docker hub
|
||||||
if: matrix.registry == 'dockerhub'
|
if: matrix.registry == 'dockerhub'
|
||||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USER }}
|
username: ${{ secrets.DOCKER_USER }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
- name: Log in to the GitHub container registry
|
- name: Log in to the GitHub container registry
|
||||||
if: matrix.registry == 'ghcr'
|
if: matrix.registry == 'ghcr'
|
||||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
|
|||||||
8
.github/workflows/sync-device-classes.yml
vendored
8
.github/workflows/sync-device-classes.yml
vendored
@@ -13,16 +13,16 @@ jobs:
|
|||||||
if: github.repository == 'esphome/esphome'
|
if: github.repository == 'esphome/esphome'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Checkout Home Assistant
|
- name: Checkout Home Assistant
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
repository: home-assistant/core
|
repository: home-assistant/core
|
||||||
path: lib/home-assistant
|
path: lib/home-assistant
|
||||||
|
|
||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: 3.13
|
python-version: 3.13
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ jobs:
|
|||||||
python script/run-in-env.py pre-commit run --all-files
|
python script/run-in-env.py pre-commit run --all-files
|
||||||
|
|
||||||
- name: Commit changes
|
- name: Commit changes
|
||||||
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
|
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
|
||||||
with:
|
with:
|
||||||
commit-message: "Synchronise Device Classes from Home Assistant"
|
commit-message: "Synchronise Device Classes from Home Assistant"
|
||||||
committer: esphomebot <esphome@openhomefoundation.org>
|
committer: esphomebot <esphome@openhomefoundation.org>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ ci:
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
# Ruff version.
|
# Ruff version.
|
||||||
rev: v0.14.13
|
rev: v0.14.14
|
||||||
hooks:
|
hooks:
|
||||||
# Run the linter.
|
# Run the linter.
|
||||||
- id: ruff
|
- id: ruff
|
||||||
|
|||||||
@@ -88,7 +88,8 @@ esphome/components/bmp3xx/* @latonita
|
|||||||
esphome/components/bmp3xx_base/* @latonita @martgras
|
esphome/components/bmp3xx_base/* @latonita @martgras
|
||||||
esphome/components/bmp3xx_i2c/* @latonita
|
esphome/components/bmp3xx_i2c/* @latonita
|
||||||
esphome/components/bmp3xx_spi/* @latonita
|
esphome/components/bmp3xx_spi/* @latonita
|
||||||
esphome/components/bmp581/* @kahrendt
|
esphome/components/bmp581_base/* @danielkent-net @kahrendt
|
||||||
|
esphome/components/bmp581_i2c/* @danielkent-net @kahrendt
|
||||||
esphome/components/bp1658cj/* @Cossid
|
esphome/components/bp1658cj/* @Cossid
|
||||||
esphome/components/bp5758d/* @Cossid
|
esphome/components/bp5758d/* @Cossid
|
||||||
esphome/components/bthome_mithermometer/* @nagyrobi
|
esphome/components/bthome_mithermometer/* @nagyrobi
|
||||||
@@ -103,6 +104,7 @@ esphome/components/cc1101/* @gabest11 @lygris
|
|||||||
esphome/components/ccs811/* @habbie
|
esphome/components/ccs811/* @habbie
|
||||||
esphome/components/cd74hc4067/* @asoehlke
|
esphome/components/cd74hc4067/* @asoehlke
|
||||||
esphome/components/ch422g/* @clydebarrow @jesterret
|
esphome/components/ch422g/* @clydebarrow @jesterret
|
||||||
|
esphome/components/ch423/* @dwmw2
|
||||||
esphome/components/chsc6x/* @kkosik20
|
esphome/components/chsc6x/* @kkosik20
|
||||||
esphome/components/climate/* @esphome/core
|
esphome/components/climate/* @esphome/core
|
||||||
esphome/components/climate_ir/* @glmnet
|
esphome/components/climate_ir/* @glmnet
|
||||||
@@ -481,6 +483,7 @@ esphome/components/switch/* @esphome/core
|
|||||||
esphome/components/switch/binary_sensor/* @ssieb
|
esphome/components/switch/binary_sensor/* @ssieb
|
||||||
esphome/components/sx126x/* @swoboda1337
|
esphome/components/sx126x/* @swoboda1337
|
||||||
esphome/components/sx127x/* @swoboda1337
|
esphome/components/sx127x/* @swoboda1337
|
||||||
|
esphome/components/sy6970/* @linkedupbits
|
||||||
esphome/components/syslog/* @clydebarrow
|
esphome/components/syslog/* @clydebarrow
|
||||||
esphome/components/t6615/* @tylermenezes
|
esphome/components/t6615/* @tylermenezes
|
||||||
esphome/components/tc74/* @sethgirvan
|
esphome/components/tc74/* @sethgirvan
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# PYTHON_ARGCOMPLETE_OK
|
# PYTHON_ARGCOMPLETE_OK
|
||||||
import argparse
|
import argparse
|
||||||
|
from collections.abc import Callable
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import functools
|
import functools
|
||||||
import getpass
|
import getpass
|
||||||
@@ -42,6 +43,7 @@ from esphome.const import (
|
|||||||
CONF_SUBSTITUTIONS,
|
CONF_SUBSTITUTIONS,
|
||||||
CONF_TOPIC,
|
CONF_TOPIC,
|
||||||
ENV_NOGITIGNORE,
|
ENV_NOGITIGNORE,
|
||||||
|
KEY_NATIVE_IDF,
|
||||||
PLATFORM_ESP32,
|
PLATFORM_ESP32,
|
||||||
PLATFORM_ESP8266,
|
PLATFORM_ESP8266,
|
||||||
PLATFORM_RP2040,
|
PLATFORM_RP2040,
|
||||||
@@ -115,6 +117,7 @@ class ArgsProtocol(Protocol):
|
|||||||
configuration: str
|
configuration: str
|
||||||
name: str
|
name: str
|
||||||
upload_speed: str | None
|
upload_speed: str | None
|
||||||
|
native_idf: bool
|
||||||
|
|
||||||
|
|
||||||
def choose_prompt(options, purpose: str = None):
|
def choose_prompt(options, purpose: str = None):
|
||||||
@@ -499,12 +502,15 @@ def wrap_to_code(name, comp):
|
|||||||
return wrapped
|
return wrapped
|
||||||
|
|
||||||
|
|
||||||
def write_cpp(config: ConfigType) -> int:
|
def write_cpp(config: ConfigType, native_idf: bool = False) -> int:
|
||||||
if not get_bool_env(ENV_NOGITIGNORE):
|
if not get_bool_env(ENV_NOGITIGNORE):
|
||||||
writer.write_gitignore()
|
writer.write_gitignore()
|
||||||
|
|
||||||
|
# Store native_idf flag so esp32 component can check it
|
||||||
|
CORE.data[KEY_NATIVE_IDF] = native_idf
|
||||||
|
|
||||||
generate_cpp_contents(config)
|
generate_cpp_contents(config)
|
||||||
return write_cpp_file()
|
return write_cpp_file(native_idf=native_idf)
|
||||||
|
|
||||||
|
|
||||||
def generate_cpp_contents(config: ConfigType) -> None:
|
def generate_cpp_contents(config: ConfigType) -> None:
|
||||||
@@ -518,32 +524,54 @@ def generate_cpp_contents(config: ConfigType) -> None:
|
|||||||
CORE.flush_tasks()
|
CORE.flush_tasks()
|
||||||
|
|
||||||
|
|
||||||
def write_cpp_file() -> int:
|
def write_cpp_file(native_idf: bool = False) -> int:
|
||||||
code_s = indent(CORE.cpp_main_section)
|
code_s = indent(CORE.cpp_main_section)
|
||||||
writer.write_cpp(code_s)
|
writer.write_cpp(code_s)
|
||||||
|
|
||||||
from esphome.build_gen import platformio
|
if native_idf and CORE.is_esp32 and CORE.target_framework == "esp-idf":
|
||||||
|
from esphome.build_gen import espidf
|
||||||
|
|
||||||
platformio.write_project()
|
espidf.write_project()
|
||||||
|
else:
|
||||||
|
from esphome.build_gen import platformio
|
||||||
|
|
||||||
|
platformio.write_project()
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
|
def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
|
||||||
from esphome import platformio_api
|
native_idf = getattr(args, "native_idf", False)
|
||||||
|
|
||||||
# NOTE: "Build path:" format is parsed by script/ci_memory_impact_extract.py
|
# NOTE: "Build path:" format is parsed by script/ci_memory_impact_extract.py
|
||||||
# If you change this format, update the regex in that script as well
|
# If you change this format, update the regex in that script as well
|
||||||
_LOGGER.info("Compiling app... Build path: %s", CORE.build_path)
|
_LOGGER.info("Compiling app... Build path: %s", CORE.build_path)
|
||||||
rc = platformio_api.run_compile(config, CORE.verbose)
|
|
||||||
if rc != 0:
|
if native_idf and CORE.is_esp32 and CORE.target_framework == "esp-idf":
|
||||||
return rc
|
from esphome import espidf_api
|
||||||
|
|
||||||
|
rc = espidf_api.run_compile(config, CORE.verbose)
|
||||||
|
if rc != 0:
|
||||||
|
return rc
|
||||||
|
|
||||||
|
# Create factory.bin and ota.bin
|
||||||
|
espidf_api.create_factory_bin()
|
||||||
|
espidf_api.create_ota_bin()
|
||||||
|
else:
|
||||||
|
from esphome import platformio_api
|
||||||
|
|
||||||
|
rc = platformio_api.run_compile(config, CORE.verbose)
|
||||||
|
if rc != 0:
|
||||||
|
return rc
|
||||||
|
|
||||||
|
idedata = platformio_api.get_idedata(config)
|
||||||
|
if idedata is None:
|
||||||
|
return 1
|
||||||
|
|
||||||
# Check if firmware was rebuilt and emit build_info + create manifest
|
# Check if firmware was rebuilt and emit build_info + create manifest
|
||||||
_check_and_emit_build_info()
|
_check_and_emit_build_info()
|
||||||
|
|
||||||
idedata = platformio_api.get_idedata(config)
|
return 0
|
||||||
return 0 if idedata is not None else 1
|
|
||||||
|
|
||||||
|
|
||||||
def _check_and_emit_build_info() -> None:
|
def _check_and_emit_build_info() -> None:
|
||||||
@@ -800,14 +828,8 @@ def command_vscode(args: ArgsProtocol) -> int | None:
|
|||||||
|
|
||||||
|
|
||||||
def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None:
|
def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||||
# Set memory analysis options in config
|
native_idf = getattr(args, "native_idf", False)
|
||||||
if args.analyze_memory:
|
exit_code = write_cpp(config, native_idf=native_idf)
|
||||||
config.setdefault(CONF_ESPHOME, {})["analyze_memory"] = True
|
|
||||||
|
|
||||||
if args.memory_report:
|
|
||||||
config.setdefault(CONF_ESPHOME, {})["memory_report_file"] = args.memory_report
|
|
||||||
|
|
||||||
exit_code = write_cpp(config)
|
|
||||||
if exit_code != 0:
|
if exit_code != 0:
|
||||||
return exit_code
|
return exit_code
|
||||||
if args.only_generate:
|
if args.only_generate:
|
||||||
@@ -862,7 +884,8 @@ def command_logs(args: ArgsProtocol, config: ConfigType) -> int | None:
|
|||||||
|
|
||||||
|
|
||||||
def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
|
def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||||
exit_code = write_cpp(config)
|
native_idf = getattr(args, "native_idf", False)
|
||||||
|
exit_code = write_cpp(config, native_idf=native_idf)
|
||||||
if exit_code != 0:
|
if exit_code != 0:
|
||||||
return exit_code
|
return exit_code
|
||||||
exit_code = compile_program(args, config)
|
exit_code = compile_program(args, config)
|
||||||
@@ -943,11 +966,21 @@ def command_dashboard(args: ArgsProtocol) -> int | None:
|
|||||||
return dashboard.start_dashboard(args)
|
return dashboard.start_dashboard(args)
|
||||||
|
|
||||||
|
|
||||||
def command_update_all(args: ArgsProtocol) -> int | None:
|
def run_multiple_configs(
|
||||||
|
files: list, command_builder: Callable[[str], list[str]]
|
||||||
|
) -> int:
|
||||||
|
"""Run a command for each configuration file in a subprocess.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
files: List of configuration files to process.
|
||||||
|
command_builder: Callable that takes a file path and returns a command list.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Number of failed files.
|
||||||
|
"""
|
||||||
import click
|
import click
|
||||||
|
|
||||||
success = {}
|
success = {}
|
||||||
files = list_yaml_files(args.configuration)
|
|
||||||
twidth = 60
|
twidth = 60
|
||||||
|
|
||||||
def print_bar(middle_text):
|
def print_bar(middle_text):
|
||||||
@@ -957,17 +990,19 @@ def command_update_all(args: ArgsProtocol) -> int | None:
|
|||||||
safe_print(f"{half_line}{middle_text}{half_line}")
|
safe_print(f"{half_line}{middle_text}{half_line}")
|
||||||
|
|
||||||
for f in files:
|
for f in files:
|
||||||
safe_print(f"Updating {color(AnsiFore.CYAN, str(f))}")
|
f_path = Path(f) if not isinstance(f, Path) else f
|
||||||
|
|
||||||
|
if any(f_path.name == x for x in SECRETS_FILES):
|
||||||
|
_LOGGER.warning("Skipping secrets file %s", f_path)
|
||||||
|
continue
|
||||||
|
|
||||||
|
safe_print(f"Processing {color(AnsiFore.CYAN, str(f))}")
|
||||||
safe_print("-" * twidth)
|
safe_print("-" * twidth)
|
||||||
safe_print()
|
safe_print()
|
||||||
if CORE.dashboard:
|
|
||||||
rc = run_external_process(
|
cmd = command_builder(f)
|
||||||
"esphome", "--dashboard", "run", f, "--no-logs", "--device", "OTA"
|
rc = run_external_process(*cmd)
|
||||||
)
|
|
||||||
else:
|
|
||||||
rc = run_external_process(
|
|
||||||
"esphome", "run", f, "--no-logs", "--device", "OTA"
|
|
||||||
)
|
|
||||||
if rc == 0:
|
if rc == 0:
|
||||||
print_bar(f"[{color(AnsiFore.BOLD_GREEN, 'SUCCESS')}] {str(f)}")
|
print_bar(f"[{color(AnsiFore.BOLD_GREEN, 'SUCCESS')}] {str(f)}")
|
||||||
success[f] = True
|
success[f] = True
|
||||||
@@ -982,6 +1017,8 @@ def command_update_all(args: ArgsProtocol) -> int | None:
|
|||||||
print_bar(f"[{color(AnsiFore.BOLD_WHITE, 'SUMMARY')}]")
|
print_bar(f"[{color(AnsiFore.BOLD_WHITE, 'SUMMARY')}]")
|
||||||
failed = 0
|
failed = 0
|
||||||
for f in files:
|
for f in files:
|
||||||
|
if f not in success:
|
||||||
|
continue # Skipped file
|
||||||
if success[f]:
|
if success[f]:
|
||||||
safe_print(f" - {str(f)}: {color(AnsiFore.GREEN, 'SUCCESS')}")
|
safe_print(f" - {str(f)}: {color(AnsiFore.GREEN, 'SUCCESS')}")
|
||||||
else:
|
else:
|
||||||
@@ -990,6 +1027,17 @@ def command_update_all(args: ArgsProtocol) -> int | None:
|
|||||||
return failed
|
return failed
|
||||||
|
|
||||||
|
|
||||||
|
def command_update_all(args: ArgsProtocol) -> int | None:
|
||||||
|
files = list_yaml_files(args.configuration)
|
||||||
|
|
||||||
|
def build_command(f):
|
||||||
|
if CORE.dashboard:
|
||||||
|
return ["esphome", "--dashboard", "run", f, "--no-logs", "--device", "OTA"]
|
||||||
|
return ["esphome", "run", f, "--no-logs", "--device", "OTA"]
|
||||||
|
|
||||||
|
return run_multiple_configs(files, build_command)
|
||||||
|
|
||||||
|
|
||||||
def command_idedata(args: ArgsProtocol, config: ConfigType) -> int:
|
def command_idedata(args: ArgsProtocol, config: ConfigType) -> int:
|
||||||
import json
|
import json
|
||||||
|
|
||||||
@@ -1292,16 +1340,10 @@ def parse_args(argv):
|
|||||||
action="store_true",
|
action="store_true",
|
||||||
)
|
)
|
||||||
parser_compile.add_argument(
|
parser_compile.add_argument(
|
||||||
"--analyze-memory",
|
"--native-idf",
|
||||||
help="Analyze and display memory usage by component after compilation.",
|
help="Build with native ESP-IDF instead of PlatformIO (ESP32 esp-idf framework only).",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
)
|
)
|
||||||
parser_compile.add_argument(
|
|
||||||
"--memory-report",
|
|
||||||
help="Save memory analysis report to a file (supports .json or .txt).",
|
|
||||||
type=str,
|
|
||||||
metavar="FILE",
|
|
||||||
)
|
|
||||||
|
|
||||||
parser_upload = subparsers.add_parser(
|
parser_upload = subparsers.add_parser(
|
||||||
"upload",
|
"upload",
|
||||||
@@ -1383,6 +1425,11 @@ def parse_args(argv):
|
|||||||
help="Reset the device before starting serial logs.",
|
help="Reset the device before starting serial logs.",
|
||||||
default=os.getenv("ESPHOME_SERIAL_LOGGING_RESET"),
|
default=os.getenv("ESPHOME_SERIAL_LOGGING_RESET"),
|
||||||
)
|
)
|
||||||
|
parser_run.add_argument(
|
||||||
|
"--native-idf",
|
||||||
|
help="Build with native ESP-IDF instead of PlatformIO (ESP32 esp-idf framework only).",
|
||||||
|
action="store_true",
|
||||||
|
)
|
||||||
|
|
||||||
parser_clean = subparsers.add_parser(
|
parser_clean = subparsers.add_parser(
|
||||||
"clean-mqtt",
|
"clean-mqtt",
|
||||||
@@ -1551,38 +1598,48 @@ def run_esphome(argv):
|
|||||||
|
|
||||||
_LOGGER.info("ESPHome %s", const.__version__)
|
_LOGGER.info("ESPHome %s", const.__version__)
|
||||||
|
|
||||||
for conf_path in args.configuration:
|
# Multiple configurations: use subprocesses to avoid state leakage
|
||||||
conf_path = Path(conf_path)
|
# between compilations (e.g., LVGL touchscreen state in module globals)
|
||||||
if any(conf_path.name == x for x in SECRETS_FILES):
|
if len(args.configuration) > 1:
|
||||||
_LOGGER.warning("Skipping secrets file %s", conf_path)
|
# Build command by reusing argv, replacing all configs with single file
|
||||||
continue
|
# argv[0] is the program path, skip it since we prefix with "esphome"
|
||||||
|
def build_command(f):
|
||||||
|
return (
|
||||||
|
["esphome"]
|
||||||
|
+ [arg for arg in argv[1:] if arg not in args.configuration]
|
||||||
|
+ [str(f)]
|
||||||
|
)
|
||||||
|
|
||||||
CORE.config_path = conf_path
|
return run_multiple_configs(args.configuration, build_command)
|
||||||
CORE.dashboard = args.dashboard
|
|
||||||
|
|
||||||
# For logs command, skip updating external components
|
# Single configuration
|
||||||
skip_external = args.command == "logs"
|
conf_path = Path(args.configuration[0])
|
||||||
config = read_config(
|
if any(conf_path.name == x for x in SECRETS_FILES):
|
||||||
dict(args.substitution) if args.substitution else {},
|
_LOGGER.warning("Skipping secrets file %s", conf_path)
|
||||||
skip_external_update=skip_external,
|
return 0
|
||||||
)
|
|
||||||
if config is None:
|
|
||||||
return 2
|
|
||||||
CORE.config = config
|
|
||||||
|
|
||||||
if args.command not in POST_CONFIG_ACTIONS:
|
CORE.config_path = conf_path
|
||||||
safe_print(f"Unknown command {args.command}")
|
CORE.dashboard = args.dashboard
|
||||||
|
|
||||||
try:
|
# For logs command, skip updating external components
|
||||||
rc = POST_CONFIG_ACTIONS[args.command](args, config)
|
skip_external = args.command == "logs"
|
||||||
except EsphomeError as e:
|
config = read_config(
|
||||||
_LOGGER.error(e, exc_info=args.verbose)
|
dict(args.substitution) if args.substitution else {},
|
||||||
return 1
|
skip_external_update=skip_external,
|
||||||
if rc != 0:
|
)
|
||||||
return rc
|
if config is None:
|
||||||
|
return 2
|
||||||
|
CORE.config = config
|
||||||
|
|
||||||
CORE.reset()
|
if args.command not in POST_CONFIG_ACTIONS:
|
||||||
return 0
|
safe_print(f"Unknown command {args.command}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
try:
|
||||||
|
return POST_CONFIG_ACTIONS[args.command](args, config)
|
||||||
|
except EsphomeError as e:
|
||||||
|
_LOGGER.error(e, exc_info=args.verbose)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
import json
|
|
||||||
import sys
|
import sys
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
@@ -439,28 +438,6 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
|||||||
|
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
def to_json(self) -> str:
|
|
||||||
"""Export analysis results as JSON."""
|
|
||||||
data = {
|
|
||||||
"components": {
|
|
||||||
name: {
|
|
||||||
"text": mem.text_size,
|
|
||||||
"rodata": mem.rodata_size,
|
|
||||||
"data": mem.data_size,
|
|
||||||
"bss": mem.bss_size,
|
|
||||||
"flash_total": mem.flash_total,
|
|
||||||
"ram_total": mem.ram_total,
|
|
||||||
"symbol_count": mem.symbol_count,
|
|
||||||
}
|
|
||||||
for name, mem in self.components.items()
|
|
||||||
},
|
|
||||||
"totals": {
|
|
||||||
"flash": sum(c.flash_total for c in self.components.values()),
|
|
||||||
"ram": sum(c.ram_total for c in self.components.values()),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return json.dumps(data, indent=2)
|
|
||||||
|
|
||||||
def dump_uncategorized_symbols(self, output_file: str | None = None) -> None:
|
def dump_uncategorized_symbols(self, output_file: str | None = None) -> None:
|
||||||
"""Dump uncategorized symbols for analysis."""
|
"""Dump uncategorized symbols for analysis."""
|
||||||
# Sort by size descending
|
# Sort by size descending
|
||||||
|
|||||||
139
esphome/build_gen/espidf.py
Normal file
139
esphome/build_gen/espidf.py
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
"""ESP-IDF direct build generator for ESPHome."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from esphome.components.esp32 import get_esp32_variant
|
||||||
|
from esphome.core import CORE
|
||||||
|
from esphome.helpers import mkdir_p, write_file_if_changed
|
||||||
|
|
||||||
|
|
||||||
|
def get_available_components() -> list[str] | None:
|
||||||
|
"""Get list of available ESP-IDF components from project_description.json.
|
||||||
|
|
||||||
|
Returns only internal ESP-IDF components, excluding external/managed
|
||||||
|
components (from idf_component.yml).
|
||||||
|
"""
|
||||||
|
project_desc = Path(CORE.build_path) / "build" / "project_description.json"
|
||||||
|
if not project_desc.exists():
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(project_desc, encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
component_info = data.get("build_component_info", {})
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for name, info in component_info.items():
|
||||||
|
# Exclude our own src component
|
||||||
|
if name == "src":
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Exclude managed/external components
|
||||||
|
comp_dir = info.get("dir", "")
|
||||||
|
if "managed_components" in comp_dir:
|
||||||
|
continue
|
||||||
|
|
||||||
|
result.append(name)
|
||||||
|
|
||||||
|
return result
|
||||||
|
except (json.JSONDecodeError, OSError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def has_discovered_components() -> bool:
|
||||||
|
"""Check if we have discovered components from a previous configure."""
|
||||||
|
return get_available_components() is not None
|
||||||
|
|
||||||
|
|
||||||
|
def get_project_cmakelists() -> str:
|
||||||
|
"""Generate the top-level CMakeLists.txt for ESP-IDF project."""
|
||||||
|
# Get IDF target from ESP32 variant (e.g., ESP32S3 -> esp32s3)
|
||||||
|
variant = get_esp32_variant()
|
||||||
|
idf_target = variant.lower().replace("-", "")
|
||||||
|
|
||||||
|
return f"""\
|
||||||
|
# Auto-generated by ESPHome
|
||||||
|
cmake_minimum_required(VERSION 3.16)
|
||||||
|
|
||||||
|
set(IDF_TARGET {idf_target})
|
||||||
|
set(EXTRA_COMPONENT_DIRS ${{CMAKE_SOURCE_DIR}}/src)
|
||||||
|
|
||||||
|
include($ENV{{IDF_PATH}}/tools/cmake/project.cmake)
|
||||||
|
project({CORE.name})
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def get_component_cmakelists(minimal: bool = False) -> str:
|
||||||
|
"""Generate the main component CMakeLists.txt."""
|
||||||
|
idf_requires = [] if minimal else (get_available_components() or [])
|
||||||
|
requires_str = " ".join(idf_requires)
|
||||||
|
|
||||||
|
# Extract compile definitions from build flags (-DXXX -> XXX)
|
||||||
|
compile_defs = [flag[2:] for flag in CORE.build_flags if flag.startswith("-D")]
|
||||||
|
compile_defs_str = "\n ".join(compile_defs) if compile_defs else ""
|
||||||
|
|
||||||
|
# Extract compile options (-W flags, excluding linker flags)
|
||||||
|
compile_opts = [
|
||||||
|
flag
|
||||||
|
for flag in CORE.build_flags
|
||||||
|
if flag.startswith("-W") and not flag.startswith("-Wl,")
|
||||||
|
]
|
||||||
|
compile_opts_str = "\n ".join(compile_opts) if compile_opts else ""
|
||||||
|
|
||||||
|
# Extract linker options (-Wl, flags)
|
||||||
|
link_opts = [flag for flag in CORE.build_flags if flag.startswith("-Wl,")]
|
||||||
|
link_opts_str = "\n ".join(link_opts) if link_opts else ""
|
||||||
|
|
||||||
|
return f"""\
|
||||||
|
# Auto-generated by ESPHome
|
||||||
|
file(GLOB_RECURSE app_sources
|
||||||
|
"${{CMAKE_CURRENT_SOURCE_DIR}}/*.cpp"
|
||||||
|
"${{CMAKE_CURRENT_SOURCE_DIR}}/*.c"
|
||||||
|
"${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.cpp"
|
||||||
|
"${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.c"
|
||||||
|
)
|
||||||
|
|
||||||
|
idf_component_register(
|
||||||
|
SRCS ${{app_sources}}
|
||||||
|
INCLUDE_DIRS "." "esphome"
|
||||||
|
REQUIRES {requires_str}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Apply C++ standard
|
||||||
|
target_compile_features(${{COMPONENT_LIB}} PUBLIC cxx_std_20)
|
||||||
|
|
||||||
|
# ESPHome compile definitions
|
||||||
|
target_compile_definitions(${{COMPONENT_LIB}} PUBLIC
|
||||||
|
{compile_defs_str}
|
||||||
|
)
|
||||||
|
|
||||||
|
# ESPHome compile options
|
||||||
|
target_compile_options(${{COMPONENT_LIB}} PUBLIC
|
||||||
|
{compile_opts_str}
|
||||||
|
)
|
||||||
|
|
||||||
|
# ESPHome linker options
|
||||||
|
target_link_options(${{COMPONENT_LIB}} PUBLIC
|
||||||
|
{link_opts_str}
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def write_project(minimal: bool = False) -> None:
|
||||||
|
"""Write ESP-IDF project files."""
|
||||||
|
mkdir_p(CORE.build_path)
|
||||||
|
mkdir_p(CORE.relative_src_path())
|
||||||
|
|
||||||
|
# Write top-level CMakeLists.txt
|
||||||
|
write_file_if_changed(
|
||||||
|
CORE.relative_build_path("CMakeLists.txt"),
|
||||||
|
get_project_cmakelists(),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Write component CMakeLists.txt in src/
|
||||||
|
write_file_if_changed(
|
||||||
|
CORE.relative_src_path("CMakeLists.txt"),
|
||||||
|
get_component_cmakelists(minimal=minimal),
|
||||||
|
)
|
||||||
@@ -2,7 +2,7 @@ import logging
|
|||||||
|
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components import sensor, voltage_sampler
|
from esphome.components import sensor, voltage_sampler
|
||||||
from esphome.components.esp32 import get_esp32_variant
|
from esphome.components.esp32 import get_esp32_variant, include_builtin_idf_component
|
||||||
from esphome.components.nrf52.const import AIN_TO_GPIO, EXTRA_ADC
|
from esphome.components.nrf52.const import AIN_TO_GPIO, EXTRA_ADC
|
||||||
from esphome.components.zephyr import (
|
from esphome.components.zephyr import (
|
||||||
zephyr_add_overlay,
|
zephyr_add_overlay,
|
||||||
@@ -118,6 +118,9 @@ async def to_code(config):
|
|||||||
cg.add(var.set_sampling_mode(config[CONF_SAMPLING_MODE]))
|
cg.add(var.set_sampling_mode(config[CONF_SAMPLING_MODE]))
|
||||||
|
|
||||||
if CORE.is_esp32:
|
if CORE.is_esp32:
|
||||||
|
# Re-enable ESP-IDF's ADC driver (excluded by default to save compile time)
|
||||||
|
include_builtin_idf_component("esp_adc")
|
||||||
|
|
||||||
if attenuation := config.get(CONF_ATTENUATION):
|
if attenuation := config.get(CONF_ATTENUATION):
|
||||||
if attenuation == "auto":
|
if attenuation == "auto":
|
||||||
cg.add(var.set_autorange(cg.global_ns.true))
|
cg.add(var.set_autorange(cg.global_ns.true))
|
||||||
@@ -160,21 +163,21 @@ async def to_code(config):
|
|||||||
zephyr_add_user("io-channels", f"<&adc {channel_id}>")
|
zephyr_add_user("io-channels", f"<&adc {channel_id}>")
|
||||||
zephyr_add_overlay(
|
zephyr_add_overlay(
|
||||||
f"""
|
f"""
|
||||||
&adc {{
|
&adc {{
|
||||||
#address-cells = <1>;
|
#address-cells = <1>;
|
||||||
#size-cells = <0>;
|
#size-cells = <0>;
|
||||||
|
|
||||||
channel@{channel_id} {{
|
channel@{channel_id} {{
|
||||||
reg = <{channel_id}>;
|
reg = <{channel_id}>;
|
||||||
zephyr,gain = "{gain}";
|
zephyr,gain = "{gain}";
|
||||||
zephyr,reference = "ADC_REF_INTERNAL";
|
zephyr,reference = "ADC_REF_INTERNAL";
|
||||||
zephyr,acquisition-time = <ADC_ACQ_TIME_DEFAULT>;
|
zephyr,acquisition-time = <ADC_ACQ_TIME_DEFAULT>;
|
||||||
zephyr,input-positive = <NRF_SAADC_{pin_number}>;
|
zephyr,input-positive = <NRF_SAADC_{pin_number}>;
|
||||||
zephyr,resolution = <14>;
|
zephyr,resolution = <14>;
|
||||||
zephyr,oversampling = <8>;
|
zephyr,oversampling = <8>;
|
||||||
}};
|
}};
|
||||||
}};
|
}};
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -67,52 +67,29 @@ void AlarmControlPanel::add_on_ready_callback(std::function<void()> &&callback)
|
|||||||
this->ready_callback_.add(std::move(callback));
|
this->ready_callback_.add(std::move(callback));
|
||||||
}
|
}
|
||||||
|
|
||||||
void AlarmControlPanel::arm_away(optional<std::string> code) {
|
void AlarmControlPanel::arm_with_code_(AlarmControlPanelCall &(AlarmControlPanelCall::*arm_method)(),
|
||||||
|
const char *code) {
|
||||||
auto call = this->make_call();
|
auto call = this->make_call();
|
||||||
call.arm_away();
|
(call.*arm_method)();
|
||||||
if (code.has_value())
|
if (code != nullptr)
|
||||||
call.set_code(code.value());
|
call.set_code(code);
|
||||||
call.perform();
|
call.perform();
|
||||||
}
|
}
|
||||||
|
|
||||||
void AlarmControlPanel::arm_home(optional<std::string> code) {
|
void AlarmControlPanel::arm_away(const char *code) { this->arm_with_code_(&AlarmControlPanelCall::arm_away, code); }
|
||||||
auto call = this->make_call();
|
|
||||||
call.arm_home();
|
void AlarmControlPanel::arm_home(const char *code) { this->arm_with_code_(&AlarmControlPanelCall::arm_home, code); }
|
||||||
if (code.has_value())
|
|
||||||
call.set_code(code.value());
|
void AlarmControlPanel::arm_night(const char *code) { this->arm_with_code_(&AlarmControlPanelCall::arm_night, code); }
|
||||||
call.perform();
|
|
||||||
|
void AlarmControlPanel::arm_vacation(const char *code) {
|
||||||
|
this->arm_with_code_(&AlarmControlPanelCall::arm_vacation, code);
|
||||||
}
|
}
|
||||||
|
|
||||||
void AlarmControlPanel::arm_night(optional<std::string> code) {
|
void AlarmControlPanel::arm_custom_bypass(const char *code) {
|
||||||
auto call = this->make_call();
|
this->arm_with_code_(&AlarmControlPanelCall::arm_custom_bypass, code);
|
||||||
call.arm_night();
|
|
||||||
if (code.has_value())
|
|
||||||
call.set_code(code.value());
|
|
||||||
call.perform();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void AlarmControlPanel::arm_vacation(optional<std::string> code) {
|
void AlarmControlPanel::disarm(const char *code) { this->arm_with_code_(&AlarmControlPanelCall::disarm, code); }
|
||||||
auto call = this->make_call();
|
|
||||||
call.arm_vacation();
|
|
||||||
if (code.has_value())
|
|
||||||
call.set_code(code.value());
|
|
||||||
call.perform();
|
|
||||||
}
|
|
||||||
|
|
||||||
void AlarmControlPanel::arm_custom_bypass(optional<std::string> code) {
|
|
||||||
auto call = this->make_call();
|
|
||||||
call.arm_custom_bypass();
|
|
||||||
if (code.has_value())
|
|
||||||
call.set_code(code.value());
|
|
||||||
call.perform();
|
|
||||||
}
|
|
||||||
|
|
||||||
void AlarmControlPanel::disarm(optional<std::string> code) {
|
|
||||||
auto call = this->make_call();
|
|
||||||
call.disarm();
|
|
||||||
if (code.has_value())
|
|
||||||
call.set_code(code.value());
|
|
||||||
call.perform();
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace esphome::alarm_control_panel
|
} // namespace esphome::alarm_control_panel
|
||||||
|
|||||||
@@ -76,37 +76,53 @@ class AlarmControlPanel : public EntityBase {
|
|||||||
*
|
*
|
||||||
* @param code The code
|
* @param code The code
|
||||||
*/
|
*/
|
||||||
void arm_away(optional<std::string> code = nullopt);
|
void arm_away(const char *code = nullptr);
|
||||||
|
void arm_away(const optional<std::string> &code) {
|
||||||
|
this->arm_away(code.has_value() ? code.value().c_str() : nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
/** arm the alarm in home mode
|
/** arm the alarm in home mode
|
||||||
*
|
*
|
||||||
* @param code The code
|
* @param code The code
|
||||||
*/
|
*/
|
||||||
void arm_home(optional<std::string> code = nullopt);
|
void arm_home(const char *code = nullptr);
|
||||||
|
void arm_home(const optional<std::string> &code) {
|
||||||
|
this->arm_home(code.has_value() ? code.value().c_str() : nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
/** arm the alarm in night mode
|
/** arm the alarm in night mode
|
||||||
*
|
*
|
||||||
* @param code The code
|
* @param code The code
|
||||||
*/
|
*/
|
||||||
void arm_night(optional<std::string> code = nullopt);
|
void arm_night(const char *code = nullptr);
|
||||||
|
void arm_night(const optional<std::string> &code) {
|
||||||
|
this->arm_night(code.has_value() ? code.value().c_str() : nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
/** arm the alarm in vacation mode
|
/** arm the alarm in vacation mode
|
||||||
*
|
*
|
||||||
* @param code The code
|
* @param code The code
|
||||||
*/
|
*/
|
||||||
void arm_vacation(optional<std::string> code = nullopt);
|
void arm_vacation(const char *code = nullptr);
|
||||||
|
void arm_vacation(const optional<std::string> &code) {
|
||||||
|
this->arm_vacation(code.has_value() ? code.value().c_str() : nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
/** arm the alarm in custom bypass mode
|
/** arm the alarm in custom bypass mode
|
||||||
*
|
*
|
||||||
* @param code The code
|
* @param code The code
|
||||||
*/
|
*/
|
||||||
void arm_custom_bypass(optional<std::string> code = nullopt);
|
void arm_custom_bypass(const char *code = nullptr);
|
||||||
|
void arm_custom_bypass(const optional<std::string> &code) {
|
||||||
|
this->arm_custom_bypass(code.has_value() ? code.value().c_str() : nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
/** disarm the alarm
|
/** disarm the alarm
|
||||||
*
|
*
|
||||||
* @param code The code
|
* @param code The code
|
||||||
*/
|
*/
|
||||||
void disarm(optional<std::string> code = nullopt);
|
void disarm(const char *code = nullptr);
|
||||||
|
void disarm(const optional<std::string> &code) { this->disarm(code.has_value() ? code.value().c_str() : nullptr); }
|
||||||
|
|
||||||
/** Get the state
|
/** Get the state
|
||||||
*
|
*
|
||||||
@@ -118,6 +134,8 @@ class AlarmControlPanel : public EntityBase {
|
|||||||
|
|
||||||
protected:
|
protected:
|
||||||
friend AlarmControlPanelCall;
|
friend AlarmControlPanelCall;
|
||||||
|
// Helper to reduce code duplication for arm/disarm methods
|
||||||
|
void arm_with_code_(AlarmControlPanelCall &(AlarmControlPanelCall::*arm_method)(), const char *code);
|
||||||
// in order to store last panel state in flash
|
// in order to store last panel state in flash
|
||||||
ESPPreferenceObject pref_;
|
ESPPreferenceObject pref_;
|
||||||
// current state
|
// current state
|
||||||
|
|||||||
@@ -10,8 +10,10 @@ static const char *const TAG = "alarm_control_panel";
|
|||||||
|
|
||||||
AlarmControlPanelCall::AlarmControlPanelCall(AlarmControlPanel *parent) : parent_(parent) {}
|
AlarmControlPanelCall::AlarmControlPanelCall(AlarmControlPanel *parent) : parent_(parent) {}
|
||||||
|
|
||||||
AlarmControlPanelCall &AlarmControlPanelCall::set_code(const std::string &code) {
|
AlarmControlPanelCall &AlarmControlPanelCall::set_code(const char *code) {
|
||||||
this->code_ = code;
|
if (code != nullptr) {
|
||||||
|
this->code_ = std::string(code);
|
||||||
|
}
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ class AlarmControlPanelCall {
|
|||||||
public:
|
public:
|
||||||
AlarmControlPanelCall(AlarmControlPanel *parent);
|
AlarmControlPanelCall(AlarmControlPanel *parent);
|
||||||
|
|
||||||
AlarmControlPanelCall &set_code(const std::string &code);
|
AlarmControlPanelCall &set_code(const char *code);
|
||||||
|
AlarmControlPanelCall &set_code(const std::string &code) { return this->set_code(code.c_str()); }
|
||||||
AlarmControlPanelCall &arm_away();
|
AlarmControlPanelCall &arm_away();
|
||||||
AlarmControlPanelCall &arm_home();
|
AlarmControlPanelCall &arm_home();
|
||||||
AlarmControlPanelCall &arm_night();
|
AlarmControlPanelCall &arm_night();
|
||||||
|
|||||||
@@ -66,15 +66,7 @@ template<typename... Ts> class ArmAwayAction : public Action<Ts...> {
|
|||||||
|
|
||||||
TEMPLATABLE_VALUE(std::string, code)
|
TEMPLATABLE_VALUE(std::string, code)
|
||||||
|
|
||||||
void play(const Ts &...x) override {
|
void play(const Ts &...x) override { this->alarm_control_panel_->arm_away(this->code_.optional_value(x...)); }
|
||||||
auto call = this->alarm_control_panel_->make_call();
|
|
||||||
auto code = this->code_.optional_value(x...);
|
|
||||||
if (code.has_value()) {
|
|
||||||
call.set_code(code.value());
|
|
||||||
}
|
|
||||||
call.arm_away();
|
|
||||||
call.perform();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
AlarmControlPanel *alarm_control_panel_;
|
AlarmControlPanel *alarm_control_panel_;
|
||||||
@@ -86,15 +78,7 @@ template<typename... Ts> class ArmHomeAction : public Action<Ts...> {
|
|||||||
|
|
||||||
TEMPLATABLE_VALUE(std::string, code)
|
TEMPLATABLE_VALUE(std::string, code)
|
||||||
|
|
||||||
void play(const Ts &...x) override {
|
void play(const Ts &...x) override { this->alarm_control_panel_->arm_home(this->code_.optional_value(x...)); }
|
||||||
auto call = this->alarm_control_panel_->make_call();
|
|
||||||
auto code = this->code_.optional_value(x...);
|
|
||||||
if (code.has_value()) {
|
|
||||||
call.set_code(code.value());
|
|
||||||
}
|
|
||||||
call.arm_home();
|
|
||||||
call.perform();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
AlarmControlPanel *alarm_control_panel_;
|
AlarmControlPanel *alarm_control_panel_;
|
||||||
@@ -106,15 +90,7 @@ template<typename... Ts> class ArmNightAction : public Action<Ts...> {
|
|||||||
|
|
||||||
TEMPLATABLE_VALUE(std::string, code)
|
TEMPLATABLE_VALUE(std::string, code)
|
||||||
|
|
||||||
void play(const Ts &...x) override {
|
void play(const Ts &...x) override { this->alarm_control_panel_->arm_night(this->code_.optional_value(x...)); }
|
||||||
auto call = this->alarm_control_panel_->make_call();
|
|
||||||
auto code = this->code_.optional_value(x...);
|
|
||||||
if (code.has_value()) {
|
|
||||||
call.set_code(code.value());
|
|
||||||
}
|
|
||||||
call.arm_night();
|
|
||||||
call.perform();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
AlarmControlPanel *alarm_control_panel_;
|
AlarmControlPanel *alarm_control_panel_;
|
||||||
|
|||||||
@@ -7,12 +7,6 @@ namespace am43 {
|
|||||||
|
|
||||||
const uint8_t START_PACKET[5] = {0x00, 0xff, 0x00, 0x00, 0x9a};
|
const uint8_t START_PACKET[5] = {0x00, 0xff, 0x00, 0x00, 0x9a};
|
||||||
|
|
||||||
std::string pkt_to_hex(const uint8_t *data, uint16_t len) {
|
|
||||||
char buf[64]; // format_hex_size(31) = 63, fits 31 bytes of hex data
|
|
||||||
format_hex_to(buf, sizeof(buf), data, len);
|
|
||||||
return buf;
|
|
||||||
}
|
|
||||||
|
|
||||||
Am43Packet *Am43Encoder::get_battery_level_request() {
|
Am43Packet *Am43Encoder::get_battery_level_request() {
|
||||||
uint8_t data = 0x1;
|
uint8_t data = 0x1;
|
||||||
return this->encode_(0xA2, &data, 1);
|
return this->encode_(0xA2, &data, 1);
|
||||||
@@ -70,7 +64,9 @@ Am43Packet *Am43Encoder::encode_(uint8_t command, uint8_t *data, uint8_t length)
|
|||||||
memcpy(&this->packet_.data[7], data, length);
|
memcpy(&this->packet_.data[7], data, length);
|
||||||
this->packet_.length = length + 7;
|
this->packet_.length = length + 7;
|
||||||
this->checksum_();
|
this->checksum_();
|
||||||
ESP_LOGV("am43", "ENC(%d): 0x%s", packet_.length, pkt_to_hex(packet_.data, packet_.length).c_str());
|
char hex_buf[format_hex_size(sizeof(this->packet_.data))];
|
||||||
|
ESP_LOGV("am43", "ENC(%d): 0x%s", this->packet_.length,
|
||||||
|
format_hex_to(hex_buf, this->packet_.data, this->packet_.length));
|
||||||
return &this->packet_;
|
return &this->packet_;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +81,8 @@ void Am43Decoder::decode(const uint8_t *data, uint16_t length) {
|
|||||||
this->has_set_state_response_ = false;
|
this->has_set_state_response_ = false;
|
||||||
this->has_position_ = false;
|
this->has_position_ = false;
|
||||||
this->has_pin_response_ = false;
|
this->has_pin_response_ = false;
|
||||||
ESP_LOGV("am43", "DEC(%d): 0x%s", length, pkt_to_hex(data, length).c_str());
|
char hex_buf[format_hex_size(24)]; // Max expected packet size
|
||||||
|
ESP_LOGV("am43", "DEC(%d): 0x%s", length, format_hex_to(hex_buf, data, length));
|
||||||
|
|
||||||
if (length < 2 || data[0] != 0x9a)
|
if (length < 2 || data[0] != 0x9a)
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -18,31 +18,31 @@ AnovaPacket *AnovaCodec::clean_packet_() {
|
|||||||
|
|
||||||
AnovaPacket *AnovaCodec::get_read_device_status_request() {
|
AnovaPacket *AnovaCodec::get_read_device_status_request() {
|
||||||
this->current_query_ = READ_DEVICE_STATUS;
|
this->current_query_ = READ_DEVICE_STATUS;
|
||||||
strncpy((char *) this->packet_.data, CMD_READ_DEVICE_STATUS, sizeof(this->packet_.data));
|
snprintf((char *) this->packet_.data, sizeof(this->packet_.data), "%s", CMD_READ_DEVICE_STATUS);
|
||||||
return this->clean_packet_();
|
return this->clean_packet_();
|
||||||
}
|
}
|
||||||
|
|
||||||
AnovaPacket *AnovaCodec::get_read_target_temp_request() {
|
AnovaPacket *AnovaCodec::get_read_target_temp_request() {
|
||||||
this->current_query_ = READ_TARGET_TEMPERATURE;
|
this->current_query_ = READ_TARGET_TEMPERATURE;
|
||||||
strncpy((char *) this->packet_.data, CMD_READ_TARGET_TEMP, sizeof(this->packet_.data));
|
snprintf((char *) this->packet_.data, sizeof(this->packet_.data), "%s", CMD_READ_TARGET_TEMP);
|
||||||
return this->clean_packet_();
|
return this->clean_packet_();
|
||||||
}
|
}
|
||||||
|
|
||||||
AnovaPacket *AnovaCodec::get_read_current_temp_request() {
|
AnovaPacket *AnovaCodec::get_read_current_temp_request() {
|
||||||
this->current_query_ = READ_CURRENT_TEMPERATURE;
|
this->current_query_ = READ_CURRENT_TEMPERATURE;
|
||||||
strncpy((char *) this->packet_.data, CMD_READ_CURRENT_TEMP, sizeof(this->packet_.data));
|
snprintf((char *) this->packet_.data, sizeof(this->packet_.data), "%s", CMD_READ_CURRENT_TEMP);
|
||||||
return this->clean_packet_();
|
return this->clean_packet_();
|
||||||
}
|
}
|
||||||
|
|
||||||
AnovaPacket *AnovaCodec::get_read_unit_request() {
|
AnovaPacket *AnovaCodec::get_read_unit_request() {
|
||||||
this->current_query_ = READ_UNIT;
|
this->current_query_ = READ_UNIT;
|
||||||
strncpy((char *) this->packet_.data, CMD_READ_UNIT, sizeof(this->packet_.data));
|
snprintf((char *) this->packet_.data, sizeof(this->packet_.data), "%s", CMD_READ_UNIT);
|
||||||
return this->clean_packet_();
|
return this->clean_packet_();
|
||||||
}
|
}
|
||||||
|
|
||||||
AnovaPacket *AnovaCodec::get_read_data_request() {
|
AnovaPacket *AnovaCodec::get_read_data_request() {
|
||||||
this->current_query_ = READ_DATA;
|
this->current_query_ = READ_DATA;
|
||||||
strncpy((char *) this->packet_.data, CMD_READ_DATA, sizeof(this->packet_.data));
|
snprintf((char *) this->packet_.data, sizeof(this->packet_.data), "%s", CMD_READ_DATA);
|
||||||
return this->clean_packet_();
|
return this->clean_packet_();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,13 +62,13 @@ AnovaPacket *AnovaCodec::get_set_unit_request(char unit) {
|
|||||||
|
|
||||||
AnovaPacket *AnovaCodec::get_start_request() {
|
AnovaPacket *AnovaCodec::get_start_request() {
|
||||||
this->current_query_ = START;
|
this->current_query_ = START;
|
||||||
strncpy((char *) this->packet_.data, CMD_START, sizeof(this->packet_.data));
|
snprintf((char *) this->packet_.data, sizeof(this->packet_.data), "%s", CMD_START);
|
||||||
return this->clean_packet_();
|
return this->clean_packet_();
|
||||||
}
|
}
|
||||||
|
|
||||||
AnovaPacket *AnovaCodec::get_stop_request() {
|
AnovaPacket *AnovaCodec::get_stop_request() {
|
||||||
this->current_query_ = STOP;
|
this->current_query_ = STOP;
|
||||||
strncpy((char *) this->packet_.data, CMD_STOP, sizeof(this->packet_.data));
|
snprintf((char *) this->packet_.data, sizeof(this->packet_.data), "%s", CMD_STOP);
|
||||||
return this->clean_packet_();
|
return this->clean_packet_();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1712,17 +1712,16 @@ void APIConnection::on_home_assistant_state_response(const HomeAssistantStateRes
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create null-terminated state for callback (parse_number needs null-termination)
|
// Create null-terminated state for callback (parse_number needs null-termination)
|
||||||
// HA state max length is 255, so 256 byte buffer covers all cases
|
// HA state max length is 255 characters, but attributes can be much longer
|
||||||
char state_buf[256];
|
// Use stack buffer for common case (states), heap fallback for large attributes
|
||||||
size_t copy_len = msg.state.size();
|
size_t state_len = msg.state.size();
|
||||||
if (copy_len >= sizeof(state_buf)) {
|
SmallBufferWithHeapFallback<MAX_STATE_LEN + 1> state_buf_alloc(state_len + 1);
|
||||||
copy_len = sizeof(state_buf) - 1; // Truncate to leave space for null terminator
|
char *state_buf = reinterpret_cast<char *>(state_buf_alloc.get());
|
||||||
|
if (state_len > 0) {
|
||||||
|
memcpy(state_buf, msg.state.c_str(), state_len);
|
||||||
}
|
}
|
||||||
if (copy_len > 0) {
|
state_buf[state_len] = '\0';
|
||||||
memcpy(state_buf, msg.state.c_str(), copy_len);
|
it.callback(StringRef(state_buf, state_len));
|
||||||
}
|
|
||||||
state_buf[copy_len] = '\0';
|
|
||||||
it.callback(StringRef(state_buf, copy_len));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -1845,23 +1844,8 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle Nagle's algorithm based on message type to prevent log messages from
|
// Set TCP_NODELAY based on message type - see set_nodelay_for_message() for details
|
||||||
// filling the TCP send buffer and crowding out important state updates.
|
this->helper_->set_nodelay_for_message(is_log_message);
|
||||||
//
|
|
||||||
// This honors the `no_delay` proto option - SubscribeLogsResponse is the only
|
|
||||||
// message with `option (no_delay) = false;` in api.proto, indicating it should
|
|
||||||
// allow Nagle coalescing. This option existed since 2019 but was never implemented.
|
|
||||||
//
|
|
||||||
// - Log messages: Enable Nagle (NODELAY=false) so small log packets coalesce
|
|
||||||
// into fewer, larger packets. They flush naturally via TCP delayed ACK timer
|
|
||||||
// (~200ms), buffer filling, or when a state update triggers a flush.
|
|
||||||
//
|
|
||||||
// - All other messages (state updates, responses): Disable Nagle (NODELAY=true)
|
|
||||||
// for immediate delivery. These are time-sensitive and should not be delayed.
|
|
||||||
//
|
|
||||||
// This must be done proactively BEFORE the buffer fills up - checking buffer
|
|
||||||
// state here would be too late since we'd already be in a degraded state.
|
|
||||||
this->helper_->set_nodelay(!is_log_message);
|
|
||||||
|
|
||||||
APIError err = this->helper_->write_protobuf_packet(message_type, buffer);
|
APIError err = this->helper_->write_protobuf_packet(message_type, buffer);
|
||||||
if (err == APIError::WOULD_BLOCK)
|
if (err == APIError::WOULD_BLOCK)
|
||||||
|
|||||||
@@ -120,26 +120,39 @@ class APIFrameHelper {
|
|||||||
}
|
}
|
||||||
return APIError::OK;
|
return APIError::OK;
|
||||||
}
|
}
|
||||||
/// Toggle TCP_NODELAY socket option to control Nagle's algorithm.
|
// Manage TCP_NODELAY (Nagle's algorithm) based on message type.
|
||||||
///
|
//
|
||||||
/// This is used to allow log messages to coalesce (Nagle enabled) while keeping
|
// For non-log messages (sensor data, state updates): Always disable Nagle
|
||||||
/// state updates low-latency (NODELAY enabled). Without this, many small log
|
// (NODELAY on) for immediate delivery - these are time-sensitive.
|
||||||
/// packets fill the TCP send buffer, crowding out important state updates.
|
//
|
||||||
///
|
// For log messages: Use Nagle to coalesce multiple small log packets into
|
||||||
/// State is tracked to minimize setsockopt() overhead - on lwip_raw (ESP8266/RP2040)
|
// fewer larger packets, reducing WiFi overhead. However, we limit batching
|
||||||
/// this is just a boolean assignment; on other platforms it's a lightweight syscall.
|
// to 3 messages to avoid excessive LWIP buffer pressure on memory-constrained
|
||||||
///
|
// devices like ESP8266. LWIP's TCP_OVERSIZE option coalesces the data into
|
||||||
/// @param enable true to enable NODELAY (disable Nagle), false to enable Nagle
|
// shared pbufs, but holding data too long waiting for Nagle's timer causes
|
||||||
/// @return true if successful or already in desired state
|
// buffer exhaustion and dropped messages.
|
||||||
bool set_nodelay(bool enable) {
|
//
|
||||||
if (this->nodelay_enabled_ == enable)
|
// Flow: Log 1 (Nagle on) -> Log 2 (Nagle on) -> Log 3 (NODELAY, flush all)
|
||||||
return true;
|
//
|
||||||
int val = enable ? 1 : 0;
|
void set_nodelay_for_message(bool is_log_message) {
|
||||||
int err = this->socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &val, sizeof(int));
|
if (!is_log_message) {
|
||||||
if (err == 0) {
|
if (this->nodelay_state_ != NODELAY_ON) {
|
||||||
this->nodelay_enabled_ = enable;
|
this->set_nodelay_raw_(true);
|
||||||
|
this->nodelay_state_ = NODELAY_ON;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log messages 1-3: state transitions -1 -> 1 -> 2 -> -1 (flush on 3rd)
|
||||||
|
if (this->nodelay_state_ == NODELAY_ON) {
|
||||||
|
this->set_nodelay_raw_(false);
|
||||||
|
this->nodelay_state_ = 1;
|
||||||
|
} else if (this->nodelay_state_ >= LOG_NAGLE_COUNT) {
|
||||||
|
this->set_nodelay_raw_(true);
|
||||||
|
this->nodelay_state_ = NODELAY_ON;
|
||||||
|
} else {
|
||||||
|
this->nodelay_state_++;
|
||||||
}
|
}
|
||||||
return err == 0;
|
|
||||||
}
|
}
|
||||||
virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0;
|
virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0;
|
||||||
// Write multiple protobuf messages in a single operation
|
// Write multiple protobuf messages in a single operation
|
||||||
@@ -229,10 +242,18 @@ class APIFrameHelper {
|
|||||||
uint8_t tx_buf_head_{0};
|
uint8_t tx_buf_head_{0};
|
||||||
uint8_t tx_buf_tail_{0};
|
uint8_t tx_buf_tail_{0};
|
||||||
uint8_t tx_buf_count_{0};
|
uint8_t tx_buf_count_{0};
|
||||||
// Tracks TCP_NODELAY state to minimize setsockopt() calls. Initialized to true
|
// Nagle batching state for log messages. NODELAY_ON (-1) means NODELAY is enabled
|
||||||
// since init_common_() enables NODELAY. Used by set_nodelay() to allow log
|
// (immediate send). Values 1-2 count log messages in the current Nagle batch.
|
||||||
// messages to coalesce while keeping state updates low-latency.
|
// After LOG_NAGLE_COUNT logs, we switch to NODELAY to flush and reset.
|
||||||
bool nodelay_enabled_{true};
|
static constexpr int8_t NODELAY_ON = -1;
|
||||||
|
static constexpr int8_t LOG_NAGLE_COUNT = 2;
|
||||||
|
int8_t nodelay_state_{NODELAY_ON};
|
||||||
|
|
||||||
|
// Internal helper to set TCP_NODELAY socket option
|
||||||
|
void set_nodelay_raw_(bool enable) {
|
||||||
|
int val = enable ? 1 : 0;
|
||||||
|
this->socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &val, sizeof(int));
|
||||||
|
}
|
||||||
|
|
||||||
// Common initialization for both plaintext and noise protocols
|
// Common initialization for both plaintext and noise protocols
|
||||||
APIError init_common_();
|
APIError init_common_();
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#ifdef USE_API_NOISE
|
#ifdef USE_API_NOISE
|
||||||
#include "api_connection.h" // For ClientInfo struct
|
#include "api_connection.h" // For ClientInfo struct
|
||||||
#include "esphome/core/application.h"
|
#include "esphome/core/application.h"
|
||||||
|
#include "esphome/core/entity_base.h"
|
||||||
#include "esphome/core/hal.h"
|
#include "esphome/core/hal.h"
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
@@ -256,28 +257,30 @@ APIError APINoiseFrameHelper::state_action_() {
|
|||||||
}
|
}
|
||||||
if (state_ == State::SERVER_HELLO) {
|
if (state_ == State::SERVER_HELLO) {
|
||||||
// send server hello
|
// send server hello
|
||||||
constexpr size_t mac_len = 13; // 12 hex chars + null terminator
|
|
||||||
const std::string &name = App.get_name();
|
const std::string &name = App.get_name();
|
||||||
char mac[mac_len];
|
char mac[MAC_ADDRESS_BUFFER_SIZE];
|
||||||
get_mac_address_into_buffer(mac);
|
get_mac_address_into_buffer(mac);
|
||||||
|
|
||||||
// Calculate positions and sizes
|
// Calculate positions and sizes
|
||||||
size_t name_len = name.size() + 1; // including null terminator
|
size_t name_len = name.size() + 1; // including null terminator
|
||||||
size_t name_offset = 1;
|
size_t name_offset = 1;
|
||||||
size_t mac_offset = name_offset + name_len;
|
size_t mac_offset = name_offset + name_len;
|
||||||
size_t total_size = 1 + name_len + mac_len;
|
size_t total_size = 1 + name_len + MAC_ADDRESS_BUFFER_SIZE;
|
||||||
|
|
||||||
auto msg = std::make_unique<uint8_t[]>(total_size);
|
// 1 (proto) + name (max ESPHOME_DEVICE_NAME_MAX_LEN) + 1 (name null)
|
||||||
|
// + mac (MAC_ADDRESS_BUFFER_SIZE - 1) + 1 (mac null)
|
||||||
|
constexpr size_t max_msg_size = 1 + ESPHOME_DEVICE_NAME_MAX_LEN + 1 + MAC_ADDRESS_BUFFER_SIZE;
|
||||||
|
uint8_t msg[max_msg_size];
|
||||||
|
|
||||||
// chosen proto
|
// chosen proto
|
||||||
msg[0] = 0x01;
|
msg[0] = 0x01;
|
||||||
|
|
||||||
// node name, terminated by null byte
|
// node name, terminated by null byte
|
||||||
std::memcpy(msg.get() + name_offset, name.c_str(), name_len);
|
std::memcpy(msg + name_offset, name.c_str(), name_len);
|
||||||
// node mac, terminated by null byte
|
// node mac, terminated by null byte
|
||||||
std::memcpy(msg.get() + mac_offset, mac, mac_len);
|
std::memcpy(msg + mac_offset, mac, MAC_ADDRESS_BUFFER_SIZE);
|
||||||
|
|
||||||
aerr = write_frame_(msg.get(), total_size);
|
aerr = write_frame_(msg, total_size);
|
||||||
if (aerr != APIError::OK)
|
if (aerr != APIError::OK)
|
||||||
return aerr;
|
return aerr;
|
||||||
|
|
||||||
@@ -353,35 +356,32 @@ APIError APINoiseFrameHelper::state_action_() {
|
|||||||
return APIError::OK;
|
return APIError::OK;
|
||||||
}
|
}
|
||||||
void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reason) {
|
void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reason) {
|
||||||
|
// Max reject message: "Bad handshake packet len" (24) + 1 (failure byte) = 25 bytes
|
||||||
|
uint8_t data[32];
|
||||||
|
data[0] = 0x01; // failure
|
||||||
|
|
||||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||||
// On ESP8266 with flash strings, we need to use PROGMEM-aware functions
|
// On ESP8266 with flash strings, we need to use PROGMEM-aware functions
|
||||||
size_t reason_len = strlen_P(reinterpret_cast<PGM_P>(reason));
|
size_t reason_len = strlen_P(reinterpret_cast<PGM_P>(reason));
|
||||||
size_t data_size = reason_len + 1;
|
|
||||||
auto data = std::make_unique<uint8_t[]>(data_size);
|
|
||||||
data[0] = 0x01; // failure
|
|
||||||
|
|
||||||
// Copy error message from PROGMEM
|
|
||||||
if (reason_len > 0) {
|
if (reason_len > 0) {
|
||||||
memcpy_P(data.get() + 1, reinterpret_cast<PGM_P>(reason), reason_len);
|
memcpy_P(data + 1, reinterpret_cast<PGM_P>(reason), reason_len);
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
// Normal memory access
|
// Normal memory access
|
||||||
const char *reason_str = LOG_STR_ARG(reason);
|
const char *reason_str = LOG_STR_ARG(reason);
|
||||||
size_t reason_len = strlen(reason_str);
|
size_t reason_len = strlen(reason_str);
|
||||||
size_t data_size = reason_len + 1;
|
|
||||||
auto data = std::make_unique<uint8_t[]>(data_size);
|
|
||||||
data[0] = 0x01; // failure
|
|
||||||
|
|
||||||
// Copy error message in bulk
|
|
||||||
if (reason_len > 0) {
|
if (reason_len > 0) {
|
||||||
std::memcpy(data.get() + 1, reason_str, reason_len);
|
// NOLINTNEXTLINE(bugprone-not-null-terminated-result) - binary protocol, not a C string
|
||||||
|
std::memcpy(data + 1, reason_str, reason_len);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
size_t data_size = reason_len + 1;
|
||||||
|
|
||||||
// temporarily remove failed state
|
// temporarily remove failed state
|
||||||
auto orig_state = state_;
|
auto orig_state = state_;
|
||||||
state_ = State::EXPLICIT_REJECT;
|
state_ = State::EXPLICIT_REJECT;
|
||||||
write_frame_(data.get(), data_size);
|
write_frame_(data, data_size);
|
||||||
state_ = orig_state;
|
state_ = orig_state;
|
||||||
}
|
}
|
||||||
APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
||||||
|
|||||||
@@ -264,9 +264,9 @@ template<typename... Ts> class APIRespondAction : public Action<Ts...> {
|
|||||||
// Build and send JSON response
|
// Build and send JSON response
|
||||||
json::JsonBuilder builder;
|
json::JsonBuilder builder;
|
||||||
this->json_builder_(x..., builder.root());
|
this->json_builder_(x..., builder.root());
|
||||||
std::string json_str = builder.serialize();
|
auto json_buf = builder.serialize();
|
||||||
this->parent_->send_action_response(call_id, success, StringRef(error_message),
|
this->parent_->send_action_response(call_id, success, StringRef(error_message),
|
||||||
reinterpret_cast<const uint8_t *>(json_str.data()), json_str.size());
|
reinterpret_cast<const uint8_t *>(json_buf.data()), json_buf.size());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -13,14 +13,11 @@ from . import AQI_CALCULATION_TYPE, CONF_CALCULATION_TYPE, aqi_ns
|
|||||||
CODEOWNERS = ["@jasstrong"]
|
CODEOWNERS = ["@jasstrong"]
|
||||||
DEPENDENCIES = ["sensor"]
|
DEPENDENCIES = ["sensor"]
|
||||||
|
|
||||||
UNIT_INDEX = "index"
|
|
||||||
|
|
||||||
AQISensor = aqi_ns.class_("AQISensor", sensor.Sensor, cg.Component)
|
AQISensor = aqi_ns.class_("AQISensor", sensor.Sensor, cg.Component)
|
||||||
|
|
||||||
CONFIG_SCHEMA = (
|
CONFIG_SCHEMA = (
|
||||||
sensor.sensor_schema(
|
sensor.sensor_schema(
|
||||||
AQISensor,
|
AQISensor,
|
||||||
unit_of_measurement=UNIT_INDEX,
|
|
||||||
accuracy_decimals=0,
|
accuracy_decimals=0,
|
||||||
device_class=DEVICE_CLASS_AQI,
|
device_class=DEVICE_CLASS_AQI,
|
||||||
state_class=STATE_CLASS_MEASUREMENT,
|
state_class=STATE_CLASS_MEASUREMENT,
|
||||||
|
|||||||
@@ -38,8 +38,10 @@ async def to_code(config):
|
|||||||
# https://github.com/ESP32Async/ESPAsyncTCP
|
# https://github.com/ESP32Async/ESPAsyncTCP
|
||||||
cg.add_library("ESP32Async/ESPAsyncTCP", "2.0.0")
|
cg.add_library("ESP32Async/ESPAsyncTCP", "2.0.0")
|
||||||
elif CORE.is_rp2040:
|
elif CORE.is_rp2040:
|
||||||
# https://github.com/khoih-prog/AsyncTCP_RP2040W
|
# https://github.com/ayushsharma82/RPAsyncTCP
|
||||||
cg.add_library("khoih-prog/AsyncTCP_RP2040W", "1.2.0")
|
# RPAsyncTCP is a drop-in replacement for AsyncTCP_RP2040W with better
|
||||||
|
# ESPAsyncWebServer compatibility
|
||||||
|
cg.add_library("ayushsharma82/RPAsyncTCP", "1.3.2")
|
||||||
# Other platforms (host, etc) use socket-based implementation
|
# Other platforms (host, etc) use socket-based implementation
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
// Use ESPAsyncTCP library for ESP8266 (always Arduino)
|
// Use ESPAsyncTCP library for ESP8266 (always Arduino)
|
||||||
#include <ESPAsyncTCP.h>
|
#include <ESPAsyncTCP.h>
|
||||||
#elif defined(USE_RP2040)
|
#elif defined(USE_RP2040)
|
||||||
// Use AsyncTCP_RP2040W library for RP2040
|
// Use RPAsyncTCP library for RP2040
|
||||||
#include <AsyncTCP_RP2040W.h>
|
#include <RPAsyncTCP.h>
|
||||||
#else
|
#else
|
||||||
// Use socket-based implementation for other platforms
|
// Use socket-based implementation for other platforms
|
||||||
#include "async_tcp_socket.h"
|
#include "async_tcp_socket.h"
|
||||||
|
|||||||
@@ -108,10 +108,14 @@ void ATM90E32Component::update() {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ATM90E32Component::get_cs_summary_(std::span<char, GPIO_SUMMARY_MAX_LEN> buffer) {
|
||||||
|
this->cs_->dump_summary(buffer.data(), buffer.size());
|
||||||
|
}
|
||||||
|
|
||||||
void ATM90E32Component::setup() {
|
void ATM90E32Component::setup() {
|
||||||
this->spi_setup();
|
this->spi_setup();
|
||||||
this->cs_summary_ = this->cs_->dump_summary();
|
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||||
const char *cs = this->cs_summary_.c_str();
|
this->get_cs_summary_(cs);
|
||||||
|
|
||||||
uint16_t mmode0 = 0x87; // 3P4W 50Hz
|
uint16_t mmode0 = 0x87; // 3P4W 50Hz
|
||||||
uint16_t high_thresh = 0;
|
uint16_t high_thresh = 0;
|
||||||
@@ -159,13 +163,13 @@ void ATM90E32Component::setup() {
|
|||||||
if (this->enable_offset_calibration_) {
|
if (this->enable_offset_calibration_) {
|
||||||
// Initialize flash storage for offset calibrations
|
// Initialize flash storage for offset calibrations
|
||||||
uint32_t o_hash = fnv1_hash("_offset_calibration_");
|
uint32_t o_hash = fnv1_hash("_offset_calibration_");
|
||||||
o_hash = fnv1_hash_extend(o_hash, this->cs_summary_);
|
o_hash = fnv1_hash_extend(o_hash, cs);
|
||||||
this->offset_pref_ = global_preferences->make_preference<OffsetCalibration[3]>(o_hash, true);
|
this->offset_pref_ = global_preferences->make_preference<OffsetCalibration[3]>(o_hash, true);
|
||||||
this->restore_offset_calibrations_();
|
this->restore_offset_calibrations_();
|
||||||
|
|
||||||
// Initialize flash storage for power offset calibrations
|
// Initialize flash storage for power offset calibrations
|
||||||
uint32_t po_hash = fnv1_hash("_power_offset_calibration_");
|
uint32_t po_hash = fnv1_hash("_power_offset_calibration_");
|
||||||
po_hash = fnv1_hash_extend(po_hash, this->cs_summary_);
|
po_hash = fnv1_hash_extend(po_hash, cs);
|
||||||
this->power_offset_pref_ = global_preferences->make_preference<PowerOffsetCalibration[3]>(po_hash, true);
|
this->power_offset_pref_ = global_preferences->make_preference<PowerOffsetCalibration[3]>(po_hash, true);
|
||||||
this->restore_power_offset_calibrations_();
|
this->restore_power_offset_calibrations_();
|
||||||
} else {
|
} else {
|
||||||
@@ -186,7 +190,7 @@ void ATM90E32Component::setup() {
|
|||||||
if (this->enable_gain_calibration_) {
|
if (this->enable_gain_calibration_) {
|
||||||
// Initialize flash storage for gain calibration
|
// Initialize flash storage for gain calibration
|
||||||
uint32_t g_hash = fnv1_hash("_gain_calibration_");
|
uint32_t g_hash = fnv1_hash("_gain_calibration_");
|
||||||
g_hash = fnv1_hash_extend(g_hash, this->cs_summary_);
|
g_hash = fnv1_hash_extend(g_hash, cs);
|
||||||
this->gain_calibration_pref_ = global_preferences->make_preference<GainCalibration[3]>(g_hash, true);
|
this->gain_calibration_pref_ = global_preferences->make_preference<GainCalibration[3]>(g_hash, true);
|
||||||
this->restore_gain_calibrations_();
|
this->restore_gain_calibrations_();
|
||||||
|
|
||||||
@@ -217,7 +221,8 @@ void ATM90E32Component::setup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ATM90E32Component::log_calibration_status_() {
|
void ATM90E32Component::log_calibration_status_() {
|
||||||
const char *cs = this->cs_summary_.c_str();
|
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||||
|
this->get_cs_summary_(cs);
|
||||||
|
|
||||||
bool offset_mismatch = false;
|
bool offset_mismatch = false;
|
||||||
bool power_mismatch = false;
|
bool power_mismatch = false;
|
||||||
@@ -568,7 +573,8 @@ float ATM90E32Component::get_chip_temperature_() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ATM90E32Component::run_gain_calibrations() {
|
void ATM90E32Component::run_gain_calibrations() {
|
||||||
const char *cs = this->cs_summary_.c_str();
|
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||||
|
this->get_cs_summary_(cs);
|
||||||
if (!this->enable_gain_calibration_) {
|
if (!this->enable_gain_calibration_) {
|
||||||
ESP_LOGW(TAG, "[CALIBRATION][%s] Gain calibration is disabled! Enable it first with enable_gain_calibration: true",
|
ESP_LOGW(TAG, "[CALIBRATION][%s] Gain calibration is disabled! Enable it first with enable_gain_calibration: true",
|
||||||
cs);
|
cs);
|
||||||
@@ -668,7 +674,8 @@ void ATM90E32Component::run_gain_calibrations() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ATM90E32Component::save_gain_calibration_to_memory_() {
|
void ATM90E32Component::save_gain_calibration_to_memory_() {
|
||||||
const char *cs = this->cs_summary_.c_str();
|
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||||
|
this->get_cs_summary_(cs);
|
||||||
bool success = this->gain_calibration_pref_.save(&this->gain_phase_);
|
bool success = this->gain_calibration_pref_.save(&this->gain_phase_);
|
||||||
global_preferences->sync();
|
global_preferences->sync();
|
||||||
if (success) {
|
if (success) {
|
||||||
@@ -681,7 +688,8 @@ void ATM90E32Component::save_gain_calibration_to_memory_() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ATM90E32Component::save_offset_calibration_to_memory_() {
|
void ATM90E32Component::save_offset_calibration_to_memory_() {
|
||||||
const char *cs = this->cs_summary_.c_str();
|
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||||
|
this->get_cs_summary_(cs);
|
||||||
bool success = this->offset_pref_.save(&this->offset_phase_);
|
bool success = this->offset_pref_.save(&this->offset_phase_);
|
||||||
global_preferences->sync();
|
global_preferences->sync();
|
||||||
if (success) {
|
if (success) {
|
||||||
@@ -697,7 +705,8 @@ void ATM90E32Component::save_offset_calibration_to_memory_() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ATM90E32Component::save_power_offset_calibration_to_memory_() {
|
void ATM90E32Component::save_power_offset_calibration_to_memory_() {
|
||||||
const char *cs = this->cs_summary_.c_str();
|
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||||
|
this->get_cs_summary_(cs);
|
||||||
bool success = this->power_offset_pref_.save(&this->power_offset_phase_);
|
bool success = this->power_offset_pref_.save(&this->power_offset_phase_);
|
||||||
global_preferences->sync();
|
global_preferences->sync();
|
||||||
if (success) {
|
if (success) {
|
||||||
@@ -713,7 +722,8 @@ void ATM90E32Component::save_power_offset_calibration_to_memory_() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ATM90E32Component::run_offset_calibrations() {
|
void ATM90E32Component::run_offset_calibrations() {
|
||||||
const char *cs = this->cs_summary_.c_str();
|
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||||
|
this->get_cs_summary_(cs);
|
||||||
if (!this->enable_offset_calibration_) {
|
if (!this->enable_offset_calibration_) {
|
||||||
ESP_LOGW(TAG,
|
ESP_LOGW(TAG,
|
||||||
"[CALIBRATION][%s] Offset calibration is disabled! Enable it first with enable_offset_calibration: true",
|
"[CALIBRATION][%s] Offset calibration is disabled! Enable it first with enable_offset_calibration: true",
|
||||||
@@ -743,7 +753,8 @@ void ATM90E32Component::run_offset_calibrations() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ATM90E32Component::run_power_offset_calibrations() {
|
void ATM90E32Component::run_power_offset_calibrations() {
|
||||||
const char *cs = this->cs_summary_.c_str();
|
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||||
|
this->get_cs_summary_(cs);
|
||||||
if (!this->enable_offset_calibration_) {
|
if (!this->enable_offset_calibration_) {
|
||||||
ESP_LOGW(
|
ESP_LOGW(
|
||||||
TAG,
|
TAG,
|
||||||
@@ -816,7 +827,8 @@ void ATM90E32Component::write_power_offsets_to_registers_(uint8_t phase, int16_t
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ATM90E32Component::restore_gain_calibrations_() {
|
void ATM90E32Component::restore_gain_calibrations_() {
|
||||||
const char *cs = this->cs_summary_.c_str();
|
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||||
|
this->get_cs_summary_(cs);
|
||||||
for (uint8_t i = 0; i < 3; ++i) {
|
for (uint8_t i = 0; i < 3; ++i) {
|
||||||
this->config_gain_phase_[i].voltage_gain = this->phase_[i].voltage_gain_;
|
this->config_gain_phase_[i].voltage_gain = this->phase_[i].voltage_gain_;
|
||||||
this->config_gain_phase_[i].current_gain = this->phase_[i].ct_gain_;
|
this->config_gain_phase_[i].current_gain = this->phase_[i].ct_gain_;
|
||||||
@@ -870,7 +882,8 @@ void ATM90E32Component::restore_gain_calibrations_() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ATM90E32Component::restore_offset_calibrations_() {
|
void ATM90E32Component::restore_offset_calibrations_() {
|
||||||
const char *cs = this->cs_summary_.c_str();
|
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||||
|
this->get_cs_summary_(cs);
|
||||||
for (uint8_t i = 0; i < 3; ++i)
|
for (uint8_t i = 0; i < 3; ++i)
|
||||||
this->config_offset_phase_[i] = this->offset_phase_[i];
|
this->config_offset_phase_[i] = this->offset_phase_[i];
|
||||||
|
|
||||||
@@ -912,7 +925,8 @@ void ATM90E32Component::restore_offset_calibrations_() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ATM90E32Component::restore_power_offset_calibrations_() {
|
void ATM90E32Component::restore_power_offset_calibrations_() {
|
||||||
const char *cs = this->cs_summary_.c_str();
|
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||||
|
this->get_cs_summary_(cs);
|
||||||
for (uint8_t i = 0; i < 3; ++i)
|
for (uint8_t i = 0; i < 3; ++i)
|
||||||
this->config_power_offset_phase_[i] = this->power_offset_phase_[i];
|
this->config_power_offset_phase_[i] = this->power_offset_phase_[i];
|
||||||
|
|
||||||
@@ -954,7 +968,8 @@ void ATM90E32Component::restore_power_offset_calibrations_() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ATM90E32Component::clear_gain_calibrations() {
|
void ATM90E32Component::clear_gain_calibrations() {
|
||||||
const char *cs = this->cs_summary_.c_str();
|
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||||
|
this->get_cs_summary_(cs);
|
||||||
if (!this->using_saved_calibrations_) {
|
if (!this->using_saved_calibrations_) {
|
||||||
ESP_LOGI(TAG, "[CALIBRATION][%s] No stored gain calibrations to clear. Current values:", cs);
|
ESP_LOGI(TAG, "[CALIBRATION][%s] No stored gain calibrations to clear. Current values:", cs);
|
||||||
ESP_LOGI(TAG, "[CALIBRATION][%s] ----------------------------------------------------------", cs);
|
ESP_LOGI(TAG, "[CALIBRATION][%s] ----------------------------------------------------------", cs);
|
||||||
@@ -1003,7 +1018,8 @@ void ATM90E32Component::clear_gain_calibrations() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ATM90E32Component::clear_offset_calibrations() {
|
void ATM90E32Component::clear_offset_calibrations() {
|
||||||
const char *cs = this->cs_summary_.c_str();
|
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||||
|
this->get_cs_summary_(cs);
|
||||||
if (!this->restored_offset_calibration_) {
|
if (!this->restored_offset_calibration_) {
|
||||||
ESP_LOGI(TAG, "[CALIBRATION][%s] No stored offset calibrations to clear. Current values:", cs);
|
ESP_LOGI(TAG, "[CALIBRATION][%s] No stored offset calibrations to clear. Current values:", cs);
|
||||||
ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs);
|
ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs);
|
||||||
@@ -1045,7 +1061,8 @@ void ATM90E32Component::clear_offset_calibrations() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ATM90E32Component::clear_power_offset_calibrations() {
|
void ATM90E32Component::clear_power_offset_calibrations() {
|
||||||
const char *cs = this->cs_summary_.c_str();
|
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||||
|
this->get_cs_summary_(cs);
|
||||||
if (!this->restored_power_offset_calibration_) {
|
if (!this->restored_power_offset_calibration_) {
|
||||||
ESP_LOGI(TAG, "[CALIBRATION][%s] No stored power offsets to clear. Current values:", cs);
|
ESP_LOGI(TAG, "[CALIBRATION][%s] No stored power offsets to clear. Current values:", cs);
|
||||||
ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs);
|
ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs);
|
||||||
@@ -1120,7 +1137,8 @@ int16_t ATM90E32Component::calibrate_power_offset(uint8_t phase, bool reactive)
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool ATM90E32Component::verify_gain_writes_() {
|
bool ATM90E32Component::verify_gain_writes_() {
|
||||||
const char *cs = this->cs_summary_.c_str();
|
char cs[GPIO_SUMMARY_MAX_LEN];
|
||||||
|
this->get_cs_summary_(cs);
|
||||||
bool success = true;
|
bool success = true;
|
||||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||||
uint16_t read_voltage = this->read16_(voltage_gain_registers[phase]);
|
uint16_t read_voltage = this->read16_(voltage_gain_registers[phase]);
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <span>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include "atm90e32_reg.h"
|
#include "atm90e32_reg.h"
|
||||||
#include "esphome/components/sensor/sensor.h"
|
#include "esphome/components/sensor/sensor.h"
|
||||||
#include "esphome/components/spi/spi.h"
|
#include "esphome/components/spi/spi.h"
|
||||||
#include "esphome/core/application.h"
|
#include "esphome/core/application.h"
|
||||||
#include "esphome/core/component.h"
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/core/gpio.h"
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/core/preferences.h"
|
#include "esphome/core/preferences.h"
|
||||||
|
|
||||||
@@ -182,6 +184,7 @@ class ATM90E32Component : public PollingComponent,
|
|||||||
bool verify_gain_writes_();
|
bool verify_gain_writes_();
|
||||||
bool validate_spi_read_(uint16_t expected, const char *context = nullptr);
|
bool validate_spi_read_(uint16_t expected, const char *context = nullptr);
|
||||||
void log_calibration_status_();
|
void log_calibration_status_();
|
||||||
|
void get_cs_summary_(std::span<char, GPIO_SUMMARY_MAX_LEN> buffer);
|
||||||
|
|
||||||
struct ATM90E32Phase {
|
struct ATM90E32Phase {
|
||||||
uint16_t voltage_gain_{0};
|
uint16_t voltage_gain_{0};
|
||||||
@@ -247,7 +250,6 @@ class ATM90E32Component : public PollingComponent,
|
|||||||
ESPPreferenceObject offset_pref_;
|
ESPPreferenceObject offset_pref_;
|
||||||
ESPPreferenceObject power_offset_pref_;
|
ESPPreferenceObject power_offset_pref_;
|
||||||
ESPPreferenceObject gain_calibration_pref_;
|
ESPPreferenceObject gain_calibration_pref_;
|
||||||
std::string cs_summary_;
|
|
||||||
|
|
||||||
sensor::Sensor *freq_sensor_{nullptr};
|
sensor::Sensor *freq_sensor_{nullptr};
|
||||||
#ifdef USE_TEXT_SENSOR
|
#ifdef USE_TEXT_SENSOR
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
|
from esphome.components.esp32 import add_idf_component, include_builtin_idf_component
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_BITS_PER_SAMPLE, CONF_NUM_CHANNELS, CONF_SAMPLE_RATE
|
from esphome.const import CONF_BITS_PER_SAMPLE, CONF_NUM_CHANNELS, CONF_SAMPLE_RATE
|
||||||
import esphome.final_validate as fv
|
import esphome.final_validate as fv
|
||||||
@@ -165,4 +166,10 @@ def final_validate_audio_schema(
|
|||||||
|
|
||||||
|
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
cg.add_library("esphome/esp-audio-libs", "2.0.1")
|
# Re-enable ESP-IDF's HTTP client (excluded by default to save compile time)
|
||||||
|
include_builtin_idf_component("esp_http_client")
|
||||||
|
|
||||||
|
add_idf_component(
|
||||||
|
name="esphome/esp-audio-libs",
|
||||||
|
ref="2.0.3",
|
||||||
|
)
|
||||||
|
|||||||
@@ -300,7 +300,7 @@ FileDecoderState AudioDecoder::decode_mp3_() {
|
|||||||
|
|
||||||
// Advance read pointer to match the offset for the syncword
|
// Advance read pointer to match the offset for the syncword
|
||||||
this->input_transfer_buffer_->decrease_buffer_length(offset);
|
this->input_transfer_buffer_->decrease_buffer_length(offset);
|
||||||
uint8_t *buffer_start = this->input_transfer_buffer_->get_buffer_start();
|
const uint8_t *buffer_start = this->input_transfer_buffer_->get_buffer_start();
|
||||||
|
|
||||||
buffer_length = (int) this->input_transfer_buffer_->available();
|
buffer_length = (int) this->input_transfer_buffer_->available();
|
||||||
int err = esp_audio_libs::helix_decoder::MP3Decode(this->mp3_decoder_, &buffer_start, &buffer_length,
|
int err = esp_audio_libs::helix_decoder::MP3Decode(this->mp3_decoder_, &buffer_start, &buffer_length,
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
#include "bedjet_hub.h"
|
#include "bedjet_hub.h"
|
||||||
#include "bedjet_child.h"
|
#include "bedjet_child.h"
|
||||||
#include "bedjet_const.h"
|
#include "bedjet_const.h"
|
||||||
#include "esphome/components/esp32_ble/ble_uuid.h"
|
|
||||||
#include "esphome/core/application.h"
|
#include "esphome/core/application.h"
|
||||||
#include <cinttypes>
|
#include <cinttypes>
|
||||||
|
|
||||||
|
|||||||
@@ -14,10 +14,7 @@ void log_binary_sensor(const char *tag, const char *prefix, const char *type, Bi
|
|||||||
}
|
}
|
||||||
|
|
||||||
ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str());
|
ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str());
|
||||||
|
LOG_ENTITY_DEVICE_CLASS(tag, prefix, *obj);
|
||||||
if (!obj->get_device_class_ref().empty()) {
|
|
||||||
ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj->get_device_class_ref().c_str());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void BinarySensor::publish_state(bool new_state) {
|
void BinarySensor::publish_state(bool new_state) {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ static const char *const TAG = "bl0940.number";
|
|||||||
void CalibrationNumber::setup() {
|
void CalibrationNumber::setup() {
|
||||||
float value = 0.0f;
|
float value = 0.0f;
|
||||||
if (this->restore_value_) {
|
if (this->restore_value_) {
|
||||||
this->pref_ = global_preferences->make_preference<float>(this->get_preference_hash());
|
this->pref_ = this->make_entity_preference<float>();
|
||||||
if (!this->pref_.load(&value)) {
|
if (!this->pref_.load(&value)) {
|
||||||
value = 0.0f;
|
value = 0.0f;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,8 +135,8 @@ void BluetoothConnection::loop() {
|
|||||||
// - For V3_WITH_CACHE: Services are never sent, disable after INIT state
|
// - For V3_WITH_CACHE: Services are never sent, disable after INIT state
|
||||||
// - For V3_WITHOUT_CACHE: Disable only after service discovery is complete
|
// - For V3_WITHOUT_CACHE: Disable only after service discovery is complete
|
||||||
// (send_service_ == DONE_SENDING_SERVICES, which is only set after services are sent)
|
// (send_service_ == DONE_SENDING_SERVICES, which is only set after services are sent)
|
||||||
if (this->state_ != espbt::ClientState::INIT && (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
|
if (this->state() != espbt::ClientState::INIT && (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
|
||||||
this->send_service_ == DONE_SENDING_SERVICES)) {
|
this->send_service_ == DONE_SENDING_SERVICES)) {
|
||||||
this->disable_loop();
|
this->disable_loop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,164 +1,5 @@
|
|||||||
import math
|
|
||||||
|
|
||||||
import esphome.codegen as cg
|
|
||||||
from esphome.components import i2c, sensor
|
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
|
||||||
CONF_ID,
|
CONFIG_SCHEMA = cv.invalid(
|
||||||
CONF_IIR_FILTER,
|
"The bmp581 sensor component has been renamed to bmp581_i2c."
|
||||||
CONF_OVERSAMPLING,
|
|
||||||
CONF_PRESSURE,
|
|
||||||
CONF_TEMPERATURE,
|
|
||||||
DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
|
|
||||||
DEVICE_CLASS_TEMPERATURE,
|
|
||||||
STATE_CLASS_MEASUREMENT,
|
|
||||||
UNIT_CELSIUS,
|
|
||||||
UNIT_PASCAL,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
CODEOWNERS = ["@kahrendt"]
|
|
||||||
DEPENDENCIES = ["i2c"]
|
|
||||||
|
|
||||||
bmp581_ns = cg.esphome_ns.namespace("bmp581")
|
|
||||||
|
|
||||||
Oversampling = bmp581_ns.enum("Oversampling")
|
|
||||||
OVERSAMPLING_OPTIONS = {
|
|
||||||
"NONE": Oversampling.OVERSAMPLING_NONE,
|
|
||||||
"2X": Oversampling.OVERSAMPLING_X2,
|
|
||||||
"4X": Oversampling.OVERSAMPLING_X4,
|
|
||||||
"8X": Oversampling.OVERSAMPLING_X8,
|
|
||||||
"16X": Oversampling.OVERSAMPLING_X16,
|
|
||||||
"32X": Oversampling.OVERSAMPLING_X32,
|
|
||||||
"64X": Oversampling.OVERSAMPLING_X64,
|
|
||||||
"128X": Oversampling.OVERSAMPLING_X128,
|
|
||||||
}
|
|
||||||
|
|
||||||
IIRFilter = bmp581_ns.enum("IIRFilter")
|
|
||||||
IIR_FILTER_OPTIONS = {
|
|
||||||
"OFF": IIRFilter.IIR_FILTER_OFF,
|
|
||||||
"2X": IIRFilter.IIR_FILTER_2,
|
|
||||||
"4X": IIRFilter.IIR_FILTER_4,
|
|
||||||
"8X": IIRFilter.IIR_FILTER_8,
|
|
||||||
"16X": IIRFilter.IIR_FILTER_16,
|
|
||||||
"32X": IIRFilter.IIR_FILTER_32,
|
|
||||||
"64X": IIRFilter.IIR_FILTER_64,
|
|
||||||
"128X": IIRFilter.IIR_FILTER_128,
|
|
||||||
}
|
|
||||||
|
|
||||||
BMP581Component = bmp581_ns.class_(
|
|
||||||
"BMP581Component", cg.PollingComponent, i2c.I2CDevice
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def compute_measurement_conversion_time(config):
|
|
||||||
# - adds up sensor conversion time based on temperature and pressure oversampling rates given in datasheet
|
|
||||||
# - returns a rounded up time in ms
|
|
||||||
|
|
||||||
# Page 12 of datasheet
|
|
||||||
PRESSURE_OVERSAMPLING_CONVERSION_TIMES = {
|
|
||||||
"NONE": 1.0,
|
|
||||||
"2X": 1.7,
|
|
||||||
"4X": 2.9,
|
|
||||||
"8X": 5.4,
|
|
||||||
"16X": 10.4,
|
|
||||||
"32X": 20.4,
|
|
||||||
"64X": 40.4,
|
|
||||||
"128X": 80.4,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Page 12 of datasheet
|
|
||||||
TEMPERATURE_OVERSAMPLING_CONVERSION_TIMES = {
|
|
||||||
"NONE": 1.0,
|
|
||||||
"2X": 1.1,
|
|
||||||
"4X": 1.5,
|
|
||||||
"8X": 2.1,
|
|
||||||
"16X": 3.3,
|
|
||||||
"32X": 5.8,
|
|
||||||
"64X": 10.8,
|
|
||||||
"128X": 20.8,
|
|
||||||
}
|
|
||||||
|
|
||||||
pressure_conversion_time = (
|
|
||||||
0.0 # No conversion time necessary without a pressure sensor
|
|
||||||
)
|
|
||||||
if pressure_config := config.get(CONF_PRESSURE):
|
|
||||||
pressure_conversion_time = PRESSURE_OVERSAMPLING_CONVERSION_TIMES[
|
|
||||||
pressure_config.get(CONF_OVERSAMPLING)
|
|
||||||
]
|
|
||||||
|
|
||||||
temperature_conversion_time = (
|
|
||||||
1.0 # BMP581 always samples the temperature even if only reading pressure
|
|
||||||
)
|
|
||||||
if temperature_config := config.get(CONF_TEMPERATURE):
|
|
||||||
temperature_conversion_time = TEMPERATURE_OVERSAMPLING_CONVERSION_TIMES[
|
|
||||||
temperature_config.get(CONF_OVERSAMPLING)
|
|
||||||
]
|
|
||||||
|
|
||||||
# Datasheet indicates a 5% possible error in each conversion time listed
|
|
||||||
return math.ceil(1.05 * (pressure_conversion_time + temperature_conversion_time))
|
|
||||||
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = (
|
|
||||||
cv.Schema(
|
|
||||||
{
|
|
||||||
cv.GenerateID(): cv.declare_id(BMP581Component),
|
|
||||||
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
|
||||||
unit_of_measurement=UNIT_CELSIUS,
|
|
||||||
accuracy_decimals=1,
|
|
||||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
|
||||||
state_class=STATE_CLASS_MEASUREMENT,
|
|
||||||
).extend(
|
|
||||||
{
|
|
||||||
cv.Optional(CONF_OVERSAMPLING, default="NONE"): cv.enum(
|
|
||||||
OVERSAMPLING_OPTIONS, upper=True
|
|
||||||
),
|
|
||||||
cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum(
|
|
||||||
IIR_FILTER_OPTIONS, upper=True
|
|
||||||
),
|
|
||||||
}
|
|
||||||
),
|
|
||||||
cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
|
|
||||||
unit_of_measurement=UNIT_PASCAL,
|
|
||||||
accuracy_decimals=0,
|
|
||||||
device_class=DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
|
|
||||||
state_class=STATE_CLASS_MEASUREMENT,
|
|
||||||
).extend(
|
|
||||||
{
|
|
||||||
cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
|
|
||||||
OVERSAMPLING_OPTIONS, upper=True
|
|
||||||
),
|
|
||||||
cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum(
|
|
||||||
IIR_FILTER_OPTIONS, upper=True
|
|
||||||
),
|
|
||||||
}
|
|
||||||
),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.extend(cv.polling_component_schema("60s"))
|
|
||||||
.extend(i2c.i2c_device_schema(0x46))
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def to_code(config):
|
|
||||||
var = cg.new_Pvariable(config[CONF_ID])
|
|
||||||
await cg.register_component(var, config)
|
|
||||||
await i2c.register_i2c_device(var, config)
|
|
||||||
if temperature_config := config.get(CONF_TEMPERATURE):
|
|
||||||
sens = await sensor.new_sensor(temperature_config)
|
|
||||||
cg.add(var.set_temperature_sensor(sens))
|
|
||||||
cg.add(
|
|
||||||
var.set_temperature_oversampling_config(
|
|
||||||
temperature_config[CONF_OVERSAMPLING]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
cg.add(
|
|
||||||
var.set_temperature_iir_filter_config(temperature_config[CONF_IIR_FILTER])
|
|
||||||
)
|
|
||||||
|
|
||||||
if pressure_config := config.get(CONF_PRESSURE):
|
|
||||||
sens = await sensor.new_sensor(pressure_config)
|
|
||||||
cg.add(var.set_pressure_sensor(sens))
|
|
||||||
cg.add(var.set_pressure_oversampling_config(pressure_config[CONF_OVERSAMPLING]))
|
|
||||||
cg.add(var.set_pressure_iir_filter_config(pressure_config[CONF_IIR_FILTER]))
|
|
||||||
|
|
||||||
cg.add(var.set_conversion_time(compute_measurement_conversion_time(config)))
|
|
||||||
|
|||||||
157
esphome/components/bmp581_base/__init__.py
Normal file
157
esphome/components/bmp581_base/__init__.py
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import math
|
||||||
|
|
||||||
|
import esphome.codegen as cg
|
||||||
|
from esphome.components import sensor
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.const import (
|
||||||
|
CONF_ID,
|
||||||
|
CONF_IIR_FILTER,
|
||||||
|
CONF_OVERSAMPLING,
|
||||||
|
CONF_PRESSURE,
|
||||||
|
CONF_TEMPERATURE,
|
||||||
|
DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
|
||||||
|
DEVICE_CLASS_TEMPERATURE,
|
||||||
|
STATE_CLASS_MEASUREMENT,
|
||||||
|
UNIT_CELSIUS,
|
||||||
|
UNIT_PASCAL,
|
||||||
|
)
|
||||||
|
|
||||||
|
CODEOWNERS = ["@kahrendt", "@danielkent-net"]
|
||||||
|
|
||||||
|
bmp581_ns = cg.esphome_ns.namespace("bmp581_base")
|
||||||
|
|
||||||
|
Oversampling = bmp581_ns.enum("Oversampling")
|
||||||
|
OVERSAMPLING_OPTIONS = {
|
||||||
|
"NONE": Oversampling.OVERSAMPLING_NONE,
|
||||||
|
"2X": Oversampling.OVERSAMPLING_X2,
|
||||||
|
"4X": Oversampling.OVERSAMPLING_X4,
|
||||||
|
"8X": Oversampling.OVERSAMPLING_X8,
|
||||||
|
"16X": Oversampling.OVERSAMPLING_X16,
|
||||||
|
"32X": Oversampling.OVERSAMPLING_X32,
|
||||||
|
"64X": Oversampling.OVERSAMPLING_X64,
|
||||||
|
"128X": Oversampling.OVERSAMPLING_X128,
|
||||||
|
}
|
||||||
|
|
||||||
|
IIRFilter = bmp581_ns.enum("IIRFilter")
|
||||||
|
IIR_FILTER_OPTIONS = {
|
||||||
|
"OFF": IIRFilter.IIR_FILTER_OFF,
|
||||||
|
"2X": IIRFilter.IIR_FILTER_2,
|
||||||
|
"4X": IIRFilter.IIR_FILTER_4,
|
||||||
|
"8X": IIRFilter.IIR_FILTER_8,
|
||||||
|
"16X": IIRFilter.IIR_FILTER_16,
|
||||||
|
"32X": IIRFilter.IIR_FILTER_32,
|
||||||
|
"64X": IIRFilter.IIR_FILTER_64,
|
||||||
|
"128X": IIRFilter.IIR_FILTER_128,
|
||||||
|
}
|
||||||
|
|
||||||
|
BMP581Component = bmp581_ns.class_("BMP581Component", cg.PollingComponent)
|
||||||
|
|
||||||
|
|
||||||
|
def compute_measurement_conversion_time(config):
|
||||||
|
# - adds up sensor conversion time based on temperature and pressure oversampling rates given in datasheet
|
||||||
|
# - returns a rounded up time in ms
|
||||||
|
|
||||||
|
# Page 12 of datasheet
|
||||||
|
PRESSURE_OVERSAMPLING_CONVERSION_TIMES = {
|
||||||
|
"NONE": 1.0,
|
||||||
|
"2X": 1.7,
|
||||||
|
"4X": 2.9,
|
||||||
|
"8X": 5.4,
|
||||||
|
"16X": 10.4,
|
||||||
|
"32X": 20.4,
|
||||||
|
"64X": 40.4,
|
||||||
|
"128X": 80.4,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Page 12 of datasheet
|
||||||
|
TEMPERATURE_OVERSAMPLING_CONVERSION_TIMES = {
|
||||||
|
"NONE": 1.0,
|
||||||
|
"2X": 1.1,
|
||||||
|
"4X": 1.5,
|
||||||
|
"8X": 2.1,
|
||||||
|
"16X": 3.3,
|
||||||
|
"32X": 5.8,
|
||||||
|
"64X": 10.8,
|
||||||
|
"128X": 20.8,
|
||||||
|
}
|
||||||
|
|
||||||
|
pressure_conversion_time = (
|
||||||
|
0.0 # No conversion time necessary without a pressure sensor
|
||||||
|
)
|
||||||
|
if pressure_config := config.get(CONF_PRESSURE):
|
||||||
|
pressure_conversion_time = PRESSURE_OVERSAMPLING_CONVERSION_TIMES[
|
||||||
|
pressure_config.get(CONF_OVERSAMPLING)
|
||||||
|
]
|
||||||
|
|
||||||
|
temperature_conversion_time = (
|
||||||
|
1.0 # BMP581 always samples the temperature even if only reading pressure
|
||||||
|
)
|
||||||
|
if temperature_config := config.get(CONF_TEMPERATURE):
|
||||||
|
temperature_conversion_time = TEMPERATURE_OVERSAMPLING_CONVERSION_TIMES[
|
||||||
|
temperature_config.get(CONF_OVERSAMPLING)
|
||||||
|
]
|
||||||
|
|
||||||
|
# Datasheet indicates a 5% possible error in each conversion time listed
|
||||||
|
return math.ceil(1.05 * (pressure_conversion_time + temperature_conversion_time))
|
||||||
|
|
||||||
|
|
||||||
|
CONFIG_SCHEMA_BASE = cv.Schema(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(BMP581Component),
|
||||||
|
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
||||||
|
unit_of_measurement=UNIT_CELSIUS,
|
||||||
|
accuracy_decimals=1,
|
||||||
|
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||||
|
state_class=STATE_CLASS_MEASUREMENT,
|
||||||
|
).extend(
|
||||||
|
{
|
||||||
|
cv.Optional(CONF_OVERSAMPLING, default="NONE"): cv.enum(
|
||||||
|
OVERSAMPLING_OPTIONS, upper=True
|
||||||
|
),
|
||||||
|
cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum(
|
||||||
|
IIR_FILTER_OPTIONS, upper=True
|
||||||
|
),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
|
||||||
|
unit_of_measurement=UNIT_PASCAL,
|
||||||
|
accuracy_decimals=0,
|
||||||
|
device_class=DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
|
||||||
|
state_class=STATE_CLASS_MEASUREMENT,
|
||||||
|
).extend(
|
||||||
|
{
|
||||||
|
cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
|
||||||
|
OVERSAMPLING_OPTIONS, upper=True
|
||||||
|
),
|
||||||
|
cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum(
|
||||||
|
IIR_FILTER_OPTIONS, upper=True
|
||||||
|
),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
}
|
||||||
|
).extend(cv.polling_component_schema("60s"))
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code_base(config):
|
||||||
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
|
await cg.register_component(var, config)
|
||||||
|
if temperature_config := config.get(CONF_TEMPERATURE):
|
||||||
|
sens = await sensor.new_sensor(temperature_config)
|
||||||
|
cg.add(var.set_temperature_sensor(sens))
|
||||||
|
cg.add(
|
||||||
|
var.set_temperature_oversampling_config(
|
||||||
|
temperature_config[CONF_OVERSAMPLING]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
cg.add(
|
||||||
|
var.set_temperature_iir_filter_config(temperature_config[CONF_IIR_FILTER])
|
||||||
|
)
|
||||||
|
|
||||||
|
if pressure_config := config.get(CONF_PRESSURE):
|
||||||
|
sens = await sensor.new_sensor(pressure_config)
|
||||||
|
cg.add(var.set_pressure_sensor(sens))
|
||||||
|
cg.add(var.set_pressure_oversampling_config(pressure_config[CONF_OVERSAMPLING]))
|
||||||
|
cg.add(var.set_pressure_iir_filter_config(pressure_config[CONF_IIR_FILTER]))
|
||||||
|
|
||||||
|
cg.add(var.set_conversion_time(compute_measurement_conversion_time(config)))
|
||||||
|
return var
|
||||||
@@ -10,12 +10,11 @@
|
|||||||
* - All datasheet page references refer to Bosch Document Number BST-BMP581-DS004-04 (revision number 1.4)
|
* - All datasheet page references refer to Bosch Document Number BST-BMP581-DS004-04 (revision number 1.4)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "bmp581.h"
|
#include "bmp581_base.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
#include "esphome/core/hal.h"
|
#include "esphome/core/hal.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome::bmp581_base {
|
||||||
namespace bmp581 {
|
|
||||||
|
|
||||||
static const char *const TAG = "bmp581";
|
static const char *const TAG = "bmp581";
|
||||||
|
|
||||||
@@ -91,7 +90,6 @@ void BMP581Component::dump_config() {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_I2C_DEVICE(this);
|
|
||||||
LOG_UPDATE_INTERVAL(this);
|
LOG_UPDATE_INTERVAL(this);
|
||||||
|
|
||||||
ESP_LOGCONFIG(TAG, " Measurement conversion time: %ums", this->conversion_time_);
|
ESP_LOGCONFIG(TAG, " Measurement conversion time: %ums", this->conversion_time_);
|
||||||
@@ -149,7 +147,7 @@ void BMP581Component::setup() {
|
|||||||
uint8_t chip_id;
|
uint8_t chip_id;
|
||||||
|
|
||||||
// read chip id from sensor
|
// read chip id from sensor
|
||||||
if (!this->read_byte(BMP581_CHIP_ID, &chip_id)) {
|
if (!this->bmp_read_byte(BMP581_CHIP_ID, &chip_id)) {
|
||||||
ESP_LOGE(TAG, "Read chip ID failed");
|
ESP_LOGE(TAG, "Read chip ID failed");
|
||||||
|
|
||||||
this->error_code_ = ERROR_COMMUNICATION_FAILED;
|
this->error_code_ = ERROR_COMMUNICATION_FAILED;
|
||||||
@@ -172,7 +170,7 @@ void BMP581Component::setup() {
|
|||||||
// 3) Verify sensor status (check if NVM is okay) //
|
// 3) Verify sensor status (check if NVM is okay) //
|
||||||
////////////////////////////////////////////////////
|
////////////////////////////////////////////////////
|
||||||
|
|
||||||
if (!this->read_byte(BMP581_STATUS, &this->status_.reg)) {
|
if (!this->bmp_read_byte(BMP581_STATUS, &this->status_.reg)) {
|
||||||
ESP_LOGE(TAG, "Failed to read status register");
|
ESP_LOGE(TAG, "Failed to read status register");
|
||||||
|
|
||||||
this->error_code_ = ERROR_COMMUNICATION_FAILED;
|
this->error_code_ = ERROR_COMMUNICATION_FAILED;
|
||||||
@@ -359,7 +357,7 @@ bool BMP581Component::check_data_readiness_() {
|
|||||||
|
|
||||||
uint8_t status;
|
uint8_t status;
|
||||||
|
|
||||||
if (!this->read_byte(BMP581_INT_STATUS, &status)) {
|
if (!this->bmp_read_byte(BMP581_INT_STATUS, &status)) {
|
||||||
ESP_LOGE(TAG, "Failed to read interrupt status register");
|
ESP_LOGE(TAG, "Failed to read interrupt status register");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -400,7 +398,7 @@ bool BMP581Component::prime_iir_filter_() {
|
|||||||
|
|
||||||
// flush the IIR filter with forced measurements (we will only flush once)
|
// flush the IIR filter with forced measurements (we will only flush once)
|
||||||
this->dsp_config_.bit.iir_flush_forced_en = true;
|
this->dsp_config_.bit.iir_flush_forced_en = true;
|
||||||
if (!this->write_byte(BMP581_DSP, this->dsp_config_.reg)) {
|
if (!this->bmp_write_byte(BMP581_DSP, this->dsp_config_.reg)) {
|
||||||
ESP_LOGE(TAG, "Failed to write IIR source register");
|
ESP_LOGE(TAG, "Failed to write IIR source register");
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -430,7 +428,7 @@ bool BMP581Component::prime_iir_filter_() {
|
|||||||
|
|
||||||
// disable IIR filter flushings on future forced measurements
|
// disable IIR filter flushings on future forced measurements
|
||||||
this->dsp_config_.bit.iir_flush_forced_en = false;
|
this->dsp_config_.bit.iir_flush_forced_en = false;
|
||||||
if (!this->write_byte(BMP581_DSP, this->dsp_config_.reg)) {
|
if (!this->bmp_write_byte(BMP581_DSP, this->dsp_config_.reg)) {
|
||||||
ESP_LOGE(TAG, "Failed to write IIR source register");
|
ESP_LOGE(TAG, "Failed to write IIR source register");
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -454,7 +452,7 @@ bool BMP581Component::read_temperature_(float &temperature) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
uint8_t data[3];
|
uint8_t data[3];
|
||||||
if (!this->read_bytes(BMP581_MEASUREMENT_DATA, &data[0], 3)) {
|
if (!this->bmp_read_bytes(BMP581_MEASUREMENT_DATA, &data[0], 3)) {
|
||||||
ESP_LOGW(TAG, "Failed to read measurement");
|
ESP_LOGW(TAG, "Failed to read measurement");
|
||||||
this->status_set_warning();
|
this->status_set_warning();
|
||||||
|
|
||||||
@@ -483,7 +481,7 @@ bool BMP581Component::read_temperature_and_pressure_(float &temperature, float &
|
|||||||
}
|
}
|
||||||
|
|
||||||
uint8_t data[6];
|
uint8_t data[6];
|
||||||
if (!this->read_bytes(BMP581_MEASUREMENT_DATA, &data[0], 6)) {
|
if (!this->bmp_read_bytes(BMP581_MEASUREMENT_DATA, &data[0], 6)) {
|
||||||
ESP_LOGW(TAG, "Failed to read measurement");
|
ESP_LOGW(TAG, "Failed to read measurement");
|
||||||
this->status_set_warning();
|
this->status_set_warning();
|
||||||
|
|
||||||
@@ -507,7 +505,7 @@ bool BMP581Component::reset_() {
|
|||||||
// - returns the Power-On-Reboot interrupt status, which is asserted if successful
|
// - returns the Power-On-Reboot interrupt status, which is asserted if successful
|
||||||
|
|
||||||
// writes reset command to BMP's command register
|
// writes reset command to BMP's command register
|
||||||
if (!this->write_byte(BMP581_COMMAND, RESET_COMMAND)) {
|
if (!this->bmp_write_byte(BMP581_COMMAND, RESET_COMMAND)) {
|
||||||
ESP_LOGE(TAG, "Failed to write reset command");
|
ESP_LOGE(TAG, "Failed to write reset command");
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -518,7 +516,7 @@ bool BMP581Component::reset_() {
|
|||||||
delay(3);
|
delay(3);
|
||||||
|
|
||||||
// read interrupt status register
|
// read interrupt status register
|
||||||
if (!this->read_byte(BMP581_INT_STATUS, &this->int_status_.reg)) {
|
if (!this->bmp_read_byte(BMP581_INT_STATUS, &this->int_status_.reg)) {
|
||||||
ESP_LOGE(TAG, "Failed to read interrupt status register");
|
ESP_LOGE(TAG, "Failed to read interrupt status register");
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -562,7 +560,7 @@ bool BMP581Component::write_iir_settings_(IIRFilter temperature_iir, IIRFilter p
|
|||||||
// BMP581_DSP register and BMP581_DSP_IIR registers are successive
|
// BMP581_DSP register and BMP581_DSP_IIR registers are successive
|
||||||
// - allows us to write the IIR configuration with one command to both registers
|
// - allows us to write the IIR configuration with one command to both registers
|
||||||
uint8_t register_data[2] = {this->dsp_config_.reg, this->iir_config_.reg};
|
uint8_t register_data[2] = {this->dsp_config_.reg, this->iir_config_.reg};
|
||||||
return this->write_bytes(BMP581_DSP, register_data, sizeof(register_data));
|
return this->bmp_write_bytes(BMP581_DSP, register_data, sizeof(register_data));
|
||||||
}
|
}
|
||||||
|
|
||||||
bool BMP581Component::write_interrupt_source_settings_(bool data_ready_enable) {
|
bool BMP581Component::write_interrupt_source_settings_(bool data_ready_enable) {
|
||||||
@@ -572,7 +570,7 @@ bool BMP581Component::write_interrupt_source_settings_(bool data_ready_enable) {
|
|||||||
this->int_source_.bit.drdy_data_reg_en = data_ready_enable;
|
this->int_source_.bit.drdy_data_reg_en = data_ready_enable;
|
||||||
|
|
||||||
// write interrupt source register
|
// write interrupt source register
|
||||||
return this->write_byte(BMP581_INT_SOURCE, this->int_source_.reg);
|
return this->bmp_write_byte(BMP581_INT_SOURCE, this->int_source_.reg);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool BMP581Component::write_oversampling_settings_(Oversampling temperature_oversampling,
|
bool BMP581Component::write_oversampling_settings_(Oversampling temperature_oversampling,
|
||||||
@@ -583,7 +581,7 @@ bool BMP581Component::write_oversampling_settings_(Oversampling temperature_over
|
|||||||
this->osr_config_.bit.osr_t = temperature_oversampling;
|
this->osr_config_.bit.osr_t = temperature_oversampling;
|
||||||
this->osr_config_.bit.osr_p = pressure_oversampling;
|
this->osr_config_.bit.osr_p = pressure_oversampling;
|
||||||
|
|
||||||
return this->write_byte(BMP581_OSR, this->osr_config_.reg);
|
return this->bmp_write_byte(BMP581_OSR, this->osr_config_.reg);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool BMP581Component::write_power_mode_(OperationMode mode) {
|
bool BMP581Component::write_power_mode_(OperationMode mode) {
|
||||||
@@ -593,8 +591,7 @@ bool BMP581Component::write_power_mode_(OperationMode mode) {
|
|||||||
this->odr_config_.bit.pwr_mode = mode;
|
this->odr_config_.bit.pwr_mode = mode;
|
||||||
|
|
||||||
// write odr register
|
// write odr register
|
||||||
return this->write_byte(BMP581_ODR, this->odr_config_.reg);
|
return this->bmp_write_byte(BMP581_ODR, this->odr_config_.reg);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace bmp581
|
} // namespace esphome::bmp581_base
|
||||||
} // namespace esphome
|
|
||||||
@@ -3,11 +3,9 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "esphome/core/component.h"
|
#include "esphome/core/component.h"
|
||||||
#include "esphome/components/i2c/i2c.h"
|
|
||||||
#include "esphome/components/sensor/sensor.h"
|
#include "esphome/components/sensor/sensor.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome::bmp581_base {
|
||||||
namespace bmp581 {
|
|
||||||
|
|
||||||
static const uint8_t BMP581_ASIC_ID = 0x50; // BMP581's ASIC chip ID (page 51 of datasheet)
|
static const uint8_t BMP581_ASIC_ID = 0x50; // BMP581's ASIC chip ID (page 51 of datasheet)
|
||||||
static const uint8_t RESET_COMMAND = 0xB6; // Soft reset command
|
static const uint8_t RESET_COMMAND = 0xB6; // Soft reset command
|
||||||
@@ -59,7 +57,7 @@ enum IIRFilter {
|
|||||||
IIR_FILTER_128 = 0x7
|
IIR_FILTER_128 = 0x7
|
||||||
};
|
};
|
||||||
|
|
||||||
class BMP581Component : public PollingComponent, public i2c::I2CDevice {
|
class BMP581Component : public PollingComponent {
|
||||||
public:
|
public:
|
||||||
void dump_config() override;
|
void dump_config() override;
|
||||||
|
|
||||||
@@ -84,6 +82,11 @@ class BMP581Component : public PollingComponent, public i2c::I2CDevice {
|
|||||||
void set_conversion_time(uint8_t conversion_time) { this->conversion_time_ = conversion_time; }
|
void set_conversion_time(uint8_t conversion_time) { this->conversion_time_ = conversion_time; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
virtual bool bmp_read_byte(uint8_t a_register, uint8_t *data) = 0;
|
||||||
|
virtual bool bmp_write_byte(uint8_t a_register, uint8_t data) = 0;
|
||||||
|
virtual bool bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0;
|
||||||
|
virtual bool bmp_write_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0;
|
||||||
|
|
||||||
sensor::Sensor *temperature_sensor_{nullptr};
|
sensor::Sensor *temperature_sensor_{nullptr};
|
||||||
sensor::Sensor *pressure_sensor_{nullptr};
|
sensor::Sensor *pressure_sensor_{nullptr};
|
||||||
|
|
||||||
@@ -216,5 +219,4 @@ class BMP581Component : public PollingComponent, public i2c::I2CDevice {
|
|||||||
} odr_config_ = {.reg = 0};
|
} odr_config_ = {.reg = 0};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace bmp581
|
} // namespace esphome::bmp581_base
|
||||||
} // namespace esphome
|
|
||||||
0
esphome/components/bmp581_i2c/__init__.py
Normal file
0
esphome/components/bmp581_i2c/__init__.py
Normal file
12
esphome/components/bmp581_i2c/bmp581_i2c.cpp
Normal file
12
esphome/components/bmp581_i2c/bmp581_i2c.cpp
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
#include "bmp581_i2c.h"
|
||||||
|
#include "esphome/core/hal.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
namespace esphome::bmp581_i2c {
|
||||||
|
|
||||||
|
void BMP581I2CComponent::dump_config() {
|
||||||
|
LOG_I2C_DEVICE(this);
|
||||||
|
BMP581Component::dump_config();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace esphome::bmp581_i2c
|
||||||
24
esphome/components/bmp581_i2c/bmp581_i2c.h
Normal file
24
esphome/components/bmp581_i2c/bmp581_i2c.h
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/components/bmp581_base/bmp581_base.h"
|
||||||
|
#include "esphome/components/i2c/i2c.h"
|
||||||
|
|
||||||
|
namespace esphome::bmp581_i2c {
|
||||||
|
|
||||||
|
static const char *const TAG = "bmp581_i2c.sensor";
|
||||||
|
|
||||||
|
/// This class implements support for the BMP581 Temperature+Pressure i2c sensor.
|
||||||
|
class BMP581I2CComponent : public esphome::bmp581_base::BMP581Component, public i2c::I2CDevice {
|
||||||
|
public:
|
||||||
|
bool bmp_read_byte(uint8_t a_register, uint8_t *data) override { return read_byte(a_register, data); }
|
||||||
|
bool bmp_write_byte(uint8_t a_register, uint8_t data) override { return write_byte(a_register, data); }
|
||||||
|
bool bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) override {
|
||||||
|
return read_bytes(a_register, data, len);
|
||||||
|
}
|
||||||
|
bool bmp_write_bytes(uint8_t a_register, uint8_t *data, size_t len) override {
|
||||||
|
return write_bytes(a_register, data, len);
|
||||||
|
}
|
||||||
|
void dump_config() override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace esphome::bmp581_i2c
|
||||||
23
esphome/components/bmp581_i2c/sensor.py
Normal file
23
esphome/components/bmp581_i2c/sensor.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import esphome.codegen as cg
|
||||||
|
from esphome.components import i2c
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
|
||||||
|
from ..bmp581_base import CONFIG_SCHEMA_BASE, to_code_base
|
||||||
|
|
||||||
|
AUTO_LOAD = ["bmp581_base"]
|
||||||
|
CODEOWNERS = ["@kahrendt", "@danielkent-net"]
|
||||||
|
DEPENDENCIES = ["i2c"]
|
||||||
|
|
||||||
|
bmp581_ns = cg.esphome_ns.namespace("bmp581_i2c")
|
||||||
|
BMP581I2CComponent = bmp581_ns.class_(
|
||||||
|
"BMP581I2CComponent", cg.PollingComponent, i2c.I2CDevice
|
||||||
|
)
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend(
|
||||||
|
i2c.i2c_device_schema(default_address=0x46)
|
||||||
|
).extend({cv.GenerateID(): cv.declare_id(BMP581I2CComponent)})
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
var = await to_code_base(config)
|
||||||
|
await i2c.register_i2c_device(var, config)
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components import esp32_ble_tracker
|
from esphome.components import esp32_ble_tracker
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_ID, CONF_MAC_ADDRESS
|
from esphome.const import CONF_BINDKEY, CONF_ID, CONF_MAC_ADDRESS
|
||||||
|
from esphome.core import HexInt
|
||||||
|
|
||||||
CODEOWNERS = ["@nagyrobi"]
|
CODEOWNERS = ["@nagyrobi"]
|
||||||
DEPENDENCIES = ["esp32_ble_tracker"]
|
DEPENDENCIES = ["esp32_ble_tracker"]
|
||||||
@@ -22,6 +23,7 @@ def bthome_mithermometer_base_schema(extra_schema=None):
|
|||||||
{
|
{
|
||||||
cv.GenerateID(CONF_ID): cv.declare_id(BTHomeMiThermometer),
|
cv.GenerateID(CONF_ID): cv.declare_id(BTHomeMiThermometer),
|
||||||
cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
|
cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
|
||||||
|
cv.Optional(CONF_BINDKEY): cv.bind_key,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.extend(BLE_DEVICE_SCHEMA)
|
.extend(BLE_DEVICE_SCHEMA)
|
||||||
@@ -34,3 +36,9 @@ async def setup_bthome_mithermometer(var, config):
|
|||||||
await cg.register_component(var, config)
|
await cg.register_component(var, config)
|
||||||
await esp32_ble_tracker.register_ble_device(var, config)
|
await esp32_ble_tracker.register_ble_device(var, config)
|
||||||
cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
|
cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
|
||||||
|
if bindkey := config.get(CONF_BINDKEY):
|
||||||
|
bindkey_bytes = [
|
||||||
|
HexInt(int(bindkey[index : index + 2], 16))
|
||||||
|
for index in range(0, len(bindkey), 2)
|
||||||
|
]
|
||||||
|
cg.add(var.set_bindkey(cg.ArrayInitializer(*bindkey_bytes)))
|
||||||
|
|||||||
@@ -3,15 +3,23 @@
|
|||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <array>
|
#include <array>
|
||||||
|
#include <cstring>
|
||||||
#include <span>
|
#include <span>
|
||||||
|
|
||||||
#ifdef USE_ESP32
|
#ifdef USE_ESP32
|
||||||
|
|
||||||
|
#include "mbedtls/ccm.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace bthome_mithermometer {
|
namespace bthome_mithermometer {
|
||||||
|
|
||||||
static const char *const TAG = "bthome_mithermometer";
|
static const char *const TAG = "bthome_mithermometer";
|
||||||
|
static constexpr size_t BTHOME_BINDKEY_SIZE = 16;
|
||||||
|
static constexpr size_t BTHOME_NONCE_SIZE = 13;
|
||||||
|
static constexpr size_t BTHOME_MIC_SIZE = 4;
|
||||||
|
static constexpr size_t BTHOME_COUNTER_SIZE = 4;
|
||||||
|
|
||||||
static const char *format_mac_address(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buffer, uint64_t address) {
|
static const char *format_mac_address(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buffer, uint64_t address) {
|
||||||
std::array<uint8_t, MAC_ADDRESS_SIZE> mac{};
|
std::array<uint8_t, MAC_ADDRESS_SIZE> mac{};
|
||||||
@@ -130,6 +138,10 @@ void BTHomeMiThermometer::dump_config() {
|
|||||||
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||||
ESP_LOGCONFIG(TAG, "BTHome MiThermometer");
|
ESP_LOGCONFIG(TAG, "BTHome MiThermometer");
|
||||||
ESP_LOGCONFIG(TAG, " MAC Address: %s", format_mac_address(addr_buf, this->address_));
|
ESP_LOGCONFIG(TAG, " MAC Address: %s", format_mac_address(addr_buf, this->address_));
|
||||||
|
if (this->has_bindkey_) {
|
||||||
|
char bindkey_hex[format_hex_pretty_size(BTHOME_BINDKEY_SIZE)];
|
||||||
|
ESP_LOGCONFIG(TAG, " Bindkey: %s", format_hex_pretty_to(bindkey_hex, this->bindkey_, BTHOME_BINDKEY_SIZE, '.'));
|
||||||
|
}
|
||||||
LOG_SENSOR(" ", "Temperature", this->temperature_);
|
LOG_SENSOR(" ", "Temperature", this->temperature_);
|
||||||
LOG_SENSOR(" ", "Humidity", this->humidity_);
|
LOG_SENSOR(" ", "Humidity", this->humidity_);
|
||||||
LOG_SENSOR(" ", "Battery Level", this->battery_level_);
|
LOG_SENSOR(" ", "Battery Level", this->battery_level_);
|
||||||
@@ -150,6 +162,60 @@ bool BTHomeMiThermometer::parse_device(const esp32_ble_tracker::ESPBTDevice &dev
|
|||||||
return matched;
|
return matched;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void BTHomeMiThermometer::set_bindkey(std::initializer_list<uint8_t> bindkey) {
|
||||||
|
if (bindkey.size() != sizeof(this->bindkey_)) {
|
||||||
|
ESP_LOGW(TAG, "BTHome bindkey size mismatch: %zu", bindkey.size());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::copy(bindkey.begin(), bindkey.end(), this->bindkey_);
|
||||||
|
this->has_bindkey_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BTHomeMiThermometer::decrypt_bthome_payload_(const std::vector<uint8_t> &data, uint64_t source_address,
|
||||||
|
std::vector<uint8_t> &payload) const {
|
||||||
|
if (data.size() <= 1 + BTHOME_COUNTER_SIZE + BTHOME_MIC_SIZE) {
|
||||||
|
ESP_LOGVV(TAG, "Encrypted BTHome payload too short: %zu", data.size());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t ciphertext_size = data.size() - 1 - BTHOME_COUNTER_SIZE - BTHOME_MIC_SIZE;
|
||||||
|
payload.resize(ciphertext_size);
|
||||||
|
|
||||||
|
std::array<uint8_t, MAC_ADDRESS_SIZE> mac{};
|
||||||
|
for (size_t i = 0; i < MAC_ADDRESS_SIZE; i++) {
|
||||||
|
mac[i] = (source_address >> ((MAC_ADDRESS_SIZE - 1 - i) * 8)) & 0xFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::array<uint8_t, BTHOME_NONCE_SIZE> nonce{};
|
||||||
|
memcpy(nonce.data(), mac.data(), mac.size());
|
||||||
|
nonce[6] = 0xD2;
|
||||||
|
nonce[7] = 0xFC;
|
||||||
|
nonce[8] = data[0];
|
||||||
|
memcpy(nonce.data() + 9, &data[data.size() - BTHOME_COUNTER_SIZE - BTHOME_MIC_SIZE], BTHOME_COUNTER_SIZE);
|
||||||
|
|
||||||
|
const uint8_t *ciphertext = data.data() + 1;
|
||||||
|
const uint8_t *mic = data.data() + data.size() - BTHOME_MIC_SIZE;
|
||||||
|
|
||||||
|
mbedtls_ccm_context ctx;
|
||||||
|
mbedtls_ccm_init(&ctx);
|
||||||
|
|
||||||
|
int ret = mbedtls_ccm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, this->bindkey_, BTHOME_BINDKEY_SIZE * 8);
|
||||||
|
if (ret) {
|
||||||
|
ESP_LOGVV(TAG, "mbedtls_ccm_setkey() failed.");
|
||||||
|
mbedtls_ccm_free(&ctx);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = mbedtls_ccm_auth_decrypt(&ctx, ciphertext_size, nonce.data(), nonce.size(), nullptr, 0, ciphertext,
|
||||||
|
payload.data(), mic, BTHOME_MIC_SIZE);
|
||||||
|
mbedtls_ccm_free(&ctx);
|
||||||
|
if (ret) {
|
||||||
|
ESP_LOGVV(TAG, "BTHome decryption failed (ret=%d).", ret);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool BTHomeMiThermometer::handle_service_data_(const esp32_ble_tracker::ServiceData &service_data,
|
bool BTHomeMiThermometer::handle_service_data_(const esp32_ble_tracker::ServiceData &service_data,
|
||||||
const esp32_ble_tracker::ESPBTDevice &device) {
|
const esp32_ble_tracker::ESPBTDevice &device) {
|
||||||
if (!service_data.uuid.contains(0xD2, 0xFC)) {
|
if (!service_data.uuid.contains(0xD2, 0xFC)) {
|
||||||
@@ -173,51 +239,88 @@ bool BTHomeMiThermometer::handle_service_data_(const esp32_ble_tracker::ServiceD
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
uint64_t source_address = device.address_uint64();
|
||||||
if (is_encrypted) {
|
bool address_matches = source_address == this->address_;
|
||||||
ESP_LOGV(TAG, "Ignoring encrypted BTHome frame from %s", device.address_str_to(addr_buf));
|
if (!is_encrypted && mac_included && data.size() >= 7) {
|
||||||
|
uint64_t advertised_address = 0;
|
||||||
|
for (int i = 5; i >= 0; i--) {
|
||||||
|
advertised_address = (advertised_address << 8) | data[1 + i];
|
||||||
|
}
|
||||||
|
address_matches = address_matches || advertised_address == this->address_;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_encrypted && !this->has_bindkey_) {
|
||||||
|
if (address_matches) {
|
||||||
|
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||||
|
ESP_LOGE(TAG, "Encrypted BTHome frame received but no bindkey configured for %s",
|
||||||
|
device.address_str_to(addr_buf));
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t payload_index = 1;
|
if (!is_encrypted && this->has_bindkey_) {
|
||||||
uint64_t source_address = device.address_uint64();
|
if (address_matches) {
|
||||||
|
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||||
|
ESP_LOGE(TAG, "Unencrypted BTHome frame received with bindkey configured for %s",
|
||||||
|
device.address_str_to(addr_buf));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::vector<uint8_t> decrypted_payload;
|
||||||
|
const uint8_t *payload = nullptr;
|
||||||
|
size_t payload_size = 0;
|
||||||
|
|
||||||
|
if (is_encrypted) {
|
||||||
|
if (!this->decrypt_bthome_payload_(data, source_address, decrypted_payload)) {
|
||||||
|
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||||
|
ESP_LOGVV(TAG, "Failed to decrypt BTHome frame from %s", device.address_str_to(addr_buf));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
payload = decrypted_payload.data();
|
||||||
|
payload_size = decrypted_payload.size();
|
||||||
|
} else {
|
||||||
|
payload = data.data() + 1;
|
||||||
|
payload_size = data.size() - 1;
|
||||||
|
}
|
||||||
|
|
||||||
if (mac_included) {
|
if (mac_included) {
|
||||||
if (data.size() < 7) {
|
if (payload_size < 6) {
|
||||||
ESP_LOGVV(TAG, "BTHome payload missing MAC address");
|
ESP_LOGVV(TAG, "BTHome payload missing MAC address");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
source_address = 0;
|
source_address = 0;
|
||||||
for (int i = 5; i >= 0; i--) {
|
for (int i = 5; i >= 0; i--) {
|
||||||
source_address = (source_address << 8) | data[1 + i];
|
source_address = (source_address << 8) | payload[i];
|
||||||
}
|
}
|
||||||
payload_index = 7;
|
payload += 6;
|
||||||
|
payload_size -= 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||||
if (source_address != this->address_) {
|
if (source_address != this->address_) {
|
||||||
ESP_LOGVV(TAG, "BTHome frame from unexpected device %s", format_mac_address(addr_buf, source_address));
|
ESP_LOGVV(TAG, "BTHome frame from unexpected device %s", format_mac_address(addr_buf, source_address));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payload_index >= data.size()) {
|
if (payload_size == 0) {
|
||||||
ESP_LOGVV(TAG, "BTHome payload empty after header");
|
ESP_LOGVV(TAG, "BTHome payload empty after header");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool reported = false;
|
bool reported = false;
|
||||||
size_t offset = payload_index;
|
size_t offset = 0;
|
||||||
uint8_t last_type = 0;
|
uint8_t last_type = 0;
|
||||||
|
|
||||||
while (offset < data.size()) {
|
while (offset < payload_size) {
|
||||||
const uint8_t obj_type = data[offset++];
|
const uint8_t obj_type = payload[offset++];
|
||||||
size_t value_length = 0;
|
size_t value_length = 0;
|
||||||
bool has_length_byte = obj_type == 0x53; // text objects include explicit length
|
bool has_length_byte = obj_type == 0x53; // text objects include explicit length
|
||||||
|
|
||||||
if (has_length_byte) {
|
if (has_length_byte) {
|
||||||
if (offset >= data.size()) {
|
if (offset >= payload_size) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
value_length = data[offset++];
|
value_length = payload[offset++];
|
||||||
} else {
|
} else {
|
||||||
if (!get_bthome_value_length(obj_type, value_length)) {
|
if (!get_bthome_value_length(obj_type, value_length)) {
|
||||||
ESP_LOGVV(TAG, "Unknown BTHome object 0x%02X", obj_type);
|
ESP_LOGVV(TAG, "Unknown BTHome object 0x%02X", obj_type);
|
||||||
@@ -229,12 +332,12 @@ bool BTHomeMiThermometer::handle_service_data_(const esp32_ble_tracker::ServiceD
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (offset + value_length > data.size()) {
|
if (offset + value_length > payload_size) {
|
||||||
ESP_LOGVV(TAG, "BTHome object length exceeds payload");
|
ESP_LOGVV(TAG, "BTHome object length exceeds payload");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const uint8_t *value = &data[offset];
|
const uint8_t *value = &payload[offset];
|
||||||
offset += value_length;
|
offset += value_length;
|
||||||
|
|
||||||
if (obj_type < last_type) {
|
if (obj_type < last_type) {
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
#include "esphome/core/component.h"
|
#include "esphome/core/component.h"
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <initializer_list>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#ifdef USE_ESP32
|
#ifdef USE_ESP32
|
||||||
|
|
||||||
@@ -14,6 +16,7 @@ namespace bthome_mithermometer {
|
|||||||
class BTHomeMiThermometer : public esp32_ble_tracker::ESPBTDeviceListener, public Component {
|
class BTHomeMiThermometer : public esp32_ble_tracker::ESPBTDeviceListener, public Component {
|
||||||
public:
|
public:
|
||||||
void set_address(uint64_t address) { this->address_ = address; }
|
void set_address(uint64_t address) { this->address_ = address; }
|
||||||
|
void set_bindkey(std::initializer_list<uint8_t> bindkey);
|
||||||
|
|
||||||
void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; }
|
void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; }
|
||||||
void set_humidity(sensor::Sensor *humidity) { this->humidity_ = humidity; }
|
void set_humidity(sensor::Sensor *humidity) { this->humidity_ = humidity; }
|
||||||
@@ -27,9 +30,13 @@ class BTHomeMiThermometer : public esp32_ble_tracker::ESPBTDeviceListener, publi
|
|||||||
protected:
|
protected:
|
||||||
bool handle_service_data_(const esp32_ble_tracker::ServiceData &service_data,
|
bool handle_service_data_(const esp32_ble_tracker::ServiceData &service_data,
|
||||||
const esp32_ble_tracker::ESPBTDevice &device);
|
const esp32_ble_tracker::ESPBTDevice &device);
|
||||||
|
bool decrypt_bthome_payload_(const std::vector<uint8_t> &data, uint64_t source_address,
|
||||||
|
std::vector<uint8_t> &payload) const;
|
||||||
|
|
||||||
uint64_t address_{0};
|
uint64_t address_{0};
|
||||||
optional<uint8_t> last_packet_id_{};
|
optional<uint8_t> last_packet_id_{};
|
||||||
|
bool has_bindkey_{false};
|
||||||
|
uint8_t bindkey_[16];
|
||||||
|
|
||||||
sensor::Sensor *temperature_{nullptr};
|
sensor::Sensor *temperature_{nullptr};
|
||||||
sensor::Sensor *humidity_{nullptr};
|
sensor::Sensor *humidity_{nullptr};
|
||||||
|
|||||||
@@ -12,10 +12,7 @@ void log_button(const char *tag, const char *prefix, const char *type, Button *o
|
|||||||
}
|
}
|
||||||
|
|
||||||
ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str());
|
ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str());
|
||||||
|
LOG_ENTITY_ICON(tag, prefix, *obj);
|
||||||
if (!obj->get_icon_ref().empty()) {
|
|
||||||
ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj->get_icon_ref().c_str());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Button::press() {
|
void Button::press() {
|
||||||
|
|||||||
@@ -96,10 +96,16 @@ void CaptivePortal::start() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void CaptivePortal::handleRequest(AsyncWebServerRequest *req) {
|
void CaptivePortal::handleRequest(AsyncWebServerRequest *req) {
|
||||||
if (req->url() == ESPHOME_F("/config.json")) {
|
#ifdef USE_ESP32
|
||||||
|
char url_buf[AsyncWebServerRequest::URL_BUF_SIZE];
|
||||||
|
StringRef url = req->url_to(url_buf);
|
||||||
|
#else
|
||||||
|
const auto &url = req->url();
|
||||||
|
#endif
|
||||||
|
if (url == ESPHOME_F("/config.json")) {
|
||||||
this->handle_config(req);
|
this->handle_config(req);
|
||||||
return;
|
return;
|
||||||
} else if (req->url() == ESPHOME_F("/wifisave")) {
|
} else if (url == ESPHOME_F("/wifisave")) {
|
||||||
this->handle_wifisave(req);
|
this->handle_wifisave(req);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,6 +152,13 @@ void CC1101Component::setup() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CC1101Component::call_listeners_(const std::vector<uint8_t> &packet, float freq_offset, float rssi, uint8_t lqi) {
|
||||||
|
for (auto &listener : this->listeners_) {
|
||||||
|
listener->on_packet(packet, freq_offset, rssi, lqi);
|
||||||
|
}
|
||||||
|
this->packet_trigger_->trigger(packet, freq_offset, rssi, lqi);
|
||||||
|
}
|
||||||
|
|
||||||
void CC1101Component::loop() {
|
void CC1101Component::loop() {
|
||||||
if (this->state_.PKT_FORMAT != static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO) || this->gdo0_pin_ == nullptr ||
|
if (this->state_.PKT_FORMAT != static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO) || this->gdo0_pin_ == nullptr ||
|
||||||
!this->gdo0_pin_->digital_read()) {
|
!this->gdo0_pin_->digital_read()) {
|
||||||
@@ -198,7 +205,7 @@ void CC1101Component::loop() {
|
|||||||
bool crc_ok = (this->state_.LQI & STATUS_CRC_OK_MASK) != 0;
|
bool crc_ok = (this->state_.LQI & STATUS_CRC_OK_MASK) != 0;
|
||||||
uint8_t lqi = this->state_.LQI & STATUS_LQI_MASK;
|
uint8_t lqi = this->state_.LQI & STATUS_LQI_MASK;
|
||||||
if (this->state_.CRC_EN == 0 || crc_ok) {
|
if (this->state_.CRC_EN == 0 || crc_ok) {
|
||||||
this->packet_trigger_->trigger(this->packet_, freq_offset, rssi, lqi);
|
this->call_listeners_(this->packet_, freq_offset, rssi, lqi);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return to rx
|
// Return to rx
|
||||||
|
|||||||
@@ -11,6 +11,11 @@ namespace esphome::cc1101 {
|
|||||||
|
|
||||||
enum class CC1101Error { NONE = 0, TIMEOUT, PARAMS, CRC_ERROR, FIFO_OVERFLOW, PLL_LOCK };
|
enum class CC1101Error { NONE = 0, TIMEOUT, PARAMS, CRC_ERROR, FIFO_OVERFLOW, PLL_LOCK };
|
||||||
|
|
||||||
|
class CC1101Listener {
|
||||||
|
public:
|
||||||
|
virtual void on_packet(const std::vector<uint8_t> &packet, float freq_offset, float rssi, uint8_t lqi) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
class CC1101Component : public Component,
|
class CC1101Component : public Component,
|
||||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
|
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
|
||||||
spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_1MHZ> {
|
spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_1MHZ> {
|
||||||
@@ -73,6 +78,7 @@ class CC1101Component : public Component,
|
|||||||
|
|
||||||
// Packet mode operations
|
// Packet mode operations
|
||||||
CC1101Error transmit_packet(const std::vector<uint8_t> &packet);
|
CC1101Error transmit_packet(const std::vector<uint8_t> &packet);
|
||||||
|
void register_listener(CC1101Listener *listener) { this->listeners_.push_back(listener); }
|
||||||
Trigger<std::vector<uint8_t>, float, float, uint8_t> *get_packet_trigger() const { return this->packet_trigger_; }
|
Trigger<std::vector<uint8_t>, float, float, uint8_t> *get_packet_trigger() const { return this->packet_trigger_; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
@@ -89,9 +95,11 @@ class CC1101Component : public Component,
|
|||||||
InternalGPIOPin *gdo0_pin_{nullptr};
|
InternalGPIOPin *gdo0_pin_{nullptr};
|
||||||
|
|
||||||
// Packet handling
|
// Packet handling
|
||||||
|
void call_listeners_(const std::vector<uint8_t> &packet, float freq_offset, float rssi, uint8_t lqi);
|
||||||
Trigger<std::vector<uint8_t>, float, float, uint8_t> *packet_trigger_{
|
Trigger<std::vector<uint8_t>, float, float, uint8_t> *packet_trigger_{
|
||||||
new Trigger<std::vector<uint8_t>, float, float, uint8_t>()};
|
new Trigger<std::vector<uint8_t>, float, float, uint8_t>()};
|
||||||
std::vector<uint8_t> packet_;
|
std::vector<uint8_t> packet_;
|
||||||
|
std::vector<CC1101Listener *> listeners_;
|
||||||
|
|
||||||
// Low-level Helpers
|
// Low-level Helpers
|
||||||
uint8_t strobe_(Command cmd);
|
uint8_t strobe_(Command cmd);
|
||||||
|
|||||||
103
esphome/components/ch423/__init__.py
Normal file
103
esphome/components/ch423/__init__.py
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
from esphome import pins
|
||||||
|
import esphome.codegen as cg
|
||||||
|
from esphome.components import i2c
|
||||||
|
from esphome.components.i2c import I2CBus
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.const import (
|
||||||
|
CONF_I2C_ID,
|
||||||
|
CONF_ID,
|
||||||
|
CONF_INPUT,
|
||||||
|
CONF_INVERTED,
|
||||||
|
CONF_MODE,
|
||||||
|
CONF_NUMBER,
|
||||||
|
CONF_OPEN_DRAIN,
|
||||||
|
CONF_OUTPUT,
|
||||||
|
)
|
||||||
|
from esphome.core import CORE
|
||||||
|
|
||||||
|
CODEOWNERS = ["@dwmw2"]
|
||||||
|
DEPENDENCIES = ["i2c"]
|
||||||
|
MULTI_CONF = True
|
||||||
|
ch423_ns = cg.esphome_ns.namespace("ch423")
|
||||||
|
|
||||||
|
CH423Component = ch423_ns.class_("CH423Component", cg.Component, i2c.I2CDevice)
|
||||||
|
CH423GPIOPin = ch423_ns.class_(
|
||||||
|
"CH423GPIOPin", cg.GPIOPin, cg.Parented.template(CH423Component)
|
||||||
|
)
|
||||||
|
|
||||||
|
CONF_CH423 = "ch423"
|
||||||
|
|
||||||
|
# Note that no address is configurable - each register in the CH423 has a dedicated i2c address
|
||||||
|
CONFIG_SCHEMA = cv.Schema(
|
||||||
|
{
|
||||||
|
cv.GenerateID(CONF_ID): cv.declare_id(CH423Component),
|
||||||
|
cv.GenerateID(CONF_I2C_ID): cv.use_id(I2CBus),
|
||||||
|
}
|
||||||
|
).extend(cv.COMPONENT_SCHEMA)
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
|
await cg.register_component(var, config)
|
||||||
|
# Can't use register_i2c_device because there is no CONF_ADDRESS
|
||||||
|
parent = await cg.get_variable(config[CONF_I2C_ID])
|
||||||
|
cg.add(var.set_i2c_bus(parent))
|
||||||
|
|
||||||
|
|
||||||
|
# This is used as a final validation step so that modes have been fully transformed.
|
||||||
|
def pin_mode_check(pin_config, _):
|
||||||
|
if pin_config[CONF_MODE][CONF_INPUT] and pin_config[CONF_NUMBER] >= 8:
|
||||||
|
raise cv.Invalid("CH423 only supports input on pins 0-7")
|
||||||
|
if pin_config[CONF_MODE][CONF_OPEN_DRAIN] and pin_config[CONF_NUMBER] < 8:
|
||||||
|
raise cv.Invalid("CH423 only supports open drain output on pins 8-23")
|
||||||
|
|
||||||
|
ch423_id = pin_config[CONF_CH423]
|
||||||
|
pin_num = pin_config[CONF_NUMBER]
|
||||||
|
is_output = pin_config[CONF_MODE][CONF_OUTPUT]
|
||||||
|
is_open_drain = pin_config[CONF_MODE][CONF_OPEN_DRAIN]
|
||||||
|
|
||||||
|
# Track pin modes per CH423 instance in CORE.data
|
||||||
|
ch423_modes = CORE.data.setdefault(CONF_CH423, {})
|
||||||
|
if ch423_id not in ch423_modes:
|
||||||
|
ch423_modes[ch423_id] = {"gpio_output": None, "gpo_open_drain": None}
|
||||||
|
|
||||||
|
if pin_num < 8:
|
||||||
|
# GPIO pins (0-7): all must have same direction
|
||||||
|
if ch423_modes[ch423_id]["gpio_output"] is None:
|
||||||
|
ch423_modes[ch423_id]["gpio_output"] = is_output
|
||||||
|
elif ch423_modes[ch423_id]["gpio_output"] != is_output:
|
||||||
|
raise cv.Invalid(
|
||||||
|
"CH423 GPIO pins (0-7) must all be configured as input or all as output"
|
||||||
|
)
|
||||||
|
# GPO pins (8-23): all must have same open-drain setting
|
||||||
|
elif ch423_modes[ch423_id]["gpo_open_drain"] is None:
|
||||||
|
ch423_modes[ch423_id]["gpo_open_drain"] = is_open_drain
|
||||||
|
elif ch423_modes[ch423_id]["gpo_open_drain"] != is_open_drain:
|
||||||
|
raise cv.Invalid(
|
||||||
|
"CH423 GPO pins (8-23) must all be configured as push-pull or all as open-drain"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
CH423_PIN_SCHEMA = pins.gpio_base_schema(
|
||||||
|
CH423GPIOPin,
|
||||||
|
cv.int_range(min=0, max=23),
|
||||||
|
modes=[CONF_INPUT, CONF_OUTPUT, CONF_OPEN_DRAIN],
|
||||||
|
).extend(
|
||||||
|
{
|
||||||
|
cv.Required(CONF_CH423): cv.use_id(CH423Component),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pins.PIN_SCHEMA_REGISTRY.register(CONF_CH423, CH423_PIN_SCHEMA, pin_mode_check)
|
||||||
|
async def ch423_pin_to_code(config):
|
||||||
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
|
parent = await cg.get_variable(config[CONF_CH423])
|
||||||
|
|
||||||
|
cg.add(var.set_parent(parent))
|
||||||
|
|
||||||
|
num = config[CONF_NUMBER]
|
||||||
|
cg.add(var.set_pin(num))
|
||||||
|
cg.add(var.set_inverted(config[CONF_INVERTED]))
|
||||||
|
cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE])))
|
||||||
|
return var
|
||||||
148
esphome/components/ch423/ch423.cpp
Normal file
148
esphome/components/ch423/ch423.cpp
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
#include "ch423.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
#include "esphome/core/progmem.h"
|
||||||
|
|
||||||
|
namespace esphome::ch423 {
|
||||||
|
|
||||||
|
static constexpr uint8_t CH423_REG_SYS = 0x24; // Set system parameters (0x48 >> 1)
|
||||||
|
static constexpr uint8_t CH423_SYS_IO_OE = 0x01; // IO output enable
|
||||||
|
static constexpr uint8_t CH423_SYS_OD_EN = 0x04; // Open drain enable for OC pins
|
||||||
|
static constexpr uint8_t CH423_REG_IO = 0x30; // Write/read IO7-IO0 (0x60 >> 1)
|
||||||
|
static constexpr uint8_t CH423_REG_IO_RD = 0x26; // Read IO7-IO0 (0x4D >> 1, rounded down)
|
||||||
|
static constexpr uint8_t CH423_REG_OCL = 0x22; // Write OC7-OC0 (0x44 >> 1)
|
||||||
|
static constexpr uint8_t CH423_REG_OCH = 0x23; // Write OC15-OC8 (0x46 >> 1)
|
||||||
|
|
||||||
|
static const char *const TAG = "ch423";
|
||||||
|
|
||||||
|
void CH423Component::setup() {
|
||||||
|
// set outputs before mode
|
||||||
|
this->write_outputs_();
|
||||||
|
// Set system parameters and check for errors
|
||||||
|
bool success = this->write_reg_(CH423_REG_SYS, this->sys_params_);
|
||||||
|
// Only read inputs if pins are configured for input (IO_OE not set)
|
||||||
|
if (success && !(this->sys_params_ & CH423_SYS_IO_OE)) {
|
||||||
|
success = this->read_inputs_();
|
||||||
|
}
|
||||||
|
if (!success) {
|
||||||
|
ESP_LOGE(TAG, "CH423 not detected");
|
||||||
|
this->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGCONFIG(TAG, "Initialization complete. Warning: %d, Error: %d", this->status_has_warning(),
|
||||||
|
this->status_has_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
void CH423Component::loop() {
|
||||||
|
// Clear all the previously read flags.
|
||||||
|
this->pin_read_flags_ = 0x00;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CH423Component::dump_config() {
|
||||||
|
ESP_LOGCONFIG(TAG, "CH423:");
|
||||||
|
if (this->is_failed()) {
|
||||||
|
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CH423Component::pin_mode(uint8_t pin, gpio::Flags flags) {
|
||||||
|
if (pin < 8) {
|
||||||
|
if (flags & gpio::FLAG_OUTPUT) {
|
||||||
|
this->sys_params_ |= CH423_SYS_IO_OE;
|
||||||
|
}
|
||||||
|
} else if (pin >= 8 && pin < 24) {
|
||||||
|
if (flags & gpio::FLAG_OPEN_DRAIN) {
|
||||||
|
this->sys_params_ |= CH423_SYS_OD_EN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CH423Component::digital_read(uint8_t pin) {
|
||||||
|
if (this->pin_read_flags_ == 0 || this->pin_read_flags_ & (1 << pin)) {
|
||||||
|
// Read values on first access or in case it's being read again in the same loop
|
||||||
|
this->read_inputs_();
|
||||||
|
}
|
||||||
|
|
||||||
|
this->pin_read_flags_ |= (1 << pin);
|
||||||
|
return (this->input_bits_ & (1 << pin)) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CH423Component::digital_write(uint8_t pin, bool value) {
|
||||||
|
if (value) {
|
||||||
|
this->output_bits_ |= (1 << pin);
|
||||||
|
} else {
|
||||||
|
this->output_bits_ &= ~(1 << pin);
|
||||||
|
}
|
||||||
|
this->write_outputs_();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CH423Component::read_inputs_() {
|
||||||
|
if (this->is_failed()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// reading inputs requires IO_OE to be 0
|
||||||
|
if (this->sys_params_ & CH423_SYS_IO_OE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
uint8_t result = this->read_reg_(CH423_REG_IO_RD);
|
||||||
|
this->input_bits_ = result;
|
||||||
|
this->status_clear_warning();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write a register. Can't use the standard write_byte() method because there is no single pre-configured i2c address.
|
||||||
|
bool CH423Component::write_reg_(uint8_t reg, uint8_t value) {
|
||||||
|
auto err = this->bus_->write_readv(reg, &value, 1, nullptr, 0);
|
||||||
|
if (err != i2c::ERROR_OK) {
|
||||||
|
char buf[64];
|
||||||
|
ESPHOME_snprintf_P(buf, sizeof(buf), ESPHOME_PSTR("write failed for register 0x%X, error %d"), reg, err);
|
||||||
|
this->status_set_warning(buf);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this->status_clear_warning();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t CH423Component::read_reg_(uint8_t reg) {
|
||||||
|
uint8_t value;
|
||||||
|
auto err = this->bus_->write_readv(reg, nullptr, 0, &value, 1);
|
||||||
|
if (err != i2c::ERROR_OK) {
|
||||||
|
char buf[64];
|
||||||
|
ESPHOME_snprintf_P(buf, sizeof(buf), ESPHOME_PSTR("read failed for register 0x%X, error %d"), reg, err);
|
||||||
|
this->status_set_warning(buf);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
this->status_clear_warning();
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CH423Component::write_outputs_() {
|
||||||
|
bool success = true;
|
||||||
|
// Write IO7-IO0
|
||||||
|
success &= this->write_reg_(CH423_REG_IO, static_cast<uint8_t>(this->output_bits_));
|
||||||
|
// Write OC7-OC0
|
||||||
|
success &= this->write_reg_(CH423_REG_OCL, static_cast<uint8_t>(this->output_bits_ >> 8));
|
||||||
|
// Write OC15-OC8
|
||||||
|
success &= this->write_reg_(CH423_REG_OCH, static_cast<uint8_t>(this->output_bits_ >> 16));
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
float CH423Component::get_setup_priority() const { return setup_priority::IO; }
|
||||||
|
|
||||||
|
// Run our loop() method very early in the loop, so that we cache read values
|
||||||
|
// before other components call our digital_read() method.
|
||||||
|
float CH423Component::get_loop_priority() const { return 9.0f; } // Just after WIFI
|
||||||
|
|
||||||
|
void CH423GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); }
|
||||||
|
bool CH423GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) ^ this->inverted_; }
|
||||||
|
|
||||||
|
void CH423GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value ^ this->inverted_); }
|
||||||
|
size_t CH423GPIOPin::dump_summary(char *buffer, size_t len) const {
|
||||||
|
return snprintf(buffer, len, "EXIO%u via CH423", this->pin_);
|
||||||
|
}
|
||||||
|
void CH423GPIOPin::set_flags(gpio::Flags flags) {
|
||||||
|
flags_ = flags;
|
||||||
|
this->parent_->pin_mode(this->pin_, flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace esphome::ch423
|
||||||
67
esphome/components/ch423/ch423.h
Normal file
67
esphome/components/ch423/ch423.h
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/core/hal.h"
|
||||||
|
#include "esphome/components/i2c/i2c.h"
|
||||||
|
|
||||||
|
namespace esphome::ch423 {
|
||||||
|
|
||||||
|
class CH423Component : public Component, public i2c::I2CDevice {
|
||||||
|
public:
|
||||||
|
CH423Component() = default;
|
||||||
|
|
||||||
|
/// Check i2c availability and setup masks
|
||||||
|
void setup() override;
|
||||||
|
/// Poll for input changes periodically
|
||||||
|
void loop() override;
|
||||||
|
/// Helper function to read the value of a pin.
|
||||||
|
bool digital_read(uint8_t pin);
|
||||||
|
/// Helper function to write the value of a pin.
|
||||||
|
void digital_write(uint8_t pin, bool value);
|
||||||
|
/// Helper function to set the pin mode of a pin.
|
||||||
|
void pin_mode(uint8_t pin, gpio::Flags flags);
|
||||||
|
|
||||||
|
float get_setup_priority() const override;
|
||||||
|
float get_loop_priority() const override;
|
||||||
|
void dump_config() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool write_reg_(uint8_t reg, uint8_t value);
|
||||||
|
uint8_t read_reg_(uint8_t reg);
|
||||||
|
bool read_inputs_();
|
||||||
|
bool write_outputs_();
|
||||||
|
|
||||||
|
/// The mask to write as output state - 1 means HIGH, 0 means LOW
|
||||||
|
uint32_t output_bits_{0x00};
|
||||||
|
/// Flags to check if read previously during this loop
|
||||||
|
uint8_t pin_read_flags_{0x00};
|
||||||
|
/// Copy of last read values
|
||||||
|
uint8_t input_bits_{0x00};
|
||||||
|
/// System parameters
|
||||||
|
uint8_t sys_params_{0x00};
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Helper class to expose a CH423 pin as a GPIO pin.
|
||||||
|
class CH423GPIOPin : public GPIOPin {
|
||||||
|
public:
|
||||||
|
void setup() override{};
|
||||||
|
void pin_mode(gpio::Flags flags) override;
|
||||||
|
bool digital_read() override;
|
||||||
|
void digital_write(bool value) override;
|
||||||
|
size_t dump_summary(char *buffer, size_t len) const override;
|
||||||
|
|
||||||
|
void set_parent(CH423Component *parent) { parent_ = parent; }
|
||||||
|
void set_pin(uint8_t pin) { pin_ = pin; }
|
||||||
|
void set_inverted(bool inverted) { inverted_ = inverted; }
|
||||||
|
void set_flags(gpio::Flags flags);
|
||||||
|
|
||||||
|
gpio::Flags get_flags() const override { return this->flags_; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
CH423Component *parent_{};
|
||||||
|
uint8_t pin_{};
|
||||||
|
bool inverted_{};
|
||||||
|
gpio::Flags flags_{};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace esphome::ch423
|
||||||
@@ -360,8 +360,7 @@ void Climate::add_on_control_callback(std::function<void(ClimateCall &)> &&callb
|
|||||||
static const uint32_t RESTORE_STATE_VERSION = 0x848EA6ADUL;
|
static const uint32_t RESTORE_STATE_VERSION = 0x848EA6ADUL;
|
||||||
|
|
||||||
optional<ClimateDeviceRestoreState> Climate::restore_state_() {
|
optional<ClimateDeviceRestoreState> Climate::restore_state_() {
|
||||||
this->rtc_ = global_preferences->make_preference<ClimateDeviceRestoreState>(this->get_preference_hash() ^
|
this->rtc_ = this->make_entity_preference<ClimateDeviceRestoreState>(RESTORE_STATE_VERSION);
|
||||||
RESTORE_STATE_VERSION);
|
|
||||||
ClimateDeviceRestoreState recovered{};
|
ClimateDeviceRestoreState recovered{};
|
||||||
if (!this->rtc_.load(&recovered))
|
if (!this->rtc_.load(&recovered))
|
||||||
return {};
|
return {};
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
#include "cover.h"
|
#include "cover.h"
|
||||||
#include "esphome/core/defines.h"
|
#include "esphome/core/defines.h"
|
||||||
#include "esphome/core/controller_registry.h"
|
#include "esphome/core/controller_registry.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
#include "esphome/core/progmem.h"
|
||||||
|
|
||||||
#include <strings.h>
|
#include <strings.h>
|
||||||
|
|
||||||
#include "esphome/core/log.h"
|
|
||||||
|
|
||||||
namespace esphome::cover {
|
namespace esphome::cover {
|
||||||
|
|
||||||
static const char *const TAG = "cover";
|
static const char *const TAG = "cover";
|
||||||
@@ -39,13 +39,13 @@ Cover::Cover() : position{COVER_OPEN} {}
|
|||||||
|
|
||||||
CoverCall::CoverCall(Cover *parent) : parent_(parent) {}
|
CoverCall::CoverCall(Cover *parent) : parent_(parent) {}
|
||||||
CoverCall &CoverCall::set_command(const char *command) {
|
CoverCall &CoverCall::set_command(const char *command) {
|
||||||
if (strcasecmp(command, "OPEN") == 0) {
|
if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("OPEN")) == 0) {
|
||||||
this->set_command_open();
|
this->set_command_open();
|
||||||
} else if (strcasecmp(command, "CLOSE") == 0) {
|
} else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("CLOSE")) == 0) {
|
||||||
this->set_command_close();
|
this->set_command_close();
|
||||||
} else if (strcasecmp(command, "STOP") == 0) {
|
} else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("STOP")) == 0) {
|
||||||
this->set_command_stop();
|
this->set_command_stop();
|
||||||
} else if (strcasecmp(command, "TOGGLE") == 0) {
|
} else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("TOGGLE")) == 0) {
|
||||||
this->set_command_toggle();
|
this->set_command_toggle();
|
||||||
} else {
|
} else {
|
||||||
ESP_LOGW(TAG, "'%s' - Unrecognized command %s", this->parent_->get_name().c_str(), command);
|
ESP_LOGW(TAG, "'%s' - Unrecognized command %s", this->parent_->get_name().c_str(), command);
|
||||||
@@ -187,7 +187,7 @@ void Cover::publish_state(bool save) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
optional<CoverRestoreState> Cover::restore_state_() {
|
optional<CoverRestoreState> Cover::restore_state_() {
|
||||||
this->rtc_ = global_preferences->make_preference<CoverRestoreState>(this->get_preference_hash());
|
this->rtc_ = this->make_entity_preference<CoverRestoreState>();
|
||||||
CoverRestoreState recovered{};
|
CoverRestoreState recovered{};
|
||||||
if (!this->rtc_.load(&recovered))
|
if (!this->rtc_.load(&recovered))
|
||||||
return {};
|
return {};
|
||||||
|
|||||||
@@ -20,9 +20,7 @@ const extern float COVER_CLOSED;
|
|||||||
if (traits_.get_is_assumed_state()) { \
|
if (traits_.get_is_assumed_state()) { \
|
||||||
ESP_LOGCONFIG(TAG, "%s Assumed State: YES", prefix); \
|
ESP_LOGCONFIG(TAG, "%s Assumed State: YES", prefix); \
|
||||||
} \
|
} \
|
||||||
if (!(obj)->get_device_class_ref().empty()) { \
|
LOG_ENTITY_DEVICE_CLASS(TAG, prefix, *(obj)); \
|
||||||
ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class_ref().c_str()); \
|
|
||||||
} \
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Cover;
|
class Cover;
|
||||||
|
|||||||
@@ -76,7 +76,6 @@ class CS5460AComponent : public Component,
|
|||||||
void restart() { restart_(); }
|
void restart() { restart_(); }
|
||||||
|
|
||||||
void setup() override;
|
void setup() override;
|
||||||
void loop() override {}
|
|
||||||
void dump_config() override;
|
void dump_config() override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
|||||||
@@ -106,9 +106,9 @@ DateCall &DateCall::set_date(uint16_t year, uint8_t month, uint8_t day) {
|
|||||||
|
|
||||||
DateCall &DateCall::set_date(ESPTime time) { return this->set_date(time.year, time.month, time.day_of_month); };
|
DateCall &DateCall::set_date(ESPTime time) { return this->set_date(time.year, time.month, time.day_of_month); };
|
||||||
|
|
||||||
DateCall &DateCall::set_date(const std::string &date) {
|
DateCall &DateCall::set_date(const char *date, size_t len) {
|
||||||
ESPTime val{};
|
ESPTime val{};
|
||||||
if (!ESPTime::strptime(date, val)) {
|
if (!ESPTime::strptime(date, len, val)) {
|
||||||
ESP_LOGE(TAG, "Could not convert the date string to an ESPTime object");
|
ESP_LOGE(TAG, "Could not convert the date string to an ESPTime object");
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,9 +15,7 @@ namespace esphome::datetime {
|
|||||||
#define LOG_DATETIME_DATE(prefix, type, obj) \
|
#define LOG_DATETIME_DATE(prefix, type, obj) \
|
||||||
if ((obj) != nullptr) { \
|
if ((obj) != nullptr) { \
|
||||||
ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \
|
ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \
|
||||||
if (!(obj)->get_icon_ref().empty()) { \
|
LOG_ENTITY_ICON(TAG, prefix, *(obj)); \
|
||||||
ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \
|
|
||||||
} \
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class DateCall;
|
class DateCall;
|
||||||
@@ -67,7 +65,9 @@ class DateCall {
|
|||||||
void perform();
|
void perform();
|
||||||
DateCall &set_date(uint16_t year, uint8_t month, uint8_t day);
|
DateCall &set_date(uint16_t year, uint8_t month, uint8_t day);
|
||||||
DateCall &set_date(ESPTime time);
|
DateCall &set_date(ESPTime time);
|
||||||
DateCall &set_date(const std::string &date);
|
DateCall &set_date(const char *date, size_t len);
|
||||||
|
DateCall &set_date(const char *date) { return this->set_date(date, strlen(date)); }
|
||||||
|
DateCall &set_date(const std::string &date) { return this->set_date(date.c_str(), date.size()); }
|
||||||
|
|
||||||
DateCall &set_year(uint16_t year) {
|
DateCall &set_year(uint16_t year) {
|
||||||
this->year_ = year;
|
this->year_ = year;
|
||||||
|
|||||||
@@ -163,9 +163,9 @@ DateTimeCall &DateTimeCall::set_datetime(ESPTime datetime) {
|
|||||||
datetime.second);
|
datetime.second);
|
||||||
};
|
};
|
||||||
|
|
||||||
DateTimeCall &DateTimeCall::set_datetime(const std::string &datetime) {
|
DateTimeCall &DateTimeCall::set_datetime(const char *datetime, size_t len) {
|
||||||
ESPTime val{};
|
ESPTime val{};
|
||||||
if (!ESPTime::strptime(datetime, val)) {
|
if (!ESPTime::strptime(datetime, len, val)) {
|
||||||
ESP_LOGE(TAG, "Could not convert the time string to an ESPTime object");
|
ESP_LOGE(TAG, "Could not convert the time string to an ESPTime object");
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,9 +15,7 @@ namespace esphome::datetime {
|
|||||||
#define LOG_DATETIME_DATETIME(prefix, type, obj) \
|
#define LOG_DATETIME_DATETIME(prefix, type, obj) \
|
||||||
if ((obj) != nullptr) { \
|
if ((obj) != nullptr) { \
|
||||||
ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \
|
ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \
|
||||||
if (!(obj)->get_icon_ref().empty()) { \
|
LOG_ENTITY_ICON(TAG, prefix, *(obj)); \
|
||||||
ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \
|
|
||||||
} \
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class DateTimeCall;
|
class DateTimeCall;
|
||||||
@@ -71,7 +69,11 @@ class DateTimeCall {
|
|||||||
void perform();
|
void perform();
|
||||||
DateTimeCall &set_datetime(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, uint8_t second);
|
DateTimeCall &set_datetime(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, uint8_t second);
|
||||||
DateTimeCall &set_datetime(ESPTime datetime);
|
DateTimeCall &set_datetime(ESPTime datetime);
|
||||||
DateTimeCall &set_datetime(const std::string &datetime);
|
DateTimeCall &set_datetime(const char *datetime, size_t len);
|
||||||
|
DateTimeCall &set_datetime(const char *datetime) { return this->set_datetime(datetime, strlen(datetime)); }
|
||||||
|
DateTimeCall &set_datetime(const std::string &datetime) {
|
||||||
|
return this->set_datetime(datetime.c_str(), datetime.size());
|
||||||
|
}
|
||||||
DateTimeCall &set_datetime(time_t epoch_seconds);
|
DateTimeCall &set_datetime(time_t epoch_seconds);
|
||||||
|
|
||||||
DateTimeCall &set_year(uint16_t year) {
|
DateTimeCall &set_year(uint16_t year) {
|
||||||
|
|||||||
@@ -74,9 +74,9 @@ TimeCall &TimeCall::set_time(uint8_t hour, uint8_t minute, uint8_t second) {
|
|||||||
|
|
||||||
TimeCall &TimeCall::set_time(ESPTime time) { return this->set_time(time.hour, time.minute, time.second); };
|
TimeCall &TimeCall::set_time(ESPTime time) { return this->set_time(time.hour, time.minute, time.second); };
|
||||||
|
|
||||||
TimeCall &TimeCall::set_time(const std::string &time) {
|
TimeCall &TimeCall::set_time(const char *time, size_t len) {
|
||||||
ESPTime val{};
|
ESPTime val{};
|
||||||
if (!ESPTime::strptime(time, val)) {
|
if (!ESPTime::strptime(time, len, val)) {
|
||||||
ESP_LOGE(TAG, "Could not convert the time string to an ESPTime object");
|
ESP_LOGE(TAG, "Could not convert the time string to an ESPTime object");
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,9 +15,7 @@ namespace esphome::datetime {
|
|||||||
#define LOG_DATETIME_TIME(prefix, type, obj) \
|
#define LOG_DATETIME_TIME(prefix, type, obj) \
|
||||||
if ((obj) != nullptr) { \
|
if ((obj) != nullptr) { \
|
||||||
ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \
|
ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \
|
||||||
if (!(obj)->get_icon_ref().empty()) { \
|
LOG_ENTITY_ICON(TAG, prefix, *(obj)); \
|
||||||
ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \
|
|
||||||
} \
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class TimeCall;
|
class TimeCall;
|
||||||
@@ -69,7 +67,9 @@ class TimeCall {
|
|||||||
void perform();
|
void perform();
|
||||||
TimeCall &set_time(uint8_t hour, uint8_t minute, uint8_t second);
|
TimeCall &set_time(uint8_t hour, uint8_t minute, uint8_t second);
|
||||||
TimeCall &set_time(ESPTime time);
|
TimeCall &set_time(ESPTime time);
|
||||||
TimeCall &set_time(const std::string &time);
|
TimeCall &set_time(const char *time, size_t len);
|
||||||
|
TimeCall &set_time(const char *time) { return this->set_time(time, strlen(time)); }
|
||||||
|
TimeCall &set_time(const std::string &time) { return this->set_time(time.c_str(), time.size()); }
|
||||||
|
|
||||||
TimeCall &set_hour(uint8_t hour) {
|
TimeCall &set_hour(uint8_t hour) {
|
||||||
this->hour_ = hour;
|
this->hour_ = hour;
|
||||||
|
|||||||
@@ -3,21 +3,80 @@
|
|||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
#include <Esp.h>
|
#include <Esp.h>
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
#include <user_interface.h>
|
||||||
|
|
||||||
|
// Global reset info struct populated by SDK at boot
|
||||||
|
extern struct rst_info resetInfo;
|
||||||
|
|
||||||
|
// Core version - either a string pointer or a version number to format as hex
|
||||||
|
extern uint32_t core_version;
|
||||||
|
extern const char *core_release;
|
||||||
|
}
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace debug {
|
namespace debug {
|
||||||
|
|
||||||
static const char *const TAG = "debug";
|
static const char *const TAG = "debug";
|
||||||
|
|
||||||
|
// Get reset reason string from reason code (no heap allocation)
|
||||||
|
// Returns LogString* pointing to flash (PROGMEM) on ESP8266
|
||||||
|
static const LogString *get_reset_reason_str(uint32_t reason) {
|
||||||
|
switch (reason) {
|
||||||
|
case REASON_DEFAULT_RST:
|
||||||
|
return LOG_STR("Power On");
|
||||||
|
case REASON_WDT_RST:
|
||||||
|
return LOG_STR("Hardware Watchdog");
|
||||||
|
case REASON_EXCEPTION_RST:
|
||||||
|
return LOG_STR("Exception");
|
||||||
|
case REASON_SOFT_WDT_RST:
|
||||||
|
return LOG_STR("Software Watchdog");
|
||||||
|
case REASON_SOFT_RESTART:
|
||||||
|
return LOG_STR("Software/System restart");
|
||||||
|
case REASON_DEEP_SLEEP_AWAKE:
|
||||||
|
return LOG_STR("Deep-Sleep Wake");
|
||||||
|
case REASON_EXT_SYS_RST:
|
||||||
|
return LOG_STR("External System");
|
||||||
|
default:
|
||||||
|
return LOG_STR("Unknown");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size for core version hex buffer
|
||||||
|
static constexpr size_t CORE_VERSION_BUFFER_SIZE = 12;
|
||||||
|
|
||||||
|
// Get core version string (no heap allocation)
|
||||||
|
// Returns either core_release directly or formats core_version as hex into provided buffer
|
||||||
|
static const char *get_core_version_str(std::span<char, CORE_VERSION_BUFFER_SIZE> buffer) {
|
||||||
|
if (core_release != nullptr) {
|
||||||
|
return core_release;
|
||||||
|
}
|
||||||
|
snprintf_P(buffer.data(), CORE_VERSION_BUFFER_SIZE, PSTR("%08x"), core_version);
|
||||||
|
return buffer.data();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size for reset info buffer
|
||||||
|
static constexpr size_t RESET_INFO_BUFFER_SIZE = 200;
|
||||||
|
|
||||||
|
// Get detailed reset info string (no heap allocation)
|
||||||
|
// For watchdog/exception resets, includes detailed exception info
|
||||||
|
static const char *get_reset_info_str(std::span<char, RESET_INFO_BUFFER_SIZE> buffer, uint32_t reason) {
|
||||||
|
if (reason >= REASON_WDT_RST && reason <= REASON_SOFT_WDT_RST) {
|
||||||
|
snprintf_P(buffer.data(), RESET_INFO_BUFFER_SIZE,
|
||||||
|
PSTR("Fatal exception:%d flag:%d (%s) epc1:0x%08x epc2:0x%08x epc3:0x%08x excvaddr:0x%08x depc:0x%08x"),
|
||||||
|
static_cast<int>(resetInfo.exccause), static_cast<int>(reason),
|
||||||
|
LOG_STR_ARG(get_reset_reason_str(reason)), resetInfo.epc1, resetInfo.epc2, resetInfo.epc3,
|
||||||
|
resetInfo.excvaddr, resetInfo.depc);
|
||||||
|
return buffer.data();
|
||||||
|
}
|
||||||
|
return LOG_STR_ARG(get_reset_reason_str(reason));
|
||||||
|
}
|
||||||
|
|
||||||
const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) {
|
const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) {
|
||||||
char *buf = buffer.data();
|
// Copy from flash to provided buffer
|
||||||
#if !defined(CLANG_TIDY)
|
strncpy_P(buffer.data(), (PGM_P) get_reset_reason_str(resetInfo.reason), RESET_REASON_BUFFER_SIZE - 1);
|
||||||
String reason = ESP.getResetReason(); // NOLINT
|
buffer[RESET_REASON_BUFFER_SIZE - 1] = '\0';
|
||||||
snprintf_P(buf, RESET_REASON_BUFFER_SIZE, PSTR("%s"), reason.c_str());
|
return buffer.data();
|
||||||
return buf;
|
|
||||||
#else
|
|
||||||
buf[0] = '\0';
|
|
||||||
return buf;
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) {
|
const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) {
|
||||||
@@ -33,37 +92,42 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
|
|||||||
constexpr size_t size = DEVICE_INFO_BUFFER_SIZE;
|
constexpr size_t size = DEVICE_INFO_BUFFER_SIZE;
|
||||||
char *buf = buffer.data();
|
char *buf = buffer.data();
|
||||||
|
|
||||||
const char *flash_mode;
|
const LogString *flash_mode;
|
||||||
switch (ESP.getFlashChipMode()) { // NOLINT(readability-static-accessed-through-instance)
|
switch (ESP.getFlashChipMode()) { // NOLINT(readability-static-accessed-through-instance)
|
||||||
case FM_QIO:
|
case FM_QIO:
|
||||||
flash_mode = "QIO";
|
flash_mode = LOG_STR("QIO");
|
||||||
break;
|
break;
|
||||||
case FM_QOUT:
|
case FM_QOUT:
|
||||||
flash_mode = "QOUT";
|
flash_mode = LOG_STR("QOUT");
|
||||||
break;
|
break;
|
||||||
case FM_DIO:
|
case FM_DIO:
|
||||||
flash_mode = "DIO";
|
flash_mode = LOG_STR("DIO");
|
||||||
break;
|
break;
|
||||||
case FM_DOUT:
|
case FM_DOUT:
|
||||||
flash_mode = "DOUT";
|
flash_mode = LOG_STR("DOUT");
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
flash_mode = "UNKNOWN";
|
flash_mode = LOG_STR("UNKNOWN");
|
||||||
}
|
}
|
||||||
uint32_t flash_size = ESP.getFlashChipSize() / 1024; // NOLINT
|
uint32_t flash_size = ESP.getFlashChipSize() / 1024; // NOLINT(readability-static-accessed-through-instance)
|
||||||
uint32_t flash_speed = ESP.getFlashChipSpeed() / 1000000; // NOLINT
|
uint32_t flash_speed = ESP.getFlashChipSpeed() / 1000000; // NOLINT(readability-static-accessed-through-instance)
|
||||||
ESP_LOGD(TAG, "Flash Chip: Size=%" PRIu32 "kB Speed=%" PRIu32 "MHz Mode=%s", flash_size, flash_speed, flash_mode);
|
ESP_LOGD(TAG, "Flash Chip: Size=%" PRIu32 "kB Speed=%" PRIu32 "MHz Mode=%s", flash_size, flash_speed,
|
||||||
|
LOG_STR_ARG(flash_mode));
|
||||||
pos = buf_append_printf(buf, size, pos, "|Flash: %" PRIu32 "kB Speed:%" PRIu32 "MHz Mode:%s", flash_size, flash_speed,
|
pos = buf_append_printf(buf, size, pos, "|Flash: %" PRIu32 "kB Speed:%" PRIu32 "MHz Mode:%s", flash_size, flash_speed,
|
||||||
flash_mode);
|
LOG_STR_ARG(flash_mode));
|
||||||
|
|
||||||
#if !defined(CLANG_TIDY)
|
|
||||||
char reason_buffer[RESET_REASON_BUFFER_SIZE];
|
char reason_buffer[RESET_REASON_BUFFER_SIZE];
|
||||||
const char *reset_reason = get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE>(reason_buffer));
|
const char *reset_reason = get_reset_reason_(reason_buffer);
|
||||||
|
char core_version_buffer[CORE_VERSION_BUFFER_SIZE];
|
||||||
|
char reset_info_buffer[RESET_INFO_BUFFER_SIZE];
|
||||||
|
// NOLINTBEGIN(readability-static-accessed-through-instance)
|
||||||
uint32_t chip_id = ESP.getChipId();
|
uint32_t chip_id = ESP.getChipId();
|
||||||
uint8_t boot_version = ESP.getBootVersion();
|
uint8_t boot_version = ESP.getBootVersion();
|
||||||
uint8_t boot_mode = ESP.getBootMode();
|
uint8_t boot_mode = ESP.getBootMode();
|
||||||
uint8_t cpu_freq = ESP.getCpuFreqMHz();
|
uint8_t cpu_freq = ESP.getCpuFreqMHz();
|
||||||
uint32_t flash_chip_id = ESP.getFlashChipId();
|
uint32_t flash_chip_id = ESP.getFlashChipId();
|
||||||
|
const char *sdk_version = ESP.getSdkVersion();
|
||||||
|
// NOLINTEND(readability-static-accessed-through-instance)
|
||||||
|
|
||||||
ESP_LOGD(TAG,
|
ESP_LOGD(TAG,
|
||||||
"Chip ID: 0x%08" PRIX32 "\n"
|
"Chip ID: 0x%08" PRIX32 "\n"
|
||||||
@@ -74,19 +138,18 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
|
|||||||
"Flash Chip ID=0x%08" PRIX32 "\n"
|
"Flash Chip ID=0x%08" PRIX32 "\n"
|
||||||
"Reset Reason: %s\n"
|
"Reset Reason: %s\n"
|
||||||
"Reset Info: %s",
|
"Reset Info: %s",
|
||||||
chip_id, ESP.getSdkVersion(), ESP.getCoreVersion().c_str(), boot_version, boot_mode, cpu_freq, flash_chip_id,
|
chip_id, sdk_version, get_core_version_str(core_version_buffer), boot_version, boot_mode, cpu_freq,
|
||||||
reset_reason, ESP.getResetInfo().c_str());
|
flash_chip_id, reset_reason, get_reset_info_str(reset_info_buffer, resetInfo.reason));
|
||||||
|
|
||||||
pos = buf_append_printf(buf, size, pos, "|Chip: 0x%08" PRIX32, chip_id);
|
pos = buf_append_printf(buf, size, pos, "|Chip: 0x%08" PRIX32, chip_id);
|
||||||
pos = buf_append_printf(buf, size, pos, "|SDK: %s", ESP.getSdkVersion());
|
pos = buf_append_printf(buf, size, pos, "|SDK: %s", sdk_version);
|
||||||
pos = buf_append_printf(buf, size, pos, "|Core: %s", ESP.getCoreVersion().c_str());
|
pos = buf_append_printf(buf, size, pos, "|Core: %s", get_core_version_str(core_version_buffer));
|
||||||
pos = buf_append_printf(buf, size, pos, "|Boot: %u", boot_version);
|
pos = buf_append_printf(buf, size, pos, "|Boot: %u", boot_version);
|
||||||
pos = buf_append_printf(buf, size, pos, "|Mode: %u", boot_mode);
|
pos = buf_append_printf(buf, size, pos, "|Mode: %u", boot_mode);
|
||||||
pos = buf_append_printf(buf, size, pos, "|CPU: %u", cpu_freq);
|
pos = buf_append_printf(buf, size, pos, "|CPU: %u", cpu_freq);
|
||||||
pos = buf_append_printf(buf, size, pos, "|Flash: 0x%08" PRIX32, flash_chip_id);
|
pos = buf_append_printf(buf, size, pos, "|Flash: 0x%08" PRIX32, flash_chip_id);
|
||||||
pos = buf_append_printf(buf, size, pos, "|Reset: %s", reset_reason);
|
pos = buf_append_printf(buf, size, pos, "|Reset: %s", reset_reason);
|
||||||
pos = buf_append_printf(buf, size, pos, "|%s", ESP.getResetInfo().c_str());
|
pos = buf_append_printf(buf, size, pos, "|%s", get_reset_info_str(reset_info_buffer, resetInfo.reason));
|
||||||
#endif
|
|
||||||
|
|
||||||
return pos;
|
return pos;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,6 +132,26 @@ void DebugComponent::log_partition_info_() {
|
|||||||
flash_area_foreach(fa_cb, nullptr);
|
flash_area_foreach(fa_cb, nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static const char *regout0_to_str(uint32_t value) {
|
||||||
|
switch (value) {
|
||||||
|
case (UICR_REGOUT0_VOUT_DEFAULT):
|
||||||
|
return "1.8V (default)";
|
||||||
|
case (UICR_REGOUT0_VOUT_1V8):
|
||||||
|
return "1.8V";
|
||||||
|
case (UICR_REGOUT0_VOUT_2V1):
|
||||||
|
return "2.1V";
|
||||||
|
case (UICR_REGOUT0_VOUT_2V4):
|
||||||
|
return "2.4V";
|
||||||
|
case (UICR_REGOUT0_VOUT_2V7):
|
||||||
|
return "2.7V";
|
||||||
|
case (UICR_REGOUT0_VOUT_3V0):
|
||||||
|
return "3.0V";
|
||||||
|
case (UICR_REGOUT0_VOUT_3V3):
|
||||||
|
return "3.3V";
|
||||||
|
}
|
||||||
|
return "???V";
|
||||||
|
}
|
||||||
|
|
||||||
size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE> buffer, size_t pos) {
|
size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE> buffer, size_t pos) {
|
||||||
constexpr size_t size = DEVICE_INFO_BUFFER_SIZE;
|
constexpr size_t size = DEVICE_INFO_BUFFER_SIZE;
|
||||||
char *buf = buffer.data();
|
char *buf = buffer.data();
|
||||||
@@ -145,34 +165,14 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
|
|||||||
// Regulator stage 0
|
// Regulator stage 0
|
||||||
if (nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_HIGH) {
|
if (nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_HIGH) {
|
||||||
const char *reg0_type = nrf_power_dcdcen_vddh_get(NRF_POWER) ? "DC/DC" : "LDO";
|
const char *reg0_type = nrf_power_dcdcen_vddh_get(NRF_POWER) ? "DC/DC" : "LDO";
|
||||||
const char *reg0_voltage;
|
const char *reg0_voltage = regout0_to_str((NRF_UICR->REGOUT0 & UICR_REGOUT0_VOUT_Msk) >> UICR_REGOUT0_VOUT_Pos);
|
||||||
switch (NRF_UICR->REGOUT0 & UICR_REGOUT0_VOUT_Msk) {
|
|
||||||
case (UICR_REGOUT0_VOUT_DEFAULT << UICR_REGOUT0_VOUT_Pos):
|
|
||||||
reg0_voltage = "1.8V (default)";
|
|
||||||
break;
|
|
||||||
case (UICR_REGOUT0_VOUT_1V8 << UICR_REGOUT0_VOUT_Pos):
|
|
||||||
reg0_voltage = "1.8V";
|
|
||||||
break;
|
|
||||||
case (UICR_REGOUT0_VOUT_2V1 << UICR_REGOUT0_VOUT_Pos):
|
|
||||||
reg0_voltage = "2.1V";
|
|
||||||
break;
|
|
||||||
case (UICR_REGOUT0_VOUT_2V4 << UICR_REGOUT0_VOUT_Pos):
|
|
||||||
reg0_voltage = "2.4V";
|
|
||||||
break;
|
|
||||||
case (UICR_REGOUT0_VOUT_2V7 << UICR_REGOUT0_VOUT_Pos):
|
|
||||||
reg0_voltage = "2.7V";
|
|
||||||
break;
|
|
||||||
case (UICR_REGOUT0_VOUT_3V0 << UICR_REGOUT0_VOUT_Pos):
|
|
||||||
reg0_voltage = "3.0V";
|
|
||||||
break;
|
|
||||||
case (UICR_REGOUT0_VOUT_3V3 << UICR_REGOUT0_VOUT_Pos):
|
|
||||||
reg0_voltage = "3.3V";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
reg0_voltage = "???V";
|
|
||||||
}
|
|
||||||
ESP_LOGD(TAG, "Regulator stage 0: %s, %s", reg0_type, reg0_voltage);
|
ESP_LOGD(TAG, "Regulator stage 0: %s, %s", reg0_type, reg0_voltage);
|
||||||
pos = buf_append_printf(buf, size, pos, "|Regulator stage 0: %s, %s", reg0_type, reg0_voltage);
|
pos = buf_append_printf(buf, size, pos, "|Regulator stage 0: %s, %s", reg0_type, reg0_voltage);
|
||||||
|
#ifdef USE_NRF52_REG0_VOUT
|
||||||
|
if ((NRF_UICR->REGOUT0 & UICR_REGOUT0_VOUT_Msk) >> UICR_REGOUT0_VOUT_Pos != USE_NRF52_REG0_VOUT) {
|
||||||
|
ESP_LOGE(TAG, "Regulator stage 0: expected %s", regout0_to_str(USE_NRF52_REG0_VOUT));
|
||||||
|
}
|
||||||
|
#endif
|
||||||
} else {
|
} else {
|
||||||
ESP_LOGD(TAG, "Regulator stage 0: disabled");
|
ESP_LOGD(TAG, "Regulator stage 0: disabled");
|
||||||
pos = buf_append_printf(buf, size, pos, "|Regulator stage 0: disabled");
|
pos = buf_append_printf(buf, size, pos, "|Regulator stage 0: disabled");
|
||||||
|
|||||||
@@ -89,10 +89,8 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r
|
|||||||
delayMicroseconds(500);
|
delayMicroseconds(500);
|
||||||
} else if (this->model_ == DHT_MODEL_DHT22_TYPE2) {
|
} else if (this->model_ == DHT_MODEL_DHT22_TYPE2) {
|
||||||
delayMicroseconds(2000);
|
delayMicroseconds(2000);
|
||||||
} else if (this->model_ == DHT_MODEL_AM2120 || this->model_ == DHT_MODEL_AM2302) {
|
|
||||||
delayMicroseconds(1000);
|
|
||||||
} else {
|
} else {
|
||||||
delayMicroseconds(800);
|
delayMicroseconds(1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef USE_ESP32
|
#ifdef USE_ESP32
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from esphome.const import (
|
|||||||
CONF_UPDATE_INTERVAL,
|
CONF_UPDATE_INTERVAL,
|
||||||
SCHEDULER_DONT_RUN,
|
SCHEDULER_DONT_RUN,
|
||||||
)
|
)
|
||||||
from esphome.core import CoroPriority, coroutine_with_priority
|
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||||
|
|
||||||
IS_PLATFORM_COMPONENT = True
|
IS_PLATFORM_COMPONENT = True
|
||||||
|
|
||||||
@@ -63,13 +63,11 @@ def validate_auto_clear(value):
|
|||||||
return cv.boolean(value)
|
return cv.boolean(value)
|
||||||
|
|
||||||
|
|
||||||
def basic_display_schema(default_update_interval: str = "1s") -> cv.Schema:
|
BASIC_DISPLAY_SCHEMA = cv.Schema(
|
||||||
"""Create a basic display schema with configurable default update interval."""
|
{
|
||||||
return cv.Schema(
|
cv.Exclusive(CONF_LAMBDA, CONF_LAMBDA): cv.lambda_,
|
||||||
{
|
}
|
||||||
cv.Exclusive(CONF_LAMBDA, CONF_LAMBDA): cv.lambda_,
|
).extend(cv.polling_component_schema("1s"))
|
||||||
}
|
|
||||||
).extend(cv.polling_component_schema(default_update_interval))
|
|
||||||
|
|
||||||
|
|
||||||
def _validate_test_card(config):
|
def _validate_test_card(config):
|
||||||
@@ -83,41 +81,34 @@ def _validate_test_card(config):
|
|||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
def full_display_schema(default_update_interval: str = "1s") -> cv.Schema:
|
FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend(
|
||||||
"""Create a full display schema with configurable default update interval."""
|
{
|
||||||
schema = basic_display_schema(default_update_interval).extend(
|
cv.Optional(CONF_ROTATION): validate_rotation,
|
||||||
{
|
cv.Exclusive(CONF_PAGES, CONF_LAMBDA): cv.All(
|
||||||
cv.Optional(CONF_ROTATION): validate_rotation,
|
cv.ensure_list(
|
||||||
cv.Exclusive(CONF_PAGES, CONF_LAMBDA): cv.All(
|
|
||||||
cv.ensure_list(
|
|
||||||
{
|
|
||||||
cv.GenerateID(): cv.declare_id(DisplayPage),
|
|
||||||
cv.Required(CONF_LAMBDA): cv.lambda_,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
cv.Length(min=1),
|
|
||||||
),
|
|
||||||
cv.Optional(CONF_ON_PAGE_CHANGE): automation.validate_automation(
|
|
||||||
{
|
{
|
||||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
cv.GenerateID(): cv.declare_id(DisplayPage),
|
||||||
DisplayOnPageChangeTrigger
|
cv.Required(CONF_LAMBDA): cv.lambda_,
|
||||||
),
|
|
||||||
cv.Optional(CONF_FROM): cv.use_id(DisplayPage),
|
|
||||||
cv.Optional(CONF_TO): cv.use_id(DisplayPage),
|
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
cv.Optional(
|
cv.Length(min=1),
|
||||||
CONF_AUTO_CLEAR_ENABLED, default=CONF_UNSPECIFIED
|
),
|
||||||
): validate_auto_clear,
|
cv.Optional(CONF_ON_PAGE_CHANGE): automation.validate_automation(
|
||||||
cv.Optional(CONF_SHOW_TEST_CARD): cv.boolean,
|
{
|
||||||
}
|
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||||
)
|
DisplayOnPageChangeTrigger
|
||||||
schema.add_extra(_validate_test_card)
|
),
|
||||||
return schema
|
cv.Optional(CONF_FROM): cv.use_id(DisplayPage),
|
||||||
|
cv.Optional(CONF_TO): cv.use_id(DisplayPage),
|
||||||
|
}
|
||||||
BASIC_DISPLAY_SCHEMA = basic_display_schema("1s")
|
),
|
||||||
FULL_DISPLAY_SCHEMA = full_display_schema("1s")
|
cv.Optional(
|
||||||
|
CONF_AUTO_CLEAR_ENABLED, default=CONF_UNSPECIFIED
|
||||||
|
): validate_auto_clear,
|
||||||
|
cv.Optional(CONF_SHOW_TEST_CARD): cv.boolean,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
FULL_DISPLAY_SCHEMA.add_extra(_validate_test_card)
|
||||||
|
|
||||||
|
|
||||||
async def setup_display_core_(var, config):
|
async def setup_display_core_(var, config):
|
||||||
@@ -231,3 +222,8 @@ async def display_is_displaying_page_to_code(config, condition_id, template_arg,
|
|||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
cg.add_global(display_ns.using)
|
cg.add_global(display_ns.using)
|
||||||
cg.add_define("USE_DISPLAY")
|
cg.add_define("USE_DISPLAY")
|
||||||
|
if CORE.is_esp32:
|
||||||
|
# Re-enable ESP-IDF's LCD driver (excluded by default to save compile time)
|
||||||
|
from esphome.components.esp32 import include_builtin_idf_component
|
||||||
|
|
||||||
|
include_builtin_idf_component("esp_lcd")
|
||||||
|
|||||||
@@ -25,29 +25,13 @@ dsmr_ns = cg.esphome_ns.namespace("esphome::dsmr")
|
|||||||
Dsmr = dsmr_ns.class_("Dsmr", cg.Component, uart.UARTDevice)
|
Dsmr = dsmr_ns.class_("Dsmr", cg.Component, uart.UARTDevice)
|
||||||
|
|
||||||
|
|
||||||
def _validate_key(value):
|
|
||||||
value = cv.string_strict(value)
|
|
||||||
parts = [value[i : i + 2] for i in range(0, len(value), 2)]
|
|
||||||
if len(parts) != 16:
|
|
||||||
raise cv.Invalid("Decryption key must consist of 16 hexadecimal numbers")
|
|
||||||
parts_int = []
|
|
||||||
if any(len(part) != 2 for part in parts):
|
|
||||||
raise cv.Invalid("Decryption key must be format XX")
|
|
||||||
for part in parts:
|
|
||||||
try:
|
|
||||||
parts_int.append(int(part, 16))
|
|
||||||
except ValueError:
|
|
||||||
# pylint: disable=raise-missing-from
|
|
||||||
raise cv.Invalid("Decryption key must be hex values from 00 to FF")
|
|
||||||
|
|
||||||
return "".join(f"{part:02X}" for part in parts_int)
|
|
||||||
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.All(
|
CONFIG_SCHEMA = cv.All(
|
||||||
cv.Schema(
|
cv.Schema(
|
||||||
{
|
{
|
||||||
cv.GenerateID(): cv.declare_id(Dsmr),
|
cv.GenerateID(): cv.declare_id(Dsmr),
|
||||||
cv.Optional(CONF_DECRYPTION_KEY): _validate_key,
|
cv.Optional(CONF_DECRYPTION_KEY): lambda value: cv.bind_key(
|
||||||
|
value, name="Decryption key"
|
||||||
|
),
|
||||||
cv.Optional(CONF_CRC_CHECK, default=True): cv.boolean,
|
cv.Optional(CONF_CRC_CHECK, default=True): cv.boolean,
|
||||||
cv.Optional(CONF_GAS_MBUS_ID, default=1): cv.int_,
|
cv.Optional(CONF_GAS_MBUS_ID, default=1): cv.int_,
|
||||||
cv.Optional(CONF_WATER_MBUS_ID, default=2): cv.int_,
|
cv.Optional(CONF_WATER_MBUS_ID, default=2): cv.int_,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "dsmr.h"
|
#include "dsmr.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
#include <AES.h>
|
#include <AES.h>
|
||||||
@@ -294,8 +295,8 @@ void Dsmr::dump_config() {
|
|||||||
DSMR_TEXT_SENSOR_LIST(DSMR_LOG_TEXT_SENSOR, )
|
DSMR_TEXT_SENSOR_LIST(DSMR_LOG_TEXT_SENSOR, )
|
||||||
}
|
}
|
||||||
|
|
||||||
void Dsmr::set_decryption_key(const std::string &decryption_key) {
|
void Dsmr::set_decryption_key(const char *decryption_key) {
|
||||||
if (decryption_key.empty()) {
|
if (decryption_key == nullptr || decryption_key[0] == '\0') {
|
||||||
ESP_LOGI(TAG, "Disabling decryption");
|
ESP_LOGI(TAG, "Disabling decryption");
|
||||||
this->decryption_key_.clear();
|
this->decryption_key_.clear();
|
||||||
if (this->crypt_telegram_ != nullptr) {
|
if (this->crypt_telegram_ != nullptr) {
|
||||||
@@ -305,21 +306,15 @@ void Dsmr::set_decryption_key(const std::string &decryption_key) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (decryption_key.length() != 32) {
|
if (!parse_hex(decryption_key, this->decryption_key_, 16)) {
|
||||||
ESP_LOGE(TAG, "Error, decryption key must be 32 character long");
|
ESP_LOGE(TAG, "Error, decryption key must be 32 hex characters");
|
||||||
|
this->decryption_key_.clear();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this->decryption_key_.clear();
|
|
||||||
|
|
||||||
ESP_LOGI(TAG, "Decryption key is set");
|
ESP_LOGI(TAG, "Decryption key is set");
|
||||||
// Verbose level prints decryption key
|
// Verbose level prints decryption key
|
||||||
ESP_LOGV(TAG, "Using decryption key: %s", decryption_key.c_str());
|
ESP_LOGV(TAG, "Using decryption key: %s", decryption_key);
|
||||||
|
|
||||||
char temp[3] = {0};
|
|
||||||
for (int i = 0; i < 16; i++) {
|
|
||||||
strncpy(temp, &(decryption_key.c_str()[i * 2]), 2);
|
|
||||||
this->decryption_key_.push_back(std::strtoul(temp, nullptr, 16));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this->crypt_telegram_ == nullptr) {
|
if (this->crypt_telegram_ == nullptr) {
|
||||||
this->crypt_telegram_ = new uint8_t[this->max_telegram_len_]; // NOLINT
|
this->crypt_telegram_ = new uint8_t[this->max_telegram_len_]; // NOLINT
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ class Dsmr : public Component, public uart::UARTDevice {
|
|||||||
|
|
||||||
void dump_config() override;
|
void dump_config() override;
|
||||||
|
|
||||||
void set_decryption_key(const std::string &decryption_key);
|
void set_decryption_key(const char *decryption_key);
|
||||||
void set_max_telegram_length(size_t length) { this->max_telegram_len_ = length; }
|
void set_max_telegram_length(size_t length) { this->max_telegram_len_ = length; }
|
||||||
void set_request_pin(GPIOPin *request_pin) { this->request_pin_ = request_pin; }
|
void set_request_pin(GPIOPin *request_pin) { this->request_pin_ = request_pin; }
|
||||||
void set_request_interval(uint32_t interval) { this->request_interval_ = interval; }
|
void set_request_interval(uint32_t interval) { this->request_interval_ = interval; }
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ void DutyTimeSensor::setup() {
|
|||||||
uint32_t seconds = 0;
|
uint32_t seconds = 0;
|
||||||
|
|
||||||
if (this->restore_) {
|
if (this->restore_) {
|
||||||
this->pref_ = global_preferences->make_preference<uint32_t>(this->get_preference_hash());
|
this->pref_ = this->make_entity_preference<uint32_t>();
|
||||||
this->pref_.load(&seconds);
|
this->pref_.load(&seconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ from esphome.const import (
|
|||||||
CONF_TRANSFORM,
|
CONF_TRANSFORM,
|
||||||
CONF_UPDATE_INTERVAL,
|
CONF_UPDATE_INTERVAL,
|
||||||
CONF_WIDTH,
|
CONF_WIDTH,
|
||||||
SCHEDULER_DONT_RUN,
|
|
||||||
)
|
)
|
||||||
from esphome.cpp_generator import RawExpression
|
from esphome.cpp_generator import RawExpression
|
||||||
from esphome.final_validate import full_config
|
from esphome.final_validate import full_config
|
||||||
@@ -73,10 +72,12 @@ TRANSFORM_OPTIONS = {CONF_MIRROR_X, CONF_MIRROR_Y, CONF_SWAP_XY}
|
|||||||
def model_schema(config):
|
def model_schema(config):
|
||||||
model = MODELS[config[CONF_MODEL]]
|
model = MODELS[config[CONF_MODEL]]
|
||||||
class_name = epaper_spi_ns.class_(model.class_name, EPaperBase)
|
class_name = epaper_spi_ns.class_(model.class_name, EPaperBase)
|
||||||
|
minimum_update_interval = update_interval(
|
||||||
|
model.get_default(CONF_MINIMUM_UPDATE_INTERVAL, "1s")
|
||||||
|
)
|
||||||
cv_dimensions = cv.Optional if model.get_default(CONF_WIDTH) else cv.Required
|
cv_dimensions = cv.Optional if model.get_default(CONF_WIDTH) else cv.Required
|
||||||
return (
|
return (
|
||||||
display.full_display_schema("60s")
|
display.FULL_DISPLAY_SCHEMA.extend(
|
||||||
.extend(
|
|
||||||
spi.spi_device_schema(
|
spi.spi_device_schema(
|
||||||
cs_pin_required=False,
|
cs_pin_required=False,
|
||||||
default_mode="MODE0",
|
default_mode="MODE0",
|
||||||
@@ -93,6 +94,9 @@ def model_schema(config):
|
|||||||
{
|
{
|
||||||
cv.Optional(CONF_ROTATION, default=0): validate_rotation,
|
cv.Optional(CONF_ROTATION, default=0): validate_rotation,
|
||||||
cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True),
|
cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True),
|
||||||
|
cv.Optional(CONF_UPDATE_INTERVAL, default=cv.UNDEFINED): cv.All(
|
||||||
|
update_interval, cv.Range(min=minimum_update_interval)
|
||||||
|
),
|
||||||
cv.Optional(CONF_TRANSFORM): cv.Schema(
|
cv.Optional(CONF_TRANSFORM): cv.Schema(
|
||||||
{
|
{
|
||||||
cv.Required(CONF_MIRROR_X): cv.boolean,
|
cv.Required(CONF_MIRROR_X): cv.boolean,
|
||||||
@@ -146,22 +150,15 @@ def _final_validate(config):
|
|||||||
global_config = full_config.get()
|
global_config = full_config.get()
|
||||||
from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN
|
from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN
|
||||||
|
|
||||||
# If no drawing methods are configured, and LVGL is not enabled, show a test card
|
if CONF_LAMBDA not in config and CONF_PAGES not in config:
|
||||||
if (
|
if LVGL_DOMAIN in global_config:
|
||||||
CONF_LAMBDA not in config
|
if CONF_UPDATE_INTERVAL not in config:
|
||||||
and CONF_PAGES not in config
|
config[CONF_UPDATE_INTERVAL] = update_interval("never")
|
||||||
and LVGL_DOMAIN not in global_config
|
else:
|
||||||
):
|
# If no drawing methods are configured, and LVGL is not enabled, show a test card
|
||||||
config[CONF_SHOW_TEST_CARD] = True
|
config[CONF_SHOW_TEST_CARD] = True
|
||||||
|
elif CONF_UPDATE_INTERVAL not in config:
|
||||||
interval = config[CONF_UPDATE_INTERVAL]
|
config[CONF_UPDATE_INTERVAL] = update_interval("1min")
|
||||||
if interval != SCHEDULER_DONT_RUN:
|
|
||||||
model = MODELS[config[CONF_MODEL]]
|
|
||||||
minimum = update_interval(model.get_default(CONF_MINIMUM_UPDATE_INTERVAL, "1s"))
|
|
||||||
if interval < minimum:
|
|
||||||
raise cv.Invalid(
|
|
||||||
f"update_interval must be at least {minimum} for {model.name}, got {interval}"
|
|
||||||
)
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
@@ -193,7 +190,7 @@ async def to_code(config):
|
|||||||
# Rotation is handled by setting the transform
|
# Rotation is handled by setting the transform
|
||||||
display_config = {k: v for k, v in config.items() if k != CONF_ROTATION}
|
display_config = {k: v for k, v in config.items() if k != CONF_ROTATION}
|
||||||
await display.register_display(var, display_config)
|
await display.register_display(var, display_config)
|
||||||
await spi.register_spi_device(var, config)
|
await spi.register_spi_device(var, config, write_only=True)
|
||||||
|
|
||||||
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
|
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
|
||||||
cg.add(var.set_dc_pin(dc))
|
cg.add(var.set_dc_pin(dc))
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import esphome.codegen as cg
|
|||||||
from esphome.components import i2c
|
from esphome.components import i2c
|
||||||
from esphome.components.audio_dac import AudioDac
|
from esphome.components.audio_dac import AudioDac
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_ID
|
from esphome.const import CONF_AUDIO_DAC, CONF_BITS_PER_SAMPLE, CONF_ID
|
||||||
|
import esphome.final_validate as fv
|
||||||
|
|
||||||
CODEOWNERS = ["@kbx81"]
|
CODEOWNERS = ["@kbx81"]
|
||||||
DEPENDENCIES = ["i2c"]
|
DEPENDENCIES = ["i2c"]
|
||||||
@@ -21,6 +22,29 @@ CONFIG_SCHEMA = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _final_validate(config):
|
||||||
|
full_config = fv.full_config.get()
|
||||||
|
|
||||||
|
# Check all speaker configurations for ones that reference this es8156
|
||||||
|
speaker_configs = full_config.get("speaker", [])
|
||||||
|
for speaker_config in speaker_configs:
|
||||||
|
audio_dac_id = speaker_config.get(CONF_AUDIO_DAC)
|
||||||
|
if (
|
||||||
|
audio_dac_id is not None
|
||||||
|
and audio_dac_id == config[CONF_ID]
|
||||||
|
and (bits_per_sample := speaker_config.get(CONF_BITS_PER_SAMPLE))
|
||||||
|
is not None
|
||||||
|
and bits_per_sample > 24
|
||||||
|
):
|
||||||
|
raise cv.Invalid(
|
||||||
|
f"ES8156 does not support more than 24 bits per sample. "
|
||||||
|
f"The speaker referencing this audio_dac has bits_per_sample set to {bits_per_sample}."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||||
|
|
||||||
|
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
var = cg.new_Pvariable(config[CONF_ID])
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
await cg.register_component(var, config)
|
await cg.register_component(var, config)
|
||||||
|
|||||||
@@ -17,24 +17,61 @@ static const char *const TAG = "es8156";
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ES8156::setup() {
|
void ES8156::setup() {
|
||||||
|
// REG02 MODE CONFIG 1: Enable software mode for I2C control of volume/mute
|
||||||
|
// Bit 2: SOFT_MODE_SEL=1 (software mode enabled)
|
||||||
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG02_SCLK_MODE, 0x04));
|
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG02_SCLK_MODE, 0x04));
|
||||||
|
|
||||||
|
// Analog system configuration (active-low power down bits, active-high enables)
|
||||||
|
// REG20 ANALOG SYSTEM: Configure analog signal path
|
||||||
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG20_ANALOG_SYS1, 0x2A));
|
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG20_ANALOG_SYS1, 0x2A));
|
||||||
|
|
||||||
|
// REG21 ANALOG SYSTEM: VSEL=0x1C (bias level ~120%), normal VREF ramp speed
|
||||||
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG21_ANALOG_SYS2, 0x3C));
|
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG21_ANALOG_SYS2, 0x3C));
|
||||||
|
|
||||||
|
// REG22 ANALOG SYSTEM: Line out mode (HPSW=0), OUT_MUTE=0 (not muted)
|
||||||
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG22_ANALOG_SYS3, 0x00));
|
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG22_ANALOG_SYS3, 0x00));
|
||||||
|
|
||||||
|
// REG24 ANALOG SYSTEM: Low power mode for VREFBUF, HPCOM, DACVRP; DAC normal power
|
||||||
|
// Bits 2:0 = 0x07: LPVREFBUF=1, LPHPCOM=1, LPDACVRP=1, LPDAC=0
|
||||||
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG24_ANALOG_LP, 0x07));
|
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG24_ANALOG_LP, 0x07));
|
||||||
|
|
||||||
|
// REG23 ANALOG SYSTEM: Lowest bias (IBIAS_SW=0), VMIDLVL=VDDA/2, normal impedance
|
||||||
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG23_ANALOG_SYS4, 0x00));
|
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG23_ANALOG_SYS4, 0x00));
|
||||||
|
|
||||||
|
// Timing and interface configuration
|
||||||
|
// REG0A/0B TIME CONTROL: Fast state machine transitions
|
||||||
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG0A_TIME_CONTROL1, 0x01));
|
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG0A_TIME_CONTROL1, 0x01));
|
||||||
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG0B_TIME_CONTROL2, 0x01));
|
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG0B_TIME_CONTROL2, 0x01));
|
||||||
|
|
||||||
|
// REG11 SDP INTERFACE CONFIG: Default I2S format (24-bit, I2S mode)
|
||||||
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG11_DAC_SDP, 0x00));
|
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG11_DAC_SDP, 0x00));
|
||||||
|
|
||||||
|
// REG19 EQ CONTROL 1: EQ disabled (EQ_ON=0), EQ_BAND_NUM=2
|
||||||
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG19_EQ_CONTROL1, 0x20));
|
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG19_EQ_CONTROL1, 0x20));
|
||||||
|
|
||||||
|
// REG0D P2S CONTROL: Parallel-to-serial converter settings
|
||||||
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG0D_P2S_CONTROL, 0x14));
|
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG0D_P2S_CONTROL, 0x14));
|
||||||
|
|
||||||
|
// REG09 MISC CONTROL 2: Default settings
|
||||||
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG09_MISC_CONTROL2, 0x00));
|
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG09_MISC_CONTROL2, 0x00));
|
||||||
|
|
||||||
|
// REG18 MISC CONTROL 3: Stereo channel routing, no inversion
|
||||||
|
// Bits 5:4 CHN_CROSS: 0=L→L/R→R, 1=L to both, 2=R to both, 3=swap L/R
|
||||||
|
// Bits 3:2: LCH_INV/RCH_INV channel inversion
|
||||||
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG18_MISC_CONTROL3, 0x00));
|
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG18_MISC_CONTROL3, 0x00));
|
||||||
|
|
||||||
|
// REG08 CLOCK OFF: Enable all internal clocks (0x3F = all clock gates open)
|
||||||
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG08_CLOCK_ON_OFF, 0x3F));
|
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG08_CLOCK_ON_OFF, 0x3F));
|
||||||
|
|
||||||
|
// REG00 RESET CONTROL: Reset sequence
|
||||||
|
// First: RST_DIG=1 (assert digital reset)
|
||||||
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG00_RESET, 0x02));
|
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG00_RESET, 0x02));
|
||||||
|
// Then: CSM_ON=1 (enable chip state machine), RST_DIG=1
|
||||||
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG00_RESET, 0x03));
|
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG00_RESET, 0x03));
|
||||||
|
|
||||||
|
// REG25 ANALOG SYSTEM: Power up analog blocks
|
||||||
|
// VMIDSEL=2 (normal VMID operation), PDN_ANA=0, ENREFR=0, ENHPCOM=0
|
||||||
|
// PDN_DACVREFGEN=0, PDN_VREFBUF=0, PDN_DAC=0 (all enabled)
|
||||||
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG25_ANALOG_SYS5, 0x20));
|
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG25_ANALOG_SYS5, 0x20));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ from esphome.const import (
|
|||||||
KEY_CORE,
|
KEY_CORE,
|
||||||
KEY_FRAMEWORK_VERSION,
|
KEY_FRAMEWORK_VERSION,
|
||||||
KEY_NAME,
|
KEY_NAME,
|
||||||
|
KEY_NATIVE_IDF,
|
||||||
KEY_TARGET_FRAMEWORK,
|
KEY_TARGET_FRAMEWORK,
|
||||||
KEY_TARGET_PLATFORM,
|
KEY_TARGET_PLATFORM,
|
||||||
PLATFORM_ESP32,
|
PLATFORM_ESP32,
|
||||||
@@ -52,7 +53,10 @@ from .const import ( # noqa
|
|||||||
KEY_BOARD,
|
KEY_BOARD,
|
||||||
KEY_COMPONENTS,
|
KEY_COMPONENTS,
|
||||||
KEY_ESP32,
|
KEY_ESP32,
|
||||||
|
KEY_EXCLUDE_COMPONENTS,
|
||||||
KEY_EXTRA_BUILD_FILES,
|
KEY_EXTRA_BUILD_FILES,
|
||||||
|
KEY_FLASH_SIZE,
|
||||||
|
KEY_FULL_CERT_BUNDLE,
|
||||||
KEY_PATH,
|
KEY_PATH,
|
||||||
KEY_REF,
|
KEY_REF,
|
||||||
KEY_REPO,
|
KEY_REPO,
|
||||||
@@ -83,6 +87,7 @@ IS_TARGET_PLATFORM = True
|
|||||||
CONF_ASSERTION_LEVEL = "assertion_level"
|
CONF_ASSERTION_LEVEL = "assertion_level"
|
||||||
CONF_COMPILER_OPTIMIZATION = "compiler_optimization"
|
CONF_COMPILER_OPTIMIZATION = "compiler_optimization"
|
||||||
CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES = "enable_idf_experimental_features"
|
CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES = "enable_idf_experimental_features"
|
||||||
|
CONF_INCLUDE_BUILTIN_IDF_COMPONENTS = "include_builtin_idf_components"
|
||||||
CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert"
|
CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert"
|
||||||
CONF_ENABLE_OTA_ROLLBACK = "enable_ota_rollback"
|
CONF_ENABLE_OTA_ROLLBACK = "enable_ota_rollback"
|
||||||
CONF_EXECUTE_FROM_PSRAM = "execute_from_psram"
|
CONF_EXECUTE_FROM_PSRAM = "execute_from_psram"
|
||||||
@@ -111,6 +116,36 @@ COMPILER_OPTIMIZATIONS = {
|
|||||||
"SIZE": "CONFIG_COMPILER_OPTIMIZATION_SIZE",
|
"SIZE": "CONFIG_COMPILER_OPTIMIZATION_SIZE",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ESP-IDF components excluded by default to reduce compile time.
|
||||||
|
# Components can be re-enabled by calling include_builtin_idf_component() in to_code().
|
||||||
|
#
|
||||||
|
# Cannot be excluded (dependencies of required components):
|
||||||
|
# - "console": espressif/mdns unconditionally depends on it
|
||||||
|
# - "sdmmc": driver -> esp_driver_sdmmc -> sdmmc dependency chain
|
||||||
|
DEFAULT_EXCLUDED_IDF_COMPONENTS = (
|
||||||
|
"cmock", # Unit testing mock framework - ESPHome doesn't use IDF's testing
|
||||||
|
"esp_adc", # ADC driver - only needed by adc component
|
||||||
|
"esp_driver_i2s", # I2S driver - only needed by i2s_audio component
|
||||||
|
"esp_driver_rmt", # RMT driver - only needed by remote_transmitter/receiver, neopixelbus
|
||||||
|
"esp_driver_touch_sens", # Touch sensor driver - only needed by esp32_touch
|
||||||
|
"esp_eth", # Ethernet driver - only needed by ethernet component
|
||||||
|
"esp_hid", # HID host/device support - ESPHome doesn't implement HID functionality
|
||||||
|
"esp_http_client", # HTTP client - only needed by http_request component
|
||||||
|
"esp_https_ota", # ESP-IDF HTTPS OTA - ESPHome has its own OTA implementation
|
||||||
|
"esp_https_server", # HTTPS server - ESPHome has its own web server
|
||||||
|
"esp_lcd", # LCD controller drivers - only needed by display component
|
||||||
|
"esp_local_ctrl", # Local control over HTTPS/BLE - ESPHome has native API
|
||||||
|
"espcoredump", # Core dump support - ESPHome has its own debug component
|
||||||
|
"fatfs", # FAT filesystem - ESPHome doesn't use filesystem storage
|
||||||
|
"mqtt", # ESP-IDF MQTT library - ESPHome has its own MQTT implementation
|
||||||
|
"perfmon", # Xtensa performance monitor - ESPHome has its own debug component
|
||||||
|
"protocomm", # Protocol communication for provisioning - unused by ESPHome
|
||||||
|
"spiffs", # SPIFFS filesystem - ESPHome doesn't use filesystem storage (IDF only)
|
||||||
|
"unity", # Unit testing framework - ESPHome doesn't use IDF's testing
|
||||||
|
"wear_levelling", # Flash wear levelling for fatfs - unused since fatfs unused
|
||||||
|
"wifi_provisioning", # WiFi provisioning - ESPHome uses its own improv implementation
|
||||||
|
)
|
||||||
|
|
||||||
# ESP32 (original) chip revision options
|
# ESP32 (original) chip revision options
|
||||||
# Setting minimum revision to 3.0 or higher:
|
# Setting minimum revision to 3.0 or higher:
|
||||||
# - Reduces flash size by excluding workaround code for older chip bugs
|
# - Reduces flash size by excluding workaround code for older chip bugs
|
||||||
@@ -180,6 +215,12 @@ def set_core_data(config):
|
|||||||
path=[CONF_CPU_FREQUENCY],
|
path=[CONF_CPU_FREQUENCY],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if variant == VARIANT_ESP32P4 and cpu_frequency == "400MHZ":
|
||||||
|
_LOGGER.warning(
|
||||||
|
"400MHz on ESP32-P4 is experimental and may not boot. "
|
||||||
|
"Consider using 360MHz instead. See https://github.com/esphome/esphome/issues/13425"
|
||||||
|
)
|
||||||
|
|
||||||
CORE.data[KEY_ESP32] = {}
|
CORE.data[KEY_ESP32] = {}
|
||||||
CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_ESP32
|
CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_ESP32
|
||||||
conf = config[CONF_FRAMEWORK]
|
conf = config[CONF_FRAMEWORK]
|
||||||
@@ -194,11 +235,15 @@ def set_core_data(config):
|
|||||||
)
|
)
|
||||||
CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] = {}
|
CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] = {}
|
||||||
CORE.data[KEY_ESP32][KEY_COMPONENTS] = {}
|
CORE.data[KEY_ESP32][KEY_COMPONENTS] = {}
|
||||||
|
# Initialize with default exclusions - components can call include_builtin_idf_component()
|
||||||
|
# to re-enable any they need
|
||||||
|
CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS] = set(DEFAULT_EXCLUDED_IDF_COMPONENTS)
|
||||||
CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse(
|
CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse(
|
||||||
config[CONF_FRAMEWORK][CONF_VERSION]
|
config[CONF_FRAMEWORK][CONF_VERSION]
|
||||||
)
|
)
|
||||||
|
|
||||||
CORE.data[KEY_ESP32][KEY_BOARD] = config[CONF_BOARD]
|
CORE.data[KEY_ESP32][KEY_BOARD] = config[CONF_BOARD]
|
||||||
|
CORE.data[KEY_ESP32][KEY_FLASH_SIZE] = config[CONF_FLASH_SIZE]
|
||||||
CORE.data[KEY_ESP32][KEY_VARIANT] = variant
|
CORE.data[KEY_ESP32][KEY_VARIANT] = variant
|
||||||
CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES] = {}
|
CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES] = {}
|
||||||
|
|
||||||
@@ -318,6 +363,28 @@ def add_idf_component(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def exclude_builtin_idf_component(name: str) -> None:
|
||||||
|
"""Exclude an ESP-IDF component from the build.
|
||||||
|
|
||||||
|
This reduces compile time by skipping components that are not needed.
|
||||||
|
The component will be passed to ESP-IDF's EXCLUDE_COMPONENTS cmake variable.
|
||||||
|
|
||||||
|
Note: Components that are dependencies of other required components
|
||||||
|
cannot be excluded - ESP-IDF will still build them.
|
||||||
|
"""
|
||||||
|
CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS].add(name)
|
||||||
|
|
||||||
|
|
||||||
|
def include_builtin_idf_component(name: str) -> None:
|
||||||
|
"""Remove an ESP-IDF component from the exclusion list.
|
||||||
|
|
||||||
|
Call this from components that need an ESP-IDF component that is
|
||||||
|
excluded by default in DEFAULT_EXCLUDED_IDF_COMPONENTS. This ensures the
|
||||||
|
component will be built when needed.
|
||||||
|
"""
|
||||||
|
CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS].discard(name)
|
||||||
|
|
||||||
|
|
||||||
def add_extra_script(stage: str, filename: str, path: Path):
|
def add_extra_script(stage: str, filename: str, path: Path):
|
||||||
"""Add an extra script to the project."""
|
"""Add an extra script to the project."""
|
||||||
key = f"{stage}:{filename}"
|
key = f"{stage}:{filename}"
|
||||||
@@ -339,7 +406,12 @@ def add_extra_build_file(filename: str, path: Path) -> bool:
|
|||||||
def _format_framework_arduino_version(ver: cv.Version) -> str:
|
def _format_framework_arduino_version(ver: cv.Version) -> str:
|
||||||
# format the given arduino (https://github.com/espressif/arduino-esp32/releases) version to
|
# format the given arduino (https://github.com/espressif/arduino-esp32/releases) version to
|
||||||
# a PIO pioarduino/framework-arduinoespressif32 value
|
# a PIO pioarduino/framework-arduinoespressif32 value
|
||||||
return f"pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/{str(ver)}/esp32-{str(ver)}.zip"
|
# 3.3.6+ changed filename from esp32-{ver}.zip to esp32-core-{ver}.tar.xz
|
||||||
|
if ver >= cv.Version(3, 3, 6):
|
||||||
|
filename = f"esp32-core-{ver}.tar.xz"
|
||||||
|
else:
|
||||||
|
filename = f"esp32-{ver}.zip"
|
||||||
|
return f"pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/{ver}/{filename}"
|
||||||
|
|
||||||
|
|
||||||
def _format_framework_espidf_version(ver: cv.Version, release: str) -> str:
|
def _format_framework_espidf_version(ver: cv.Version, release: str) -> str:
|
||||||
@@ -374,11 +446,12 @@ def _is_framework_url(source: str) -> bool:
|
|||||||
# The default/recommended arduino framework version
|
# The default/recommended arduino framework version
|
||||||
# - https://github.com/espressif/arduino-esp32/releases
|
# - https://github.com/espressif/arduino-esp32/releases
|
||||||
ARDUINO_FRAMEWORK_VERSION_LOOKUP = {
|
ARDUINO_FRAMEWORK_VERSION_LOOKUP = {
|
||||||
"recommended": cv.Version(3, 3, 5),
|
"recommended": cv.Version(3, 3, 6),
|
||||||
"latest": cv.Version(3, 3, 5),
|
"latest": cv.Version(3, 3, 6),
|
||||||
"dev": cv.Version(3, 3, 5),
|
"dev": cv.Version(3, 3, 6),
|
||||||
}
|
}
|
||||||
ARDUINO_PLATFORM_VERSION_LOOKUP = {
|
ARDUINO_PLATFORM_VERSION_LOOKUP = {
|
||||||
|
cv.Version(3, 3, 6): cv.Version(55, 3, 36),
|
||||||
cv.Version(3, 3, 5): cv.Version(55, 3, 35),
|
cv.Version(3, 3, 5): cv.Version(55, 3, 35),
|
||||||
cv.Version(3, 3, 4): cv.Version(55, 3, 31, "2"),
|
cv.Version(3, 3, 4): cv.Version(55, 3, 31, "2"),
|
||||||
cv.Version(3, 3, 3): cv.Version(55, 3, 31, "2"),
|
cv.Version(3, 3, 3): cv.Version(55, 3, 31, "2"),
|
||||||
@@ -396,6 +469,7 @@ ARDUINO_PLATFORM_VERSION_LOOKUP = {
|
|||||||
# These versions correspond to pioarduino/esp-idf releases
|
# These versions correspond to pioarduino/esp-idf releases
|
||||||
# See: https://github.com/pioarduino/esp-idf/releases
|
# See: https://github.com/pioarduino/esp-idf/releases
|
||||||
ARDUINO_IDF_VERSION_LOOKUP = {
|
ARDUINO_IDF_VERSION_LOOKUP = {
|
||||||
|
cv.Version(3, 3, 6): cv.Version(5, 5, 2),
|
||||||
cv.Version(3, 3, 5): cv.Version(5, 5, 2),
|
cv.Version(3, 3, 5): cv.Version(5, 5, 2),
|
||||||
cv.Version(3, 3, 4): cv.Version(5, 5, 1),
|
cv.Version(3, 3, 4): cv.Version(5, 5, 1),
|
||||||
cv.Version(3, 3, 3): cv.Version(5, 5, 1),
|
cv.Version(3, 3, 3): cv.Version(5, 5, 1),
|
||||||
@@ -418,7 +492,7 @@ ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
|
|||||||
"dev": cv.Version(5, 5, 2),
|
"dev": cv.Version(5, 5, 2),
|
||||||
}
|
}
|
||||||
ESP_IDF_PLATFORM_VERSION_LOOKUP = {
|
ESP_IDF_PLATFORM_VERSION_LOOKUP = {
|
||||||
cv.Version(5, 5, 2): cv.Version(55, 3, 35),
|
cv.Version(5, 5, 2): cv.Version(55, 3, 36),
|
||||||
cv.Version(5, 5, 1): cv.Version(55, 3, 31, "2"),
|
cv.Version(5, 5, 1): cv.Version(55, 3, 31, "2"),
|
||||||
cv.Version(5, 5, 0): cv.Version(55, 3, 31, "2"),
|
cv.Version(5, 5, 0): cv.Version(55, 3, 31, "2"),
|
||||||
cv.Version(5, 4, 3): cv.Version(55, 3, 32),
|
cv.Version(5, 4, 3): cv.Version(55, 3, 32),
|
||||||
@@ -435,9 +509,9 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = {
|
|||||||
# The platform-espressif32 version
|
# The platform-espressif32 version
|
||||||
# - https://github.com/pioarduino/platform-espressif32/releases
|
# - https://github.com/pioarduino/platform-espressif32/releases
|
||||||
PLATFORM_VERSION_LOOKUP = {
|
PLATFORM_VERSION_LOOKUP = {
|
||||||
"recommended": cv.Version(55, 3, 35),
|
"recommended": cv.Version(55, 3, 36),
|
||||||
"latest": cv.Version(55, 3, 35),
|
"latest": cv.Version(55, 3, 36),
|
||||||
"dev": cv.Version(55, 3, 35),
|
"dev": cv.Version(55, 3, 36),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -654,14 +728,26 @@ CONF_FREERTOS_IN_IRAM = "freertos_in_iram"
|
|||||||
CONF_RINGBUF_IN_IRAM = "ringbuf_in_iram"
|
CONF_RINGBUF_IN_IRAM = "ringbuf_in_iram"
|
||||||
CONF_HEAP_IN_IRAM = "heap_in_iram"
|
CONF_HEAP_IN_IRAM = "heap_in_iram"
|
||||||
CONF_LOOP_TASK_STACK_SIZE = "loop_task_stack_size"
|
CONF_LOOP_TASK_STACK_SIZE = "loop_task_stack_size"
|
||||||
|
CONF_USE_FULL_CERTIFICATE_BUNDLE = "use_full_certificate_bundle"
|
||||||
|
CONF_DISABLE_DEBUG_STUBS = "disable_debug_stubs"
|
||||||
|
CONF_DISABLE_OCD_AWARE = "disable_ocd_aware"
|
||||||
|
CONF_DISABLE_USB_SERIAL_JTAG_SECONDARY = "disable_usb_serial_jtag_secondary"
|
||||||
|
CONF_DISABLE_DEV_NULL_VFS = "disable_dev_null_vfs"
|
||||||
|
CONF_DISABLE_MBEDTLS_PEER_CERT = "disable_mbedtls_peer_cert"
|
||||||
|
CONF_DISABLE_MBEDTLS_PKCS7 = "disable_mbedtls_pkcs7"
|
||||||
|
CONF_DISABLE_REGI2C_IN_IRAM = "disable_regi2c_in_iram"
|
||||||
|
CONF_DISABLE_FATFS = "disable_fatfs"
|
||||||
|
|
||||||
# VFS requirement tracking
|
# VFS requirement tracking
|
||||||
# Components that need VFS features can call require_vfs_select() or require_vfs_dir()
|
# Components that need VFS features can call require_vfs_select() or require_vfs_dir()
|
||||||
KEY_VFS_SELECT_REQUIRED = "vfs_select_required"
|
KEY_VFS_SELECT_REQUIRED = "vfs_select_required"
|
||||||
KEY_VFS_DIR_REQUIRED = "vfs_dir_required"
|
KEY_VFS_DIR_REQUIRED = "vfs_dir_required"
|
||||||
|
# Feature requirement tracking - components can call require_* functions to re-enable
|
||||||
# Ring buffer IRAM requirement tracking
|
# These are stored in CORE.data[KEY_ESP32] dict
|
||||||
KEY_RINGBUF_IN_IRAM = "ringbuf_in_iram"
|
KEY_USB_SERIAL_JTAG_SECONDARY_REQUIRED = "usb_serial_jtag_secondary_required"
|
||||||
|
KEY_MBEDTLS_PEER_CERT_REQUIRED = "mbedtls_peer_cert_required"
|
||||||
|
KEY_MBEDTLS_PKCS7_REQUIRED = "mbedtls_pkcs7_required"
|
||||||
|
KEY_FATFS_REQUIRED = "fatfs_required"
|
||||||
|
|
||||||
|
|
||||||
def require_vfs_select() -> None:
|
def require_vfs_select() -> None:
|
||||||
@@ -682,15 +768,53 @@ def require_vfs_dir() -> None:
|
|||||||
CORE.data[KEY_VFS_DIR_REQUIRED] = True
|
CORE.data[KEY_VFS_DIR_REQUIRED] = True
|
||||||
|
|
||||||
|
|
||||||
def enable_ringbuf_in_iram() -> None:
|
def require_full_certificate_bundle() -> None:
|
||||||
"""Keep ring buffer functions in IRAM instead of moving them to flash.
|
"""Request the full certificate bundle instead of the common-CAs-only bundle.
|
||||||
|
|
||||||
Call this from components that use esphome/core/ring_buffer.cpp and need
|
By default, ESPHome uses CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_CMN which
|
||||||
the ring buffer functions to remain in IRAM for performance reasons
|
includes only CAs with >1% market share (~51 KB smaller than full bundle).
|
||||||
(e.g., voice assistants, audio components).
|
This covers ~99% of websites including Let's Encrypt, DigiCert, Google, Amazon.
|
||||||
This prevents CONFIG_RINGBUF_PLACE_FUNCTIONS_INTO_FLASH from being enabled.
|
|
||||||
|
Call this from components that need to connect to services using uncommon CAs.
|
||||||
"""
|
"""
|
||||||
CORE.data[KEY_RINGBUF_IN_IRAM] = True
|
CORE.data[KEY_ESP32][KEY_FULL_CERT_BUNDLE] = True
|
||||||
|
|
||||||
|
|
||||||
|
def require_usb_serial_jtag_secondary() -> None:
|
||||||
|
"""Mark that USB Serial/JTAG secondary console is required by a component.
|
||||||
|
|
||||||
|
Call this from components (e.g., logger) that need USB Serial/JTAG console output.
|
||||||
|
This prevents CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG from being disabled.
|
||||||
|
"""
|
||||||
|
CORE.data[KEY_ESP32][KEY_USB_SERIAL_JTAG_SECONDARY_REQUIRED] = True
|
||||||
|
|
||||||
|
|
||||||
|
def require_mbedtls_peer_cert() -> None:
|
||||||
|
"""Mark that mbedTLS peer certificate retention is required by a component.
|
||||||
|
|
||||||
|
Call this from components that need access to the peer certificate after
|
||||||
|
the TLS handshake is complete. This prevents CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE
|
||||||
|
from being disabled.
|
||||||
|
"""
|
||||||
|
CORE.data[KEY_ESP32][KEY_MBEDTLS_PEER_CERT_REQUIRED] = True
|
||||||
|
|
||||||
|
|
||||||
|
def require_mbedtls_pkcs7() -> None:
|
||||||
|
"""Mark that mbedTLS PKCS#7 support is required by a component.
|
||||||
|
|
||||||
|
Call this from components that need PKCS#7 certificate validation.
|
||||||
|
This prevents CONFIG_MBEDTLS_PKCS7_C from being disabled.
|
||||||
|
"""
|
||||||
|
CORE.data[KEY_ESP32][KEY_MBEDTLS_PKCS7_REQUIRED] = True
|
||||||
|
|
||||||
|
|
||||||
|
def require_fatfs() -> None:
|
||||||
|
"""Mark that FATFS support is required by a component.
|
||||||
|
|
||||||
|
Call this from components that use FATFS (e.g., SD card, storage components).
|
||||||
|
This prevents FATFS from being disabled when disable_fatfs is set.
|
||||||
|
"""
|
||||||
|
CORE.data[KEY_ESP32][KEY_FATFS_REQUIRED] = True
|
||||||
|
|
||||||
|
|
||||||
def _parse_idf_component(value: str) -> ConfigType:
|
def _parse_idf_component(value: str) -> ConfigType:
|
||||||
@@ -774,6 +898,22 @@ FRAMEWORK_SCHEMA = cv.Schema(
|
|||||||
min=8192, max=32768
|
min=8192, max=32768
|
||||||
),
|
),
|
||||||
cv.Optional(CONF_ENABLE_OTA_ROLLBACK, default=True): cv.boolean,
|
cv.Optional(CONF_ENABLE_OTA_ROLLBACK, default=True): cv.boolean,
|
||||||
|
cv.Optional(
|
||||||
|
CONF_USE_FULL_CERTIFICATE_BUNDLE, default=False
|
||||||
|
): cv.boolean,
|
||||||
|
cv.Optional(
|
||||||
|
CONF_INCLUDE_BUILTIN_IDF_COMPONENTS, default=[]
|
||||||
|
): cv.ensure_list(cv.string_strict),
|
||||||
|
cv.Optional(CONF_DISABLE_DEBUG_STUBS, default=True): cv.boolean,
|
||||||
|
cv.Optional(CONF_DISABLE_OCD_AWARE, default=True): cv.boolean,
|
||||||
|
cv.Optional(
|
||||||
|
CONF_DISABLE_USB_SERIAL_JTAG_SECONDARY, default=True
|
||||||
|
): cv.boolean,
|
||||||
|
cv.Optional(CONF_DISABLE_DEV_NULL_VFS, default=True): cv.boolean,
|
||||||
|
cv.Optional(CONF_DISABLE_MBEDTLS_PEER_CERT, default=True): cv.boolean,
|
||||||
|
cv.Optional(CONF_DISABLE_MBEDTLS_PKCS7, default=True): cv.boolean,
|
||||||
|
cv.Optional(CONF_DISABLE_REGI2C_IN_IRAM, default=True): cv.boolean,
|
||||||
|
cv.Optional(CONF_DISABLE_FATFS, default=True): cv.boolean,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list(
|
cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list(
|
||||||
@@ -963,6 +1103,19 @@ def _configure_lwip_max_sockets(conf: dict) -> None:
|
|||||||
add_idf_sdkconfig_option("CONFIG_LWIP_MAX_SOCKETS", max_sockets)
|
add_idf_sdkconfig_option("CONFIG_LWIP_MAX_SOCKETS", max_sockets)
|
||||||
|
|
||||||
|
|
||||||
|
@coroutine_with_priority(CoroPriority.FINAL)
|
||||||
|
async def _write_exclude_components() -> None:
|
||||||
|
"""Write EXCLUDE_COMPONENTS cmake arg after all components have registered exclusions."""
|
||||||
|
if KEY_ESP32 not in CORE.data:
|
||||||
|
return
|
||||||
|
excluded = CORE.data[KEY_ESP32].get(KEY_EXCLUDE_COMPONENTS)
|
||||||
|
if excluded:
|
||||||
|
exclude_list = ";".join(sorted(excluded))
|
||||||
|
cg.add_platformio_option(
|
||||||
|
"board_build.cmake_extra_args", f"-DEXCLUDE_COMPONENTS={exclude_list}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@coroutine_with_priority(CoroPriority.FINAL)
|
@coroutine_with_priority(CoroPriority.FINAL)
|
||||||
async def _add_yaml_idf_components(components: list[ConfigType]):
|
async def _add_yaml_idf_components(components: list[ConfigType]):
|
||||||
"""Add IDF components from YAML config with final priority to override code-added components."""
|
"""Add IDF components from YAML config with final priority to override code-added components."""
|
||||||
@@ -976,12 +1129,54 @@ async def _add_yaml_idf_components(components: list[ConfigType]):
|
|||||||
|
|
||||||
|
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
cg.add_platformio_option("board", config[CONF_BOARD])
|
framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
|
||||||
cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE])
|
conf = config[CONF_FRAMEWORK]
|
||||||
cg.add_platformio_option(
|
|
||||||
"board_upload.maximum_size",
|
# Check if using native ESP-IDF build (--native-idf)
|
||||||
int(config[CONF_FLASH_SIZE].removesuffix("MB")) * 1024 * 1024,
|
use_platformio = not CORE.data.get(KEY_NATIVE_IDF, False)
|
||||||
)
|
if use_platformio:
|
||||||
|
# Clear IDF environment variables to avoid conflicts with PlatformIO's ESP-IDF
|
||||||
|
# but keep them when using --native-idf for native ESP-IDF builds
|
||||||
|
for clean_var in ("IDF_PATH", "IDF_TOOLS_PATH"):
|
||||||
|
os.environ.pop(clean_var, None)
|
||||||
|
|
||||||
|
cg.add_platformio_option("lib_ldf_mode", "off")
|
||||||
|
cg.add_platformio_option("lib_compat_mode", "strict")
|
||||||
|
cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION])
|
||||||
|
cg.add_platformio_option("board", config[CONF_BOARD])
|
||||||
|
cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE])
|
||||||
|
cg.add_platformio_option(
|
||||||
|
"board_upload.maximum_size",
|
||||||
|
int(config[CONF_FLASH_SIZE].removesuffix("MB")) * 1024 * 1024,
|
||||||
|
)
|
||||||
|
|
||||||
|
if CONF_SOURCE in conf:
|
||||||
|
cg.add_platformio_option("platform_packages", [conf[CONF_SOURCE]])
|
||||||
|
|
||||||
|
add_extra_script(
|
||||||
|
"pre",
|
||||||
|
"pre_build.py",
|
||||||
|
Path(__file__).parent / "pre_build.py.script",
|
||||||
|
)
|
||||||
|
|
||||||
|
add_extra_script(
|
||||||
|
"post",
|
||||||
|
"post_build.py",
|
||||||
|
Path(__file__).parent / "post_build.py.script",
|
||||||
|
)
|
||||||
|
|
||||||
|
# In testing mode, add IRAM fix script to allow linking grouped component tests
|
||||||
|
# Similar to ESP8266's approach but for ESP-IDF
|
||||||
|
if CORE.testing_mode:
|
||||||
|
cg.add_build_flag("-DESPHOME_TESTING_MODE")
|
||||||
|
add_extra_script(
|
||||||
|
"pre",
|
||||||
|
"iram_fix.py",
|
||||||
|
Path(__file__).parent / "iram_fix.py.script",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
cg.add_build_flag("-Wno-error=format")
|
||||||
|
|
||||||
cg.set_cpp_standard("gnu++20")
|
cg.set_cpp_standard("gnu++20")
|
||||||
cg.add_build_flag("-DUSE_ESP32")
|
cg.add_build_flag("-DUSE_ESP32")
|
||||||
cg.add_build_flag("-Wl,-z,noexecstack")
|
cg.add_build_flag("-Wl,-z,noexecstack")
|
||||||
@@ -991,81 +1186,76 @@ async def to_code(config):
|
|||||||
cg.add_define("ESPHOME_VARIANT", VARIANT_FRIENDLY[variant])
|
cg.add_define("ESPHOME_VARIANT", VARIANT_FRIENDLY[variant])
|
||||||
cg.add_define(ThreadModel.MULTI_ATOMICS)
|
cg.add_define(ThreadModel.MULTI_ATOMICS)
|
||||||
|
|
||||||
cg.add_platformio_option("lib_ldf_mode", "off")
|
|
||||||
cg.add_platformio_option("lib_compat_mode", "strict")
|
|
||||||
|
|
||||||
framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
|
|
||||||
|
|
||||||
conf = config[CONF_FRAMEWORK]
|
|
||||||
cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION])
|
|
||||||
if CONF_SOURCE in conf:
|
|
||||||
cg.add_platformio_option("platform_packages", [conf[CONF_SOURCE]])
|
|
||||||
|
|
||||||
if conf[CONF_ADVANCED][CONF_IGNORE_EFUSE_CUSTOM_MAC]:
|
if conf[CONF_ADVANCED][CONF_IGNORE_EFUSE_CUSTOM_MAC]:
|
||||||
cg.add_define("USE_ESP32_IGNORE_EFUSE_CUSTOM_MAC")
|
cg.add_define("USE_ESP32_IGNORE_EFUSE_CUSTOM_MAC")
|
||||||
|
|
||||||
for clean_var in ("IDF_PATH", "IDF_TOOLS_PATH"):
|
|
||||||
os.environ.pop(clean_var, None)
|
|
||||||
|
|
||||||
# Set the location of the IDF component manager cache
|
# Set the location of the IDF component manager cache
|
||||||
os.environ["IDF_COMPONENT_CACHE_PATH"] = str(
|
os.environ["IDF_COMPONENT_CACHE_PATH"] = str(
|
||||||
CORE.relative_internal_path(".espressif")
|
CORE.relative_internal_path(".espressif")
|
||||||
)
|
)
|
||||||
|
|
||||||
add_extra_script(
|
|
||||||
"pre",
|
|
||||||
"pre_build.py",
|
|
||||||
Path(__file__).parent / "pre_build.py.script",
|
|
||||||
)
|
|
||||||
|
|
||||||
add_extra_script(
|
|
||||||
"post",
|
|
||||||
"post_build.py",
|
|
||||||
Path(__file__).parent / "post_build.py.script",
|
|
||||||
)
|
|
||||||
|
|
||||||
# In testing mode, add IRAM fix script to allow linking grouped component tests
|
|
||||||
# Similar to ESP8266's approach but for ESP-IDF
|
|
||||||
if CORE.testing_mode:
|
|
||||||
cg.add_build_flag("-DESPHOME_TESTING_MODE")
|
|
||||||
add_extra_script(
|
|
||||||
"pre",
|
|
||||||
"iram_fix.py",
|
|
||||||
Path(__file__).parent / "iram_fix.py.script",
|
|
||||||
)
|
|
||||||
|
|
||||||
if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
|
if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
|
||||||
cg.add_platformio_option("framework", "espidf")
|
|
||||||
cg.add_build_flag("-DUSE_ESP_IDF")
|
cg.add_build_flag("-DUSE_ESP_IDF")
|
||||||
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF")
|
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF")
|
||||||
|
if use_platformio:
|
||||||
|
cg.add_platformio_option("framework", "espidf")
|
||||||
|
|
||||||
|
# Wrap std::__throw_* functions to abort immediately, eliminating ~3KB of
|
||||||
|
# exception class overhead. See throw_stubs.cpp for implementation.
|
||||||
|
# ESP-IDF already compiles with -fno-exceptions, so this code was dead anyway.
|
||||||
|
for mangled in [
|
||||||
|
"_ZSt20__throw_length_errorPKc",
|
||||||
|
"_ZSt19__throw_logic_errorPKc",
|
||||||
|
"_ZSt20__throw_out_of_rangePKc",
|
||||||
|
"_ZSt24__throw_out_of_range_fmtPKcz",
|
||||||
|
"_ZSt17__throw_bad_allocv",
|
||||||
|
"_ZSt25__throw_bad_function_callv",
|
||||||
|
]:
|
||||||
|
cg.add_build_flag(f"-Wl,--wrap={mangled}")
|
||||||
else:
|
else:
|
||||||
cg.add_platformio_option("framework", "arduino, espidf")
|
|
||||||
cg.add_build_flag("-DUSE_ARDUINO")
|
cg.add_build_flag("-DUSE_ARDUINO")
|
||||||
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO")
|
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO")
|
||||||
|
if use_platformio:
|
||||||
|
cg.add_platformio_option("framework", "arduino, espidf")
|
||||||
|
|
||||||
|
# Add IDF framework source for Arduino builds to ensure it uses the same version as
|
||||||
|
# the ESP-IDF framework
|
||||||
|
if (idf_ver := ARDUINO_IDF_VERSION_LOOKUP.get(framework_ver)) is not None:
|
||||||
|
cg.add_platformio_option(
|
||||||
|
"platform_packages",
|
||||||
|
[_format_framework_espidf_version(idf_ver, None)],
|
||||||
|
)
|
||||||
|
|
||||||
|
# ESP32-S2 Arduino: Disable USB Serial on boot to avoid TinyUSB dependency
|
||||||
|
if get_esp32_variant() == VARIANT_ESP32S2:
|
||||||
|
cg.add_build_unflag("-DARDUINO_USB_CDC_ON_BOOT=1")
|
||||||
|
cg.add_build_unflag("-DARDUINO_USB_CDC_ON_BOOT=0")
|
||||||
|
cg.add_build_flag("-DARDUINO_USB_CDC_ON_BOOT=0")
|
||||||
|
|
||||||
cg.add_define(
|
cg.add_define(
|
||||||
"USE_ARDUINO_VERSION_CODE",
|
"USE_ARDUINO_VERSION_CODE",
|
||||||
cg.RawExpression(
|
cg.RawExpression(
|
||||||
f"VERSION_CODE({framework_ver.major}, {framework_ver.minor}, {framework_ver.patch})"
|
f"VERSION_CODE({framework_ver.major}, {framework_ver.minor}, {framework_ver.patch})"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
add_idf_sdkconfig_option("CONFIG_MBEDTLS_PSK_MODES", True)
|
add_idf_sdkconfig_option("CONFIG_MBEDTLS_PSK_MODES", True)
|
||||||
add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True)
|
add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True)
|
||||||
|
|
||||||
# Add IDF framework source for Arduino builds to ensure it uses the same version as
|
|
||||||
# the ESP-IDF framework
|
|
||||||
if (idf_ver := ARDUINO_IDF_VERSION_LOOKUP.get(framework_ver)) is not None:
|
|
||||||
cg.add_platformio_option(
|
|
||||||
"platform_packages", [_format_framework_espidf_version(idf_ver, None)]
|
|
||||||
)
|
|
||||||
|
|
||||||
# ESP32-S2 Arduino: Disable USB Serial on boot to avoid TinyUSB dependency
|
|
||||||
if get_esp32_variant() == VARIANT_ESP32S2:
|
|
||||||
cg.add_build_unflag("-DARDUINO_USB_CDC_ON_BOOT=1")
|
|
||||||
cg.add_build_unflag("-DARDUINO_USB_CDC_ON_BOOT=0")
|
|
||||||
cg.add_build_flag("-DARDUINO_USB_CDC_ON_BOOT=0")
|
|
||||||
|
|
||||||
cg.add_build_flag("-Wno-nonnull-compare")
|
cg.add_build_flag("-Wno-nonnull-compare")
|
||||||
|
|
||||||
|
# Use CMN (common CAs) bundle by default to save ~51KB flash
|
||||||
|
# CMN covers CAs with >1% market share (~99% of websites)
|
||||||
|
# Components needing uncommon CAs can call require_full_certificate_bundle()
|
||||||
|
use_full_bundle = conf[CONF_ADVANCED].get(
|
||||||
|
CONF_USE_FULL_CERTIFICATE_BUNDLE, False
|
||||||
|
) or CORE.data[KEY_ESP32].get(KEY_FULL_CERT_BUNDLE, False)
|
||||||
|
add_idf_sdkconfig_option(
|
||||||
|
"CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL", use_full_bundle
|
||||||
|
)
|
||||||
|
if not use_full_bundle:
|
||||||
|
add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_CMN", True)
|
||||||
|
|
||||||
add_idf_sdkconfig_option(f"CONFIG_IDF_TARGET_{variant}", True)
|
add_idf_sdkconfig_option(f"CONFIG_IDF_TARGET_{variant}", True)
|
||||||
add_idf_sdkconfig_option(
|
add_idf_sdkconfig_option(
|
||||||
f"CONFIG_ESPTOOLPY_FLASHSIZE_{config[CONF_FLASH_SIZE]}", True
|
f"CONFIG_ESPTOOLPY_FLASHSIZE_{config[CONF_FLASH_SIZE]}", True
|
||||||
@@ -1104,18 +1294,14 @@ async def to_code(config):
|
|||||||
add_idf_sdkconfig_option("CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH", True)
|
add_idf_sdkconfig_option("CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH", True)
|
||||||
|
|
||||||
# Place ring buffer functions into flash instead of IRAM by default
|
# Place ring buffer functions into flash instead of IRAM by default
|
||||||
# This saves IRAM but may impact performance for audio/voice components.
|
# This saves IRAM. In ESP-IDF 6.0 flash placement becomes the default.
|
||||||
# Components that need ring buffer in IRAM call enable_ringbuf_in_iram().
|
# Users can set ringbuf_in_iram: true as an escape hatch if they encounter issues.
|
||||||
# Users can also set ringbuf_in_iram: true to force IRAM placement.
|
if conf[CONF_ADVANCED][CONF_RINGBUF_IN_IRAM]:
|
||||||
# In ESP-IDF 6.0 flash placement becomes the default.
|
# User requests ring buffer in IRAM
|
||||||
if conf[CONF_ADVANCED][CONF_RINGBUF_IN_IRAM] or CORE.data.get(
|
|
||||||
KEY_RINGBUF_IN_IRAM, False
|
|
||||||
):
|
|
||||||
# User config or component requires ring buffer in IRAM for performance
|
|
||||||
# IDF 6.0+: will need CONFIG_RINGBUF_PLACE_ISR_FUNCTIONS_INTO_FLASH=n
|
# IDF 6.0+: will need CONFIG_RINGBUF_PLACE_ISR_FUNCTIONS_INTO_FLASH=n
|
||||||
add_idf_sdkconfig_option("CONFIG_RINGBUF_PLACE_ISR_FUNCTIONS_INTO_FLASH", False)
|
add_idf_sdkconfig_option("CONFIG_RINGBUF_PLACE_ISR_FUNCTIONS_INTO_FLASH", False)
|
||||||
else:
|
else:
|
||||||
# No component needs it - place in flash to save IRAM
|
# Place in flash to save IRAM (default)
|
||||||
add_idf_sdkconfig_option("CONFIG_RINGBUF_PLACE_FUNCTIONS_INTO_FLASH", True)
|
add_idf_sdkconfig_option("CONFIG_RINGBUF_PLACE_FUNCTIONS_INTO_FLASH", True)
|
||||||
|
|
||||||
# Place heap functions into flash to save IRAM (~4-6KB savings)
|
# Place heap functions into flash to save IRAM (~4-6KB savings)
|
||||||
@@ -1143,6 +1329,11 @@ async def to_code(config):
|
|||||||
|
|
||||||
# Apply LWIP optimization settings
|
# Apply LWIP optimization settings
|
||||||
advanced = conf[CONF_ADVANCED]
|
advanced = conf[CONF_ADVANCED]
|
||||||
|
|
||||||
|
# Re-include any IDF components the user explicitly requested
|
||||||
|
for component_name in advanced.get(CONF_INCLUDE_BUILTIN_IDF_COMPONENTS, []):
|
||||||
|
include_builtin_idf_component(component_name)
|
||||||
|
|
||||||
# DHCP server: only disable if explicitly set to false
|
# DHCP server: only disable if explicitly set to false
|
||||||
# WiFi component handles its own optimization when AP mode is not used
|
# WiFi component handles its own optimization when AP mode is not used
|
||||||
# When using Arduino with Ethernet, DHCP server functions must be available
|
# When using Arduino with Ethernet, DHCP server functions must be available
|
||||||
@@ -1214,7 +1405,8 @@ async def to_code(config):
|
|||||||
"CONFIG_VFS_SUPPORT_DIR", not advanced[CONF_DISABLE_VFS_SUPPORT_DIR]
|
"CONFIG_VFS_SUPPORT_DIR", not advanced[CONF_DISABLE_VFS_SUPPORT_DIR]
|
||||||
)
|
)
|
||||||
|
|
||||||
cg.add_platformio_option("board_build.partitions", "partitions.csv")
|
if use_platformio:
|
||||||
|
cg.add_platformio_option("board_build.partitions", "partitions.csv")
|
||||||
if CONF_PARTITIONS in config:
|
if CONF_PARTITIONS in config:
|
||||||
add_extra_build_file(
|
add_extra_build_file(
|
||||||
"partitions.csv", CORE.relative_config_path(config[CONF_PARTITIONS])
|
"partitions.csv", CORE.relative_config_path(config[CONF_PARTITIONS])
|
||||||
@@ -1263,6 +1455,61 @@ async def to_code(config):
|
|||||||
|
|
||||||
add_idf_sdkconfig_option(f"CONFIG_LOG_DEFAULT_LEVEL_{conf[CONF_LOG_LEVEL]}", True)
|
add_idf_sdkconfig_option(f"CONFIG_LOG_DEFAULT_LEVEL_{conf[CONF_LOG_LEVEL]}", True)
|
||||||
|
|
||||||
|
# Disable OpenOCD debug stubs to save code size
|
||||||
|
# These are used for on-chip debugging with OpenOCD/JTAG, rarely needed for ESPHome
|
||||||
|
if advanced[CONF_DISABLE_DEBUG_STUBS]:
|
||||||
|
add_idf_sdkconfig_option("CONFIG_ESP_DEBUG_STUBS_ENABLE", False)
|
||||||
|
|
||||||
|
# Disable OCD-aware exception handlers
|
||||||
|
# When enabled, the panic handler detects JTAG debugger and halts instead of resetting
|
||||||
|
# Most ESPHome users don't use JTAG debugging
|
||||||
|
if advanced[CONF_DISABLE_OCD_AWARE]:
|
||||||
|
add_idf_sdkconfig_option("CONFIG_ESP_DEBUG_OCDAWARE", False)
|
||||||
|
|
||||||
|
# Disable USB Serial/JTAG secondary console
|
||||||
|
# Components like logger can call require_usb_serial_jtag_secondary() to re-enable
|
||||||
|
if CORE.data[KEY_ESP32].get(KEY_USB_SERIAL_JTAG_SECONDARY_REQUIRED, False):
|
||||||
|
add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG", True)
|
||||||
|
elif advanced[CONF_DISABLE_USB_SERIAL_JTAG_SECONDARY]:
|
||||||
|
add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_SECONDARY_NONE", True)
|
||||||
|
|
||||||
|
# Disable /dev/null VFS initialization
|
||||||
|
# ESPHome doesn't typically need /dev/null
|
||||||
|
if advanced[CONF_DISABLE_DEV_NULL_VFS]:
|
||||||
|
add_idf_sdkconfig_option("CONFIG_VFS_INITIALIZE_DEV_NULL", False)
|
||||||
|
|
||||||
|
# Disable keeping peer certificate after TLS handshake
|
||||||
|
# Saves ~4KB heap per connection, but prevents certificate inspection after handshake
|
||||||
|
# Components that need it can call require_mbedtls_peer_cert()
|
||||||
|
if CORE.data[KEY_ESP32].get(KEY_MBEDTLS_PEER_CERT_REQUIRED, False):
|
||||||
|
add_idf_sdkconfig_option("CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE", True)
|
||||||
|
elif advanced[CONF_DISABLE_MBEDTLS_PEER_CERT]:
|
||||||
|
add_idf_sdkconfig_option("CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE", False)
|
||||||
|
|
||||||
|
# Disable PKCS#7 support in mbedTLS
|
||||||
|
# Only needed for specific certificate validation scenarios
|
||||||
|
# Components that need it can call require_mbedtls_pkcs7()
|
||||||
|
if CORE.data[KEY_ESP32].get(KEY_MBEDTLS_PKCS7_REQUIRED, False):
|
||||||
|
# Component called require_mbedtls_pkcs7() - enable regardless of user setting
|
||||||
|
add_idf_sdkconfig_option("CONFIG_MBEDTLS_PKCS7_C", True)
|
||||||
|
elif advanced[CONF_DISABLE_MBEDTLS_PKCS7]:
|
||||||
|
add_idf_sdkconfig_option("CONFIG_MBEDTLS_PKCS7_C", False)
|
||||||
|
|
||||||
|
# Disable regi2c control functions in IRAM
|
||||||
|
# Only needed if using analog peripherals (ADC, DAC, etc.) from ISRs while cache is disabled
|
||||||
|
if advanced[CONF_DISABLE_REGI2C_IN_IRAM]:
|
||||||
|
add_idf_sdkconfig_option("CONFIG_ESP_REGI2C_CTRL_FUNC_IN_IRAM", False)
|
||||||
|
|
||||||
|
# Disable FATFS support
|
||||||
|
# Components that need FATFS (SD card, etc.) can call require_fatfs()
|
||||||
|
if CORE.data[KEY_ESP32].get(KEY_FATFS_REQUIRED, False):
|
||||||
|
# Component called require_fatfs() - enable regardless of user setting
|
||||||
|
add_idf_sdkconfig_option("CONFIG_FATFS_LFN_NONE", False)
|
||||||
|
add_idf_sdkconfig_option("CONFIG_FATFS_VOLUME_COUNT", 2)
|
||||||
|
elif advanced[CONF_DISABLE_FATFS]:
|
||||||
|
add_idf_sdkconfig_option("CONFIG_FATFS_LFN_NONE", True)
|
||||||
|
add_idf_sdkconfig_option("CONFIG_FATFS_VOLUME_COUNT", 0)
|
||||||
|
|
||||||
for name, value in conf[CONF_SDKCONFIG_OPTIONS].items():
|
for name, value in conf[CONF_SDKCONFIG_OPTIONS].items():
|
||||||
add_idf_sdkconfig_option(name, RawSdkconfigValue(value))
|
add_idf_sdkconfig_option(name, RawSdkconfigValue(value))
|
||||||
|
|
||||||
@@ -1271,6 +1518,11 @@ async def to_code(config):
|
|||||||
if conf[CONF_COMPONENTS]:
|
if conf[CONF_COMPONENTS]:
|
||||||
CORE.add_job(_add_yaml_idf_components, conf[CONF_COMPONENTS])
|
CORE.add_job(_add_yaml_idf_components, conf[CONF_COMPONENTS])
|
||||||
|
|
||||||
|
# Write EXCLUDE_COMPONENTS at FINAL priority after all components have had
|
||||||
|
# a chance to call include_builtin_idf_component() to re-enable components they need.
|
||||||
|
# Default exclusions are added in set_core_data() during config validation.
|
||||||
|
CORE.add_job(_write_exclude_components)
|
||||||
|
|
||||||
|
|
||||||
APP_PARTITION_SIZES = {
|
APP_PARTITION_SIZES = {
|
||||||
"2MB": 0x0C0000, # 768 KB
|
"2MB": 0x0C0000, # 768 KB
|
||||||
@@ -1379,19 +1631,16 @@ def copy_files():
|
|||||||
_write_idf_component_yml()
|
_write_idf_component_yml()
|
||||||
|
|
||||||
if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]:
|
if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]:
|
||||||
|
flash_size = CORE.data[KEY_ESP32][KEY_FLASH_SIZE]
|
||||||
if CORE.using_arduino:
|
if CORE.using_arduino:
|
||||||
write_file_if_changed(
|
write_file_if_changed(
|
||||||
CORE.relative_build_path("partitions.csv"),
|
CORE.relative_build_path("partitions.csv"),
|
||||||
get_arduino_partition_csv(
|
get_arduino_partition_csv(flash_size),
|
||||||
CORE.platformio_options.get("board_upload.flash_size")
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
write_file_if_changed(
|
write_file_if_changed(
|
||||||
CORE.relative_build_path("partitions.csv"),
|
CORE.relative_build_path("partitions.csv"),
|
||||||
get_idf_partition_csv(
|
get_idf_partition_csv(flash_size),
|
||||||
CORE.platformio_options.get("board_upload.flash_size")
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
# IDF build scripts look for version string to put in the build.
|
# IDF build scripts look for version string to put in the build.
|
||||||
# However, if the build path does not have an initialized git repo,
|
# However, if the build path does not have an initialized git repo,
|
||||||
|
|||||||
@@ -175,6 +175,32 @@ ESP32_BOARD_PINS = {
|
|||||||
"LED": 13,
|
"LED": 13,
|
||||||
"LED_BUILTIN": 13,
|
"LED_BUILTIN": 13,
|
||||||
},
|
},
|
||||||
|
"adafruit_feather_esp32s3_reversetft": {
|
||||||
|
"BUTTON": 0,
|
||||||
|
"A0": 18,
|
||||||
|
"A1": 17,
|
||||||
|
"A2": 16,
|
||||||
|
"A3": 15,
|
||||||
|
"A4": 14,
|
||||||
|
"A5": 8,
|
||||||
|
"SCK": 36,
|
||||||
|
"MOSI": 35,
|
||||||
|
"MISO": 37,
|
||||||
|
"RX": 38,
|
||||||
|
"TX": 39,
|
||||||
|
"SCL": 4,
|
||||||
|
"SDA": 3,
|
||||||
|
"NEOPIXEL": 33,
|
||||||
|
"PIN_NEOPIXEL": 33,
|
||||||
|
"NEOPIXEL_POWER": 21,
|
||||||
|
"TFT_I2C_POWER": 7,
|
||||||
|
"TFT_CS": 42,
|
||||||
|
"TFT_DC": 40,
|
||||||
|
"TFT_RESET": 41,
|
||||||
|
"TFT_BACKLIGHT": 45,
|
||||||
|
"LED": 13,
|
||||||
|
"LED_BUILTIN": 13,
|
||||||
|
},
|
||||||
"adafruit_feather_esp32s3_tft": {
|
"adafruit_feather_esp32s3_tft": {
|
||||||
"BUTTON": 0,
|
"BUTTON": 0,
|
||||||
"A0": 18,
|
"A0": 18,
|
||||||
|
|||||||
@@ -2,15 +2,18 @@ import esphome.codegen as cg
|
|||||||
|
|
||||||
KEY_ESP32 = "esp32"
|
KEY_ESP32 = "esp32"
|
||||||
KEY_BOARD = "board"
|
KEY_BOARD = "board"
|
||||||
|
KEY_FLASH_SIZE = "flash_size"
|
||||||
KEY_VARIANT = "variant"
|
KEY_VARIANT = "variant"
|
||||||
KEY_SDKCONFIG_OPTIONS = "sdkconfig_options"
|
KEY_SDKCONFIG_OPTIONS = "sdkconfig_options"
|
||||||
KEY_COMPONENTS = "components"
|
KEY_COMPONENTS = "components"
|
||||||
|
KEY_EXCLUDE_COMPONENTS = "exclude_components"
|
||||||
KEY_REPO = "repo"
|
KEY_REPO = "repo"
|
||||||
KEY_REF = "ref"
|
KEY_REF = "ref"
|
||||||
KEY_REFRESH = "refresh"
|
KEY_REFRESH = "refresh"
|
||||||
KEY_PATH = "path"
|
KEY_PATH = "path"
|
||||||
KEY_SUBMODULES = "submodules"
|
KEY_SUBMODULES = "submodules"
|
||||||
KEY_EXTRA_BUILD_FILES = "extra_build_files"
|
KEY_EXTRA_BUILD_FILES = "extra_build_files"
|
||||||
|
KEY_FULL_CERT_BUNDLE = "full_cert_bundle"
|
||||||
|
|
||||||
VARIANT_ESP32 = "ESP32"
|
VARIANT_ESP32 = "ESP32"
|
||||||
VARIANT_ESP32C2 = "ESP32C2"
|
VARIANT_ESP32C2 = "ESP32C2"
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ void ESP32InternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpi
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
gpio_set_intr_type(this->get_pin_num(), idf_type);
|
gpio_set_intr_type(this->get_pin_num(), idf_type);
|
||||||
|
gpio_intr_enable(this->get_pin_num());
|
||||||
if (!isr_service_installed) {
|
if (!isr_service_installed) {
|
||||||
auto res = gpio_install_isr_service(ESP_INTR_FLAG_LEVEL3);
|
auto res = gpio_install_isr_service(ESP_INTR_FLAG_LEVEL3);
|
||||||
if (res != ESP_OK) {
|
if (res != ESP_OK) {
|
||||||
@@ -94,7 +95,6 @@ void ESP32InternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpi
|
|||||||
isr_service_installed = true;
|
isr_service_installed = true;
|
||||||
}
|
}
|
||||||
gpio_isr_handler_add(this->get_pin_num(), func, arg);
|
gpio_isr_handler_add(this->get_pin_num(), func, arg);
|
||||||
gpio_intr_enable(this->get_pin_num());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t ESP32InternalGPIOPin::dump_summary(char *buffer, size_t len) const {
|
size_t ESP32InternalGPIOPin::dump_summary(char *buffer, size_t len) const {
|
||||||
|
|||||||
@@ -19,7 +19,16 @@ static constexpr size_t KEY_BUFFER_SIZE = 12;
|
|||||||
|
|
||||||
struct NVSData {
|
struct NVSData {
|
||||||
uint32_t key;
|
uint32_t key;
|
||||||
SmallInlineBuffer<8> data; // Most prefs fit in 8 bytes (covers fan, cover, select, etc.)
|
std::unique_ptr<uint8_t[]> data;
|
||||||
|
size_t len;
|
||||||
|
|
||||||
|
void set_data(const uint8_t *src, size_t size) {
|
||||||
|
if (!this->data || this->len != size) {
|
||||||
|
this->data = std::make_unique<uint8_t[]>(size);
|
||||||
|
this->len = size;
|
||||||
|
}
|
||||||
|
memcpy(this->data.get(), src, size);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
static std::vector<NVSData> s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
static std::vector<NVSData> s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||||
@@ -32,14 +41,14 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend {
|
|||||||
// try find in pending saves and update that
|
// try find in pending saves and update that
|
||||||
for (auto &obj : s_pending_save) {
|
for (auto &obj : s_pending_save) {
|
||||||
if (obj.key == this->key) {
|
if (obj.key == this->key) {
|
||||||
obj.data.set(data, len);
|
obj.set_data(data, len);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
NVSData save{};
|
NVSData save{};
|
||||||
save.key = this->key;
|
save.key = this->key;
|
||||||
save.data.set(data, len);
|
save.set_data(data, len);
|
||||||
s_pending_save.push_back(std::move(save));
|
s_pending_save.emplace_back(std::move(save));
|
||||||
ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len);
|
ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -47,11 +56,11 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend {
|
|||||||
// try find in pending saves and load from that
|
// try find in pending saves and load from that
|
||||||
for (auto &obj : s_pending_save) {
|
for (auto &obj : s_pending_save) {
|
||||||
if (obj.key == this->key) {
|
if (obj.key == this->key) {
|
||||||
if (obj.data.size() != len) {
|
if (obj.len != len) {
|
||||||
// size mismatch
|
// size mismatch
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
memcpy(data, obj.data.data(), len);
|
memcpy(data, obj.data.get(), len);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,10 +136,10 @@ class ESP32Preferences : public ESPPreferences {
|
|||||||
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
|
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
|
||||||
ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str);
|
ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str);
|
||||||
if (this->is_changed_(this->nvs_handle, save, key_str)) {
|
if (this->is_changed_(this->nvs_handle, save, key_str)) {
|
||||||
esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.data(), save.data.size());
|
esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.get(), save.len);
|
||||||
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size());
|
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.len);
|
||||||
if (err != 0) {
|
if (err != 0) {
|
||||||
ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.data.size(), esp_err_to_name(err));
|
ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.len, esp_err_to_name(err));
|
||||||
failed++;
|
failed++;
|
||||||
last_err = err;
|
last_err = err;
|
||||||
last_key = save.key;
|
last_key = save.key;
|
||||||
@@ -138,7 +147,7 @@ class ESP32Preferences : public ESPPreferences {
|
|||||||
}
|
}
|
||||||
written++;
|
written++;
|
||||||
} else {
|
} else {
|
||||||
ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.data.size());
|
ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.len);
|
||||||
cached++;
|
cached++;
|
||||||
}
|
}
|
||||||
s_pending_save.erase(s_pending_save.begin() + i);
|
s_pending_save.erase(s_pending_save.begin() + i);
|
||||||
@@ -169,16 +178,17 @@ class ESP32Preferences : public ESPPreferences {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Check size first before allocating memory
|
// Check size first before allocating memory
|
||||||
if (actual_len != to_save.data.size()) {
|
if (actual_len != to_save.len) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
auto stored_data = std::make_unique<uint8_t[]>(actual_len);
|
// Most preferences are small, use stack buffer with heap fallback for large ones
|
||||||
|
SmallBufferWithHeapFallback<256> stored_data(actual_len);
|
||||||
err = nvs_get_blob(nvs_handle, key_str, stored_data.get(), &actual_len);
|
err = nvs_get_blob(nvs_handle, key_str, stored_data.get(), &actual_len);
|
||||||
if (err != 0) {
|
if (err != 0) {
|
||||||
ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err));
|
ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return memcmp(to_save.data.data(), stored_data.get(), to_save.data.size()) != 0;
|
return memcmp(to_save.data.get(), stored_data.get(), to_save.len) != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool reset() override {
|
bool reset() override {
|
||||||
|
|||||||
57
esphome/components/esp32/throw_stubs.cpp
Normal file
57
esphome/components/esp32/throw_stubs.cpp
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
* Linker wrap stubs for std::__throw_* functions.
|
||||||
|
*
|
||||||
|
* ESP-IDF compiles with -fno-exceptions, so C++ exceptions always abort.
|
||||||
|
* However, ESP-IDF only wraps low-level functions (__cxa_throw, etc.),
|
||||||
|
* not the std::__throw_* functions that construct exception objects first.
|
||||||
|
* This pulls in ~3KB of dead exception class code that can never run.
|
||||||
|
*
|
||||||
|
* ESP8266 Arduino already solved this: their toolchain rebuilds libstdc++
|
||||||
|
* with throw functions that just call abort(). We achieve the same result
|
||||||
|
* using linker --wrap without requiring toolchain changes.
|
||||||
|
*
|
||||||
|
* These stubs abort immediately with a descriptive message, allowing
|
||||||
|
* the linker to dead-code eliminate the exception class infrastructure.
|
||||||
|
*
|
||||||
|
* Wrapped functions and their callers:
|
||||||
|
* - std::__throw_length_error: std::string::reserve, std::vector::reserve
|
||||||
|
* - std::__throw_logic_error: std::promise, std::packaged_task
|
||||||
|
* - std::__throw_out_of_range: std::string::at, std::vector::at
|
||||||
|
* - std::__throw_out_of_range_fmt: std::bitset::to_ulong
|
||||||
|
* - std::__throw_bad_alloc: operator new
|
||||||
|
* - std::__throw_bad_function_call: std::function::operator()
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifdef USE_ESP_IDF
|
||||||
|
#include "esp_system.h"
|
||||||
|
|
||||||
|
namespace esphome::esp32 {}
|
||||||
|
|
||||||
|
// Linker wraps for std::__throw_* - must be extern "C" at global scope.
|
||||||
|
// Names must be __wrap_ + mangled name for the linker's --wrap option.
|
||||||
|
|
||||||
|
// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
// std::__throw_length_error(char const*) - called when container size exceeds max_size()
|
||||||
|
void __wrap__ZSt20__throw_length_errorPKc(const char *) { esp_system_abort("std::length_error"); }
|
||||||
|
|
||||||
|
// std::__throw_logic_error(char const*) - called for logic errors (e.g., promise already satisfied)
|
||||||
|
void __wrap__ZSt19__throw_logic_errorPKc(const char *) { esp_system_abort("std::logic_error"); }
|
||||||
|
|
||||||
|
// std::__throw_out_of_range(char const*) - called by at() when index is out of bounds
|
||||||
|
void __wrap__ZSt20__throw_out_of_rangePKc(const char *) { esp_system_abort("std::out_of_range"); }
|
||||||
|
|
||||||
|
// std::__throw_out_of_range_fmt(char const*, ...) - called by bitset::to_ulong when value doesn't fit
|
||||||
|
void __wrap__ZSt24__throw_out_of_range_fmtPKcz(const char *, ...) { esp_system_abort("std::out_of_range"); }
|
||||||
|
|
||||||
|
// std::__throw_bad_alloc() - called when operator new fails
|
||||||
|
void __wrap__ZSt17__throw_bad_allocv() { esp_system_abort("std::bad_alloc"); }
|
||||||
|
|
||||||
|
// std::__throw_bad_function_call() - called when invoking empty std::function
|
||||||
|
void __wrap__ZSt25__throw_bad_function_callv() { esp_system_abort("std::bad_function_call"); }
|
||||||
|
|
||||||
|
} // extern "C"
|
||||||
|
// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
|
||||||
|
|
||||||
|
#endif // USE_ESP_IDF
|
||||||
@@ -98,10 +98,6 @@ void ESP32BLE::advertising_set_service_data(const std::vector<uint8_t> &data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ESP32BLE::advertising_set_manufacturer_data(const std::vector<uint8_t> &data) {
|
void ESP32BLE::advertising_set_manufacturer_data(const std::vector<uint8_t> &data) {
|
||||||
this->advertising_set_manufacturer_data(std::span<const uint8_t>(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
void ESP32BLE::advertising_set_manufacturer_data(std::span<const uint8_t> data) {
|
|
||||||
this->advertising_init_();
|
this->advertising_init_();
|
||||||
this->advertising_->set_manufacturer_data(data);
|
this->advertising_->set_manufacturer_data(data);
|
||||||
this->advertising_start();
|
this->advertising_start();
|
||||||
|
|||||||
@@ -118,7 +118,6 @@ class ESP32BLE : public Component {
|
|||||||
void advertising_start();
|
void advertising_start();
|
||||||
void advertising_set_service_data(const std::vector<uint8_t> &data);
|
void advertising_set_service_data(const std::vector<uint8_t> &data);
|
||||||
void advertising_set_manufacturer_data(const std::vector<uint8_t> &data);
|
void advertising_set_manufacturer_data(const std::vector<uint8_t> &data);
|
||||||
void advertising_set_manufacturer_data(std::span<const uint8_t> data);
|
|
||||||
void advertising_set_appearance(uint16_t appearance) { this->appearance_ = appearance; }
|
void advertising_set_appearance(uint16_t appearance) { this->appearance_ = appearance; }
|
||||||
void advertising_set_service_data_and_name(std::span<const uint8_t> data, bool include_name);
|
void advertising_set_service_data_and_name(std::span<const uint8_t> data, bool include_name);
|
||||||
void advertising_add_service_uuid(ESPBTUUID uuid);
|
void advertising_add_service_uuid(ESPBTUUID uuid);
|
||||||
|
|||||||
@@ -59,10 +59,6 @@ void BLEAdvertising::set_service_data(const std::vector<uint8_t> &data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void BLEAdvertising::set_manufacturer_data(const std::vector<uint8_t> &data) {
|
void BLEAdvertising::set_manufacturer_data(const std::vector<uint8_t> &data) {
|
||||||
this->set_manufacturer_data(std::span<const uint8_t>(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
void BLEAdvertising::set_manufacturer_data(std::span<const uint8_t> data) {
|
|
||||||
delete[] this->advertising_data_.p_manufacturer_data;
|
delete[] this->advertising_data_.p_manufacturer_data;
|
||||||
this->advertising_data_.p_manufacturer_data = nullptr;
|
this->advertising_data_.p_manufacturer_data = nullptr;
|
||||||
this->advertising_data_.manufacturer_len = data.size();
|
this->advertising_data_.manufacturer_len = data.size();
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ class BLEAdvertising {
|
|||||||
void set_scan_response(bool scan_response) { this->scan_response_ = scan_response; }
|
void set_scan_response(bool scan_response) { this->scan_response_ = scan_response; }
|
||||||
void set_min_preferred_interval(uint16_t interval) { this->advertising_data_.min_interval = interval; }
|
void set_min_preferred_interval(uint16_t interval) { this->advertising_data_.min_interval = interval; }
|
||||||
void set_manufacturer_data(const std::vector<uint8_t> &data);
|
void set_manufacturer_data(const std::vector<uint8_t> &data);
|
||||||
void set_manufacturer_data(std::span<const uint8_t> data);
|
|
||||||
void set_appearance(uint16_t appearance) { this->advertising_data_.appearance = appearance; }
|
void set_appearance(uint16_t appearance) { this->advertising_data_.appearance = appearance; }
|
||||||
void set_service_data(const std::vector<uint8_t> &data);
|
void set_service_data(const std::vector<uint8_t> &data);
|
||||||
void set_service_data(std::span<const uint8_t> data);
|
void set_service_data(std::span<const uint8_t> data);
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ class ESPBTUUID {
|
|||||||
|
|
||||||
esp_bt_uuid_t get_uuid() const;
|
esp_bt_uuid_t get_uuid() const;
|
||||||
|
|
||||||
|
// Remove before 2026.8.0
|
||||||
|
ESPDEPRECATED("Use to_str() instead. Removed in 2026.8.0", "2026.2.0")
|
||||||
std::string to_string() const;
|
std::string to_string() const;
|
||||||
const char *to_str(std::span<char, UUID_STR_LEN> output) const;
|
const char *to_str(std::span<char, UUID_STR_LEN> output) const;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
#include "esp32_ble_beacon.h"
|
#include "esp32_ble_beacon.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
#include "esphome/core/helpers.h"
|
|
||||||
|
|
||||||
#ifdef USE_ESP32
|
#ifdef USE_ESP32
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ void BLEClientBase::loop() {
|
|||||||
this->set_state(espbt::ClientState::INIT);
|
this->set_state(espbt::ClientState::INIT);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this->state_ == espbt::ClientState::INIT) {
|
if (this->state() == espbt::ClientState::INIT) {
|
||||||
auto ret = esp_ble_gattc_app_register(this->app_id);
|
auto ret = esp_ble_gattc_app_register(this->app_id);
|
||||||
if (ret) {
|
if (ret) {
|
||||||
ESP_LOGE(TAG, "gattc app register failed. app_id=%d code=%d", this->app_id, ret);
|
ESP_LOGE(TAG, "gattc app register failed. app_id=%d code=%d", this->app_id, ret);
|
||||||
@@ -60,7 +60,7 @@ void BLEClientBase::loop() {
|
|||||||
}
|
}
|
||||||
// If idle, we can disable the loop as connect()
|
// If idle, we can disable the loop as connect()
|
||||||
// will enable it again when a connection is needed.
|
// will enable it again when a connection is needed.
|
||||||
else if (this->state_ == espbt::ClientState::IDLE) {
|
else if (this->state() == espbt::ClientState::IDLE) {
|
||||||
this->disable_loop();
|
this->disable_loop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,7 +86,7 @@ bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) {
|
|||||||
return false;
|
return false;
|
||||||
if (this->address_ == 0 || device.address_uint64() != this->address_)
|
if (this->address_ == 0 || device.address_uint64() != this->address_)
|
||||||
return false;
|
return false;
|
||||||
if (this->state_ != espbt::ClientState::IDLE)
|
if (this->state() != espbt::ClientState::IDLE)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
this->log_event_("Found device");
|
this->log_event_("Found device");
|
||||||
@@ -102,10 +102,10 @@ bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) {
|
|||||||
|
|
||||||
void BLEClientBase::connect() {
|
void BLEClientBase::connect() {
|
||||||
// Prevent duplicate connection attempts
|
// Prevent duplicate connection attempts
|
||||||
if (this->state_ == espbt::ClientState::CONNECTING || this->state_ == espbt::ClientState::CONNECTED ||
|
if (this->state() == espbt::ClientState::CONNECTING || this->state() == espbt::ClientState::CONNECTED ||
|
||||||
this->state_ == espbt::ClientState::ESTABLISHED) {
|
this->state() == espbt::ClientState::ESTABLISHED) {
|
||||||
ESP_LOGW(TAG, "[%d] [%s] Connection already in progress, state=%s", this->connection_index_, this->address_str_,
|
ESP_LOGW(TAG, "[%d] [%s] Connection already in progress, state=%s", this->connection_index_, this->address_str_,
|
||||||
espbt::client_state_to_string(this->state_));
|
espbt::client_state_to_string(this->state()));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ESP_LOGI(TAG, "[%d] [%s] 0x%02x Connecting", this->connection_index_, this->address_str_, this->remote_addr_type_);
|
ESP_LOGI(TAG, "[%d] [%s] 0x%02x Connecting", this->connection_index_, this->address_str_, this->remote_addr_type_);
|
||||||
@@ -133,12 +133,12 @@ void BLEClientBase::connect() {
|
|||||||
esp_err_t BLEClientBase::pair() { return esp_ble_set_encryption(this->remote_bda_, ESP_BLE_SEC_ENCRYPT); }
|
esp_err_t BLEClientBase::pair() { return esp_ble_set_encryption(this->remote_bda_, ESP_BLE_SEC_ENCRYPT); }
|
||||||
|
|
||||||
void BLEClientBase::disconnect() {
|
void BLEClientBase::disconnect() {
|
||||||
if (this->state_ == espbt::ClientState::IDLE || this->state_ == espbt::ClientState::DISCONNECTING) {
|
if (this->state() == espbt::ClientState::IDLE || this->state() == espbt::ClientState::DISCONNECTING) {
|
||||||
ESP_LOGI(TAG, "[%d] [%s] Disconnect requested, but already %s", this->connection_index_, this->address_str_,
|
ESP_LOGI(TAG, "[%d] [%s] Disconnect requested, but already %s", this->connection_index_, this->address_str_,
|
||||||
espbt::client_state_to_string(this->state_));
|
espbt::client_state_to_string(this->state()));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this->state_ == espbt::ClientState::CONNECTING || this->conn_id_ == UNSET_CONN_ID) {
|
if (this->state() == espbt::ClientState::CONNECTING || this->conn_id_ == UNSET_CONN_ID) {
|
||||||
ESP_LOGD(TAG, "[%d] [%s] Disconnect before connected, disconnect scheduled", this->connection_index_,
|
ESP_LOGD(TAG, "[%d] [%s] Disconnect before connected, disconnect scheduled", this->connection_index_,
|
||||||
this->address_str_);
|
this->address_str_);
|
||||||
this->want_disconnect_ = true;
|
this->want_disconnect_ = true;
|
||||||
@@ -150,7 +150,7 @@ void BLEClientBase::disconnect() {
|
|||||||
void BLEClientBase::unconditional_disconnect() {
|
void BLEClientBase::unconditional_disconnect() {
|
||||||
// Disconnect without checking the state.
|
// Disconnect without checking the state.
|
||||||
ESP_LOGI(TAG, "[%d] [%s] Disconnecting (conn_id: %d).", this->connection_index_, this->address_str_, this->conn_id_);
|
ESP_LOGI(TAG, "[%d] [%s] Disconnecting (conn_id: %d).", this->connection_index_, this->address_str_, this->conn_id_);
|
||||||
if (this->state_ == espbt::ClientState::DISCONNECTING) {
|
if (this->state() == espbt::ClientState::DISCONNECTING) {
|
||||||
this->log_error_("Already disconnecting");
|
this->log_error_("Already disconnecting");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -170,7 +170,7 @@ void BLEClientBase::unconditional_disconnect() {
|
|||||||
this->log_gattc_warning_("esp_ble_gattc_close", err);
|
this->log_gattc_warning_("esp_ble_gattc_close", err);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this->state_ == espbt::ClientState::DISCOVERED) {
|
if (this->state() == espbt::ClientState::DISCOVERED) {
|
||||||
this->set_address(0);
|
this->set_address(0);
|
||||||
this->set_state(espbt::ClientState::IDLE);
|
this->set_state(espbt::ClientState::IDLE);
|
||||||
} else {
|
} else {
|
||||||
@@ -295,18 +295,18 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
|||||||
// ESP-IDF's BLE stack may send ESP_GATTC_OPEN_EVT after esp_ble_gattc_open() returns an
|
// ESP-IDF's BLE stack may send ESP_GATTC_OPEN_EVT after esp_ble_gattc_open() returns an
|
||||||
// error, if the error occurred at the BTA/GATT layer. This can result in the event
|
// error, if the error occurred at the BTA/GATT layer. This can result in the event
|
||||||
// arriving after we've already transitioned to IDLE state.
|
// arriving after we've already transitioned to IDLE state.
|
||||||
if (this->state_ == espbt::ClientState::IDLE) {
|
if (this->state() == espbt::ClientState::IDLE) {
|
||||||
ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in IDLE state (status=%d), ignoring", this->connection_index_,
|
ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in IDLE state (status=%d), ignoring", this->connection_index_,
|
||||||
this->address_str_, param->open.status);
|
this->address_str_, param->open.status);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this->state_ != espbt::ClientState::CONNECTING) {
|
if (this->state() != espbt::ClientState::CONNECTING) {
|
||||||
// This should not happen but lets log it in case it does
|
// This should not happen but lets log it in case it does
|
||||||
// because it means we have a bad assumption about how the
|
// because it means we have a bad assumption about how the
|
||||||
// ESP BT stack works.
|
// ESP BT stack works.
|
||||||
ESP_LOGE(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in %s state (status=%d)", this->connection_index_,
|
ESP_LOGE(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in %s state (status=%d)", this->connection_index_,
|
||||||
this->address_str_, espbt::client_state_to_string(this->state_), param->open.status);
|
this->address_str_, espbt::client_state_to_string(this->state()), param->open.status);
|
||||||
}
|
}
|
||||||
if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) {
|
if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) {
|
||||||
this->log_gattc_warning_("Connection open", param->open.status);
|
this->log_gattc_warning_("Connection open", param->open.status);
|
||||||
@@ -327,7 +327,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
|||||||
if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
|
if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
|
||||||
// Cached connections already connected with medium parameters, no update needed
|
// Cached connections already connected with medium parameters, no update needed
|
||||||
// only set our state, subclients might have more stuff to do yet.
|
// only set our state, subclients might have more stuff to do yet.
|
||||||
this->state_ = espbt::ClientState::ESTABLISHED;
|
this->set_state_internal_(espbt::ClientState::ESTABLISHED);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// For V3_WITHOUT_CACHE, we already set fast params before connecting
|
// For V3_WITHOUT_CACHE, we already set fast params before connecting
|
||||||
@@ -356,7 +356,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
|||||||
return false;
|
return false;
|
||||||
// Check if we were disconnected while waiting for service discovery
|
// Check if we were disconnected while waiting for service discovery
|
||||||
if (param->disconnect.reason == ESP_GATT_CONN_TERMINATE_PEER_USER &&
|
if (param->disconnect.reason == ESP_GATT_CONN_TERMINATE_PEER_USER &&
|
||||||
this->state_ == espbt::ClientState::CONNECTED) {
|
this->state() == espbt::ClientState::CONNECTED) {
|
||||||
this->log_warning_("Remote closed during discovery");
|
this->log_warning_("Remote closed during discovery");
|
||||||
} else {
|
} else {
|
||||||
ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason 0x%02x", this->connection_index_, this->address_str_,
|
ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason 0x%02x", this->connection_index_, this->address_str_,
|
||||||
@@ -433,7 +433,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
ESP_LOGI(TAG, "[%d] [%s] Service discovery complete", this->connection_index_, this->address_str_);
|
ESP_LOGI(TAG, "[%d] [%s] Service discovery complete", this->connection_index_, this->address_str_);
|
||||||
this->state_ = espbt::ClientState::ESTABLISHED;
|
this->set_state_internal_(espbt::ClientState::ESTABLISHED);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case ESP_GATTC_READ_DESCR_EVT: {
|
case ESP_GATTC_READ_DESCR_EVT: {
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
|
|||||||
void unconditional_disconnect();
|
void unconditional_disconnect();
|
||||||
void release_services();
|
void release_services();
|
||||||
|
|
||||||
bool connected() { return this->state_ == espbt::ClientState::ESTABLISHED; }
|
bool connected() { return this->state() == espbt::ClientState::ESTABLISHED; }
|
||||||
|
|
||||||
void set_auto_connect(bool auto_connect) { this->auto_connect_ = auto_connect; }
|
void set_auto_connect(bool auto_connect) { this->auto_connect_ = auto_connect; }
|
||||||
|
|
||||||
|
|||||||
@@ -15,10 +15,7 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_characteristic_on_w
|
|||||||
Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory)
|
Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory)
|
||||||
new Trigger<std::vector<uint8_t>, uint16_t>();
|
new Trigger<std::vector<uint8_t>, uint16_t>();
|
||||||
characteristic->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) {
|
characteristic->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) {
|
||||||
// Convert span to vector for trigger - copy is necessary because:
|
// Convert span to vector for trigger
|
||||||
// 1. Trigger stores the data for use in automation actions that execute later
|
|
||||||
// 2. The span is only valid during this callback (points to temporary BLE stack data)
|
|
||||||
// 3. User lambdas in automations need persistent data they can access asynchronously
|
|
||||||
on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id);
|
on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id);
|
||||||
});
|
});
|
||||||
return on_write_trigger;
|
return on_write_trigger;
|
||||||
@@ -30,10 +27,7 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_descriptor_on_write
|
|||||||
Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory)
|
Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory)
|
||||||
new Trigger<std::vector<uint8_t>, uint16_t>();
|
new Trigger<std::vector<uint8_t>, uint16_t>();
|
||||||
descriptor->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) {
|
descriptor->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) {
|
||||||
// Convert span to vector for trigger - copy is necessary because:
|
// Convert span to vector for trigger
|
||||||
// 1. Trigger stores the data for use in automation actions that execute later
|
|
||||||
// 2. The span is only valid during this callback (points to temporary BLE stack data)
|
|
||||||
// 3. User lambdas in automations need persistent data they can access asynchronously
|
|
||||||
on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id);
|
on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id);
|
||||||
});
|
});
|
||||||
return on_write_trigger;
|
return on_write_trigger;
|
||||||
|
|||||||
@@ -105,15 +105,13 @@ void ESP32BLETracker::loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check for scan timeout - moved here from scheduler to avoid false reboots
|
// Check for scan timeout - moved here from scheduler to avoid false reboots
|
||||||
// when the loop is blocked
|
// when the loop is blocked. This must run every iteration for safety.
|
||||||
if (this->scanner_state_ == ScannerState::RUNNING) {
|
if (this->scanner_state_ == ScannerState::RUNNING) {
|
||||||
switch (this->scan_timeout_state_) {
|
switch (this->scan_timeout_state_) {
|
||||||
case ScanTimeoutState::MONITORING: {
|
case ScanTimeoutState::MONITORING: {
|
||||||
uint32_t now = App.get_loop_component_start_time();
|
|
||||||
uint32_t timeout_ms = this->scan_duration_ * 2000;
|
|
||||||
// Robust time comparison that handles rollover correctly
|
// Robust time comparison that handles rollover correctly
|
||||||
// This works because unsigned arithmetic wraps around predictably
|
// This works because unsigned arithmetic wraps around predictably
|
||||||
if ((now - this->scan_start_time_) > timeout_ms) {
|
if ((App.get_loop_component_start_time() - this->scan_start_time_) > this->scan_timeout_ms_) {
|
||||||
// First time we've seen the timeout exceeded - wait one more loop iteration
|
// First time we've seen the timeout exceeded - wait one more loop iteration
|
||||||
// This ensures all components have had a chance to process pending events
|
// This ensures all components have had a chance to process pending events
|
||||||
// This is because esp32_ble may not have run yet and called
|
// This is because esp32_ble may not have run yet and called
|
||||||
@@ -128,13 +126,31 @@ void ESP32BLETracker::loop() {
|
|||||||
ESP_LOGE(TAG, "Scan never terminated, rebooting");
|
ESP_LOGE(TAG, "Scan never terminated, rebooting");
|
||||||
App.reboot();
|
App.reboot();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ScanTimeoutState::INACTIVE:
|
case ScanTimeoutState::INACTIVE:
|
||||||
// This case should be unreachable - scanner and timeout states are always synchronized
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fast path: skip expensive client state counting and processing
|
||||||
|
// if no state has changed since last loop iteration.
|
||||||
|
//
|
||||||
|
// How state changes ensure we reach the code below:
|
||||||
|
// - handle_scanner_failure_(): scanner_state_ becomes FAILED via set_scanner_state_(), or
|
||||||
|
// scan_set_param_failed_ requires scanner_state_==RUNNING which can only be reached via
|
||||||
|
// set_scanner_state_(RUNNING) in gap_scan_start_complete_() (scan params are set during
|
||||||
|
// STARTING, not RUNNING, so version is always incremented before this condition is true)
|
||||||
|
// - start_scan_(): scanner_state_ becomes IDLE via set_scanner_state_() in cleanup_scan_state_()
|
||||||
|
// - try_promote_discovered_clients_(): client enters DISCOVERED via set_state(), or
|
||||||
|
// connecting client finishes (state change), or scanner reaches RUNNING/IDLE
|
||||||
|
//
|
||||||
|
// All conditions that affect the logic below are tied to state changes that increment
|
||||||
|
// state_version_, so the fast path is safe.
|
||||||
|
if (this->state_version_ == this->last_processed_version_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->last_processed_version_ = this->state_version_;
|
||||||
|
|
||||||
|
// State changed - do full processing
|
||||||
ClientStateCounts counts = this->count_client_states_();
|
ClientStateCounts counts = this->count_client_states_();
|
||||||
if (counts != this->client_state_counts_) {
|
if (counts != this->client_state_counts_) {
|
||||||
this->client_state_counts_ = counts;
|
this->client_state_counts_ = counts;
|
||||||
@@ -142,6 +158,7 @@ void ESP32BLETracker::loop() {
|
|||||||
this->client_state_counts_.discovered, this->client_state_counts_.disconnecting);
|
this->client_state_counts_.discovered, this->client_state_counts_.disconnecting);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Scanner failure: reached when set_scanner_state_(FAILED) or scan_set_param_failed_ set
|
||||||
if (this->scanner_state_ == ScannerState::FAILED ||
|
if (this->scanner_state_ == ScannerState::FAILED ||
|
||||||
(this->scan_set_param_failed_ && this->scanner_state_ == ScannerState::RUNNING)) {
|
(this->scan_set_param_failed_ && this->scanner_state_ == ScannerState::RUNNING)) {
|
||||||
this->handle_scanner_failure_();
|
this->handle_scanner_failure_();
|
||||||
@@ -160,6 +177,8 @@ void ESP32BLETracker::loop() {
|
|||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// Start scan: reached when scanner_state_ becomes IDLE (via set_scanner_state_()) and
|
||||||
|
// all clients are idle (their state changes increment version when they finish)
|
||||||
if (this->scanner_state_ == ScannerState::IDLE && !counts.connecting && !counts.disconnecting && !counts.discovered) {
|
if (this->scanner_state_ == ScannerState::IDLE && !counts.connecting && !counts.disconnecting && !counts.discovered) {
|
||||||
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
|
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
|
||||||
this->update_coex_preference_(false);
|
this->update_coex_preference_(false);
|
||||||
@@ -168,8 +187,9 @@ void ESP32BLETracker::loop() {
|
|||||||
this->start_scan_(false); // first = false
|
this->start_scan_(false); // first = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If there is a discovered client and no connecting
|
// Promote discovered clients: reached when a client's state becomes DISCOVERED (via set_state()),
|
||||||
// clients, then promote the discovered client to ready to connect.
|
// or when a blocking condition clears (connecting client finishes, scanner reaches RUNNING/IDLE).
|
||||||
|
// All these trigger state_version_ increment, so we'll process and check promotion eligibility.
|
||||||
// We check both RUNNING and IDLE states because:
|
// We check both RUNNING and IDLE states because:
|
||||||
// - RUNNING: gap_scan_event_handler initiates stop_scan_() but promotion can happen immediately
|
// - RUNNING: gap_scan_event_handler initiates stop_scan_() but promotion can happen immediately
|
||||||
// - IDLE: Scanner has already stopped (naturally or by gap_scan_event_handler)
|
// - IDLE: Scanner has already stopped (naturally or by gap_scan_event_handler)
|
||||||
@@ -236,6 +256,7 @@ void ESP32BLETracker::start_scan_(bool first) {
|
|||||||
// Start timeout monitoring in loop() instead of using scheduler
|
// Start timeout monitoring in loop() instead of using scheduler
|
||||||
// This prevents false reboots when the loop is blocked
|
// This prevents false reboots when the loop is blocked
|
||||||
this->scan_start_time_ = App.get_loop_component_start_time();
|
this->scan_start_time_ = App.get_loop_component_start_time();
|
||||||
|
this->scan_timeout_ms_ = this->scan_duration_ * 2000;
|
||||||
this->scan_timeout_state_ = ScanTimeoutState::MONITORING;
|
this->scan_timeout_state_ = ScanTimeoutState::MONITORING;
|
||||||
|
|
||||||
esp_err_t err = esp_ble_gap_set_scan_params(&this->scan_params_);
|
esp_err_t err = esp_ble_gap_set_scan_params(&this->scan_params_);
|
||||||
@@ -253,6 +274,10 @@ void ESP32BLETracker::start_scan_(bool first) {
|
|||||||
void ESP32BLETracker::register_client(ESPBTClient *client) {
|
void ESP32BLETracker::register_client(ESPBTClient *client) {
|
||||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
|
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
|
||||||
client->app_id = ++this->app_id_;
|
client->app_id = ++this->app_id_;
|
||||||
|
// Give client a pointer to our state_version_ so it can notify us of state changes.
|
||||||
|
// This enables loop() fast-path optimization - we skip expensive work when no state changed.
|
||||||
|
// Safe because ESP32BLETracker (singleton) outlives all registered clients.
|
||||||
|
client->set_tracker_state_version(&this->state_version_);
|
||||||
this->clients_.push_back(client);
|
this->clients_.push_back(client);
|
||||||
this->recalculate_advertisement_parser_types();
|
this->recalculate_advertisement_parser_types();
|
||||||
#endif
|
#endif
|
||||||
@@ -382,6 +407,7 @@ void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_i
|
|||||||
|
|
||||||
void ESP32BLETracker::set_scanner_state_(ScannerState state) {
|
void ESP32BLETracker::set_scanner_state_(ScannerState state) {
|
||||||
this->scanner_state_ = state;
|
this->scanner_state_ = state;
|
||||||
|
this->state_version_++;
|
||||||
for (auto *listener : this->scanner_state_listeners_) {
|
for (auto *listener : this->scanner_state_listeners_) {
|
||||||
listener->on_scanner_state(state);
|
listener->on_scanner_state(state);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -216,6 +216,19 @@ enum class ConnectionType : uint8_t {
|
|||||||
V3_WITHOUT_CACHE
|
V3_WITHOUT_CACHE
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Base class for BLE GATT clients that connect to remote devices.
|
||||||
|
///
|
||||||
|
/// State Change Tracking Design:
|
||||||
|
/// -----------------------------
|
||||||
|
/// ESP32BLETracker::loop() needs to know when client states change to avoid
|
||||||
|
/// expensive polling. Rather than checking all clients every iteration (~7000/min),
|
||||||
|
/// we use a version counter owned by ESP32BLETracker that clients increment on
|
||||||
|
/// state changes. The tracker compares versions to skip work when nothing changed.
|
||||||
|
///
|
||||||
|
/// Ownership: ESP32BLETracker owns state_version_. Clients hold a non-owning
|
||||||
|
/// pointer (tracker_state_version_) set during register_client(). Clients
|
||||||
|
/// increment the counter through this pointer when their state changes.
|
||||||
|
/// The pointer may be null if the client is not registered with a tracker.
|
||||||
class ESPBTClient : public ESPBTDeviceListener {
|
class ESPBTClient : public ESPBTDeviceListener {
|
||||||
public:
|
public:
|
||||||
virtual bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
virtual bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||||
@@ -225,26 +238,49 @@ class ESPBTClient : public ESPBTDeviceListener {
|
|||||||
virtual void disconnect() = 0;
|
virtual void disconnect() = 0;
|
||||||
bool disconnect_pending() const { return this->want_disconnect_; }
|
bool disconnect_pending() const { return this->want_disconnect_; }
|
||||||
void cancel_pending_disconnect() { this->want_disconnect_ = false; }
|
void cancel_pending_disconnect() { this->want_disconnect_ = false; }
|
||||||
|
|
||||||
|
/// Set the client state with IDLE handling (clears want_disconnect_).
|
||||||
|
/// Notifies the tracker of state change for loop optimization.
|
||||||
virtual void set_state(ClientState st) {
|
virtual void set_state(ClientState st) {
|
||||||
this->state_ = st;
|
this->set_state_internal_(st);
|
||||||
if (st == ClientState::IDLE) {
|
if (st == ClientState::IDLE) {
|
||||||
this->want_disconnect_ = false;
|
this->want_disconnect_ = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ClientState state() const { return state_; }
|
ClientState state() const { return this->state_; }
|
||||||
|
|
||||||
|
/// Called by ESP32BLETracker::register_client() to enable state change notifications.
|
||||||
|
/// The pointer must remain valid for the lifetime of the client (guaranteed since
|
||||||
|
/// ESP32BLETracker is a singleton that outlives all clients).
|
||||||
|
void set_tracker_state_version(uint8_t *version) { this->tracker_state_version_ = version; }
|
||||||
|
|
||||||
// Memory optimized layout
|
// Memory optimized layout
|
||||||
uint8_t app_id; // App IDs are small integers assigned sequentially
|
uint8_t app_id; // App IDs are small integers assigned sequentially
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
// Group 1: 1-byte types
|
/// Set state without IDLE handling - use for direct state transitions.
|
||||||
ClientState state_{ClientState::INIT};
|
/// Increments the tracker's state version counter to signal that loop()
|
||||||
|
/// should do full processing on the next iteration.
|
||||||
|
void set_state_internal_(ClientState st) {
|
||||||
|
this->state_ = st;
|
||||||
|
// Notify tracker that state changed (tracker_state_version_ is owned by ESP32BLETracker)
|
||||||
|
if (this->tracker_state_version_ != nullptr) {
|
||||||
|
(*this->tracker_state_version_)++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// want_disconnect_ is set to true when a disconnect is requested
|
// want_disconnect_ is set to true when a disconnect is requested
|
||||||
// while the client is connecting. This is used to disconnect the
|
// while the client is connecting. This is used to disconnect the
|
||||||
// client as soon as we get the connection id (conn_id_) from the
|
// client as soon as we get the connection id (conn_id_) from the
|
||||||
// ESP_GATTC_OPEN_EVT event.
|
// ESP_GATTC_OPEN_EVT event.
|
||||||
bool want_disconnect_{false};
|
bool want_disconnect_{false};
|
||||||
// 2 bytes used, 2 bytes padding
|
|
||||||
|
private:
|
||||||
|
ClientState state_{ClientState::INIT};
|
||||||
|
/// Non-owning pointer to ESP32BLETracker::state_version_. When this client's
|
||||||
|
/// state changes, we increment the tracker's counter to signal that loop()
|
||||||
|
/// should perform full processing. Null if client not registered with tracker.
|
||||||
|
uint8_t *tracker_state_version_{nullptr};
|
||||||
};
|
};
|
||||||
|
|
||||||
class ESP32BLETracker : public Component,
|
class ESP32BLETracker : public Component,
|
||||||
@@ -380,6 +416,16 @@ class ESP32BLETracker : public Component,
|
|||||||
// Group 4: 1-byte types (enums, uint8_t, bool)
|
// Group 4: 1-byte types (enums, uint8_t, bool)
|
||||||
uint8_t app_id_{0};
|
uint8_t app_id_{0};
|
||||||
uint8_t scan_start_fail_count_{0};
|
uint8_t scan_start_fail_count_{0};
|
||||||
|
/// Version counter for loop() fast-path optimization. Incremented when:
|
||||||
|
/// - Scanner state changes (via set_scanner_state_())
|
||||||
|
/// - Any registered client's state changes (clients hold pointer to this counter)
|
||||||
|
/// Owned by this class; clients receive non-owning pointer via register_client().
|
||||||
|
/// When loop() sees state_version_ == last_processed_version_, it skips expensive
|
||||||
|
/// client state counting and takes the fast path (just timeout check + return).
|
||||||
|
uint8_t state_version_{0};
|
||||||
|
/// Last state_version_ value when loop() did full processing. Compared against
|
||||||
|
/// state_version_ to detect if any state changed since last iteration.
|
||||||
|
uint8_t last_processed_version_{0};
|
||||||
ScannerState scanner_state_{ScannerState::IDLE};
|
ScannerState scanner_state_{ScannerState::IDLE};
|
||||||
bool scan_continuous_;
|
bool scan_continuous_;
|
||||||
bool scan_active_;
|
bool scan_active_;
|
||||||
@@ -396,6 +442,8 @@ class ESP32BLETracker : public Component,
|
|||||||
EXCEEDED_WAIT, // Timeout exceeded, waiting one loop before reboot
|
EXCEEDED_WAIT, // Timeout exceeded, waiting one loop before reboot
|
||||||
};
|
};
|
||||||
uint32_t scan_start_time_{0};
|
uint32_t scan_start_time_{0};
|
||||||
|
/// Precomputed timeout value: scan_duration_ * 2000
|
||||||
|
uint32_t scan_timeout_ms_{0};
|
||||||
ScanTimeoutState scan_timeout_state_{ScanTimeoutState::INACTIVE};
|
ScanTimeoutState scan_timeout_state_{ScanTimeoutState::INACTIVE};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user