mirror of
https://github.com/esphome/esphome.git
synced 2026-01-31 08:52:07 -07:00
Compare commits
119 Commits
integratio
...
lazy_time_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0cc602979 | ||
|
|
5e3561d60b | ||
|
|
ca9ed369f9 | ||
|
|
4e96b20b46 | ||
|
|
a1a60c44da | ||
|
|
898c8a5836 | ||
|
|
20edd11ca7 | ||
|
|
9a8c71a58b | ||
|
|
1a7435250e | ||
|
|
3c91d72403 | ||
|
|
0a63fc6f05 | ||
|
|
50e739ee8e | ||
|
|
6c84f20491 | ||
|
|
a68506f924 | ||
|
|
a20d42ca0b | ||
|
|
4ec8846198 | ||
|
|
40ea65b1c0 | ||
|
|
f7937ef952 | ||
|
|
d6bf137026 | ||
|
|
ed9a672f44 | ||
|
|
823b5ac1ab | ||
|
|
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 |
@@ -1 +1 @@
|
||||
15dc295268b2dcf75942f42759b3ddec64eba89f75525698eb39c95a7f4b14ce
|
||||
069fa9526c52f7c580a9ec17c7678d12f142221387e9b561c18f95394d4629a3
|
||||
|
||||
2
.github/actions/restore-python/action.yml
vendored
2
.github/actions/restore-python/action.yml
vendored
@@ -22,7 +22,7 @@ runs:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: venv
|
||||
# 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
|
||||
};
|
||||
632
.github/workflows/auto-label-pr.yml
vendored
632
.github/workflows/auto-label-pr.yml
vendored
@@ -36,633 +36,5 @@ jobs:
|
||||
with:
|
||||
github-token: ${{ steps.generate-token.outputs.token }}
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
const script = require('./.github/scripts/auto-label-pr/index.js');
|
||||
await script({ github, context });
|
||||
|
||||
30
.github/workflows/ci.yml
vendored
30
.github/workflows/ci.yml
vendored
@@ -47,7 +47,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: venv
|
||||
# yamllint disable-line rule:line-length
|
||||
@@ -157,7 +157,7 @@ jobs:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
- name: Save Python virtual environment cache
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache/save@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: venv
|
||||
key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
|
||||
@@ -193,7 +193,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Restore components graph cache
|
||||
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: .temp/components_graph.json
|
||||
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
|
||||
- name: Save components graph cache
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache/save@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: .temp/components_graph.json
|
||||
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
|
||||
@@ -245,7 +245,7 @@ jobs:
|
||||
python-version: "3.13"
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: venv
|
||||
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
|
||||
@@ -334,14 +334,14 @@ jobs:
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref != 'refs/heads/dev'
|
||||
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
||||
@@ -413,14 +413,14 @@ jobs:
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref != 'refs/heads/dev'
|
||||
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
@@ -502,14 +502,14 @@ jobs:
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref != 'refs/heads/dev'
|
||||
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
@@ -735,7 +735,7 @@ jobs:
|
||||
- name: Restore cached memory analysis
|
||||
id: cache-memory-analysis
|
||||
if: steps.check-script.outputs.skip != 'true'
|
||||
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: memory-analysis-target.json
|
||||
key: ${{ steps.cache-key.outputs.cache-key }}
|
||||
@@ -759,7 +759,7 @@ jobs:
|
||||
|
||||
- name: Cache platformio
|
||||
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:
|
||||
path: ~/.platformio
|
||||
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
|
||||
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:
|
||||
path: memory-analysis-target.json
|
||||
key: ${{ steps.cache-key.outputs.cache-key }}
|
||||
@@ -847,7 +847,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Cache platformio
|
||||
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -58,7 +58,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
|
||||
uses: github/codeql-action/init@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
@@ -86,6 +86,6 @@ jobs:
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
|
||||
uses: github/codeql-action/analyze@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@@ -102,12 +102,12 @@ jobs:
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
|
||||
- name: Log in to docker hub
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- 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:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -182,13 +182,13 @@ jobs:
|
||||
|
||||
- name: Log in to docker hub
|
||||
if: matrix.registry == 'dockerhub'
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Log in to the GitHub container registry
|
||||
if: matrix.registry == 'ghcr'
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
||||
@@ -11,7 +11,7 @@ ci:
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.14.13
|
||||
rev: v0.14.14
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -88,7 +88,8 @@ esphome/components/bmp3xx/* @latonita
|
||||
esphome/components/bmp3xx_base/* @latonita @martgras
|
||||
esphome/components/bmp3xx_i2c/* @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/bp5758d/* @Cossid
|
||||
esphome/components/bthome_mithermometer/* @nagyrobi
|
||||
@@ -103,6 +104,7 @@ esphome/components/cc1101/* @gabest11 @lygris
|
||||
esphome/components/ccs811/* @habbie
|
||||
esphome/components/cd74hc4067/* @asoehlke
|
||||
esphome/components/ch422g/* @clydebarrow @jesterret
|
||||
esphome/components/ch423/* @dwmw2
|
||||
esphome/components/chsc6x/* @kkosik20
|
||||
esphome/components/climate/* @esphome/core
|
||||
esphome/components/climate_ir/* @glmnet
|
||||
@@ -481,6 +483,7 @@ esphome/components/switch/* @esphome/core
|
||||
esphome/components/switch/binary_sensor/* @ssieb
|
||||
esphome/components/sx126x/* @swoboda1337
|
||||
esphome/components/sx127x/* @swoboda1337
|
||||
esphome/components/sy6970/* @linkedupbits
|
||||
esphome/components/syslog/* @clydebarrow
|
||||
esphome/components/t6615/* @tylermenezes
|
||||
esphome/components/tc74/* @sethgirvan
|
||||
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable
|
||||
import json
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
@@ -439,28 +438,6 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
|
||||
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:
|
||||
"""Dump uncategorized symbols for analysis."""
|
||||
# Sort by size descending
|
||||
|
||||
@@ -2,7 +2,7 @@ import logging
|
||||
|
||||
import esphome.codegen as cg
|
||||
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.zephyr import (
|
||||
zephyr_add_overlay,
|
||||
@@ -118,6 +118,9 @@ async def to_code(config):
|
||||
cg.add(var.set_sampling_mode(config[CONF_SAMPLING_MODE]))
|
||||
|
||||
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 == "auto":
|
||||
cg.add(var.set_autorange(cg.global_ns.true))
|
||||
|
||||
@@ -38,8 +38,10 @@ async def to_code(config):
|
||||
# https://github.com/ESP32Async/ESPAsyncTCP
|
||||
cg.add_library("ESP32Async/ESPAsyncTCP", "2.0.0")
|
||||
elif CORE.is_rp2040:
|
||||
# https://github.com/khoih-prog/AsyncTCP_RP2040W
|
||||
cg.add_library("khoih-prog/AsyncTCP_RP2040W", "1.2.0")
|
||||
# https://github.com/ayushsharma82/RPAsyncTCP
|
||||
# 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
|
||||
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
// Use ESPAsyncTCP library for ESP8266 (always Arduino)
|
||||
#include <ESPAsyncTCP.h>
|
||||
#elif defined(USE_RP2040)
|
||||
// Use AsyncTCP_RP2040W library for RP2040
|
||||
#include <AsyncTCP_RP2040W.h>
|
||||
// Use RPAsyncTCP library for RP2040
|
||||
#include <RPAsyncTCP.h>
|
||||
#else
|
||||
// Use socket-based implementation for other platforms
|
||||
#include "async_tcp_socket.h"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.esp32 import add_idf_component
|
||||
from esphome.components.esp32 import add_idf_component, include_builtin_idf_component
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_BITS_PER_SAMPLE, CONF_NUM_CHANNELS, CONF_SAMPLE_RATE
|
||||
import esphome.final_validate as fv
|
||||
@@ -166,6 +166,9 @@ def final_validate_audio_schema(
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
# 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",
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include "bedjet_hub.h"
|
||||
#include "bedjet_child.h"
|
||||
#include "bedjet_const.h"
|
||||
#include "esphome/components/esp32_ble/ble_uuid.h"
|
||||
#include "esphome/core/application.h"
|
||||
#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());
|
||||
|
||||
if (!obj->get_device_class_ref().empty()) {
|
||||
ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj->get_device_class_ref().c_str());
|
||||
}
|
||||
LOG_ENTITY_DEVICE_CLASS(tag, prefix, *obj);
|
||||
}
|
||||
|
||||
void BinarySensor::publish_state(bool new_state) {
|
||||
|
||||
@@ -9,7 +9,7 @@ static const char *const TAG = "bl0940.number";
|
||||
void CalibrationNumber::setup() {
|
||||
float value = 0.0f;
|
||||
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)) {
|
||||
value = 0.0f;
|
||||
}
|
||||
|
||||
@@ -1,164 +1,5 @@
|
||||
import math
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, 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,
|
||||
|
||||
CONFIG_SCHEMA = cv.invalid(
|
||||
"The bmp581 sensor component has been renamed to bmp581_i2c."
|
||||
)
|
||||
|
||||
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)
|
||||
*/
|
||||
|
||||
#include "bmp581.h"
|
||||
#include "bmp581_base.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace bmp581 {
|
||||
namespace esphome::bmp581_base {
|
||||
|
||||
static const char *const TAG = "bmp581";
|
||||
|
||||
@@ -91,7 +90,6 @@ void BMP581Component::dump_config() {
|
||||
break;
|
||||
}
|
||||
|
||||
LOG_I2C_DEVICE(this);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
|
||||
ESP_LOGCONFIG(TAG, " Measurement conversion time: %ums", this->conversion_time_);
|
||||
@@ -149,7 +147,7 @@ void BMP581Component::setup() {
|
||||
uint8_t chip_id;
|
||||
|
||||
// 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");
|
||||
|
||||
this->error_code_ = ERROR_COMMUNICATION_FAILED;
|
||||
@@ -172,7 +170,7 @@ void BMP581Component::setup() {
|
||||
// 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");
|
||||
|
||||
this->error_code_ = ERROR_COMMUNICATION_FAILED;
|
||||
@@ -359,7 +357,7 @@ bool BMP581Component::check_data_readiness_() {
|
||||
|
||||
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");
|
||||
return false;
|
||||
}
|
||||
@@ -400,7 +398,7 @@ bool BMP581Component::prime_iir_filter_() {
|
||||
|
||||
// flush the IIR filter with forced measurements (we will only flush once)
|
||||
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");
|
||||
|
||||
return false;
|
||||
@@ -430,7 +428,7 @@ bool BMP581Component::prime_iir_filter_() {
|
||||
|
||||
// disable IIR filter flushings on future forced measurements
|
||||
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");
|
||||
|
||||
return false;
|
||||
@@ -454,7 +452,7 @@ bool BMP581Component::read_temperature_(float &temperature) {
|
||||
}
|
||||
|
||||
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");
|
||||
this->status_set_warning();
|
||||
|
||||
@@ -483,7 +481,7 @@ bool BMP581Component::read_temperature_and_pressure_(float &temperature, float &
|
||||
}
|
||||
|
||||
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");
|
||||
this->status_set_warning();
|
||||
|
||||
@@ -507,7 +505,7 @@ bool BMP581Component::reset_() {
|
||||
// - returns the Power-On-Reboot interrupt status, which is asserted if successful
|
||||
|
||||
// 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");
|
||||
|
||||
return false;
|
||||
@@ -518,7 +516,7 @@ bool BMP581Component::reset_() {
|
||||
delay(3);
|
||||
|
||||
// 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");
|
||||
|
||||
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
|
||||
// - 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};
|
||||
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) {
|
||||
@@ -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;
|
||||
|
||||
// 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,
|
||||
@@ -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_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) {
|
||||
@@ -593,8 +591,7 @@ bool BMP581Component::write_power_mode_(OperationMode mode) {
|
||||
this->odr_config_.bit.pwr_mode = mode;
|
||||
|
||||
// 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
|
||||
} // namespace esphome::bmp581_base
|
||||
@@ -3,11 +3,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace bmp581 {
|
||||
namespace esphome::bmp581_base {
|
||||
|
||||
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
|
||||
@@ -59,7 +57,7 @@ enum IIRFilter {
|
||||
IIR_FILTER_128 = 0x7
|
||||
};
|
||||
|
||||
class BMP581Component : public PollingComponent, public i2c::I2CDevice {
|
||||
class BMP581Component : public PollingComponent {
|
||||
public:
|
||||
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; }
|
||||
|
||||
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 *pressure_sensor_{nullptr};
|
||||
|
||||
@@ -216,5 +219,4 @@ class BMP581Component : public PollingComponent, public i2c::I2CDevice {
|
||||
} odr_config_ = {.reg = 0};
|
||||
};
|
||||
|
||||
} // namespace bmp581
|
||||
} // namespace esphome
|
||||
} // namespace esphome::bmp581_base
|
||||
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)
|
||||
@@ -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());
|
||||
|
||||
if (!obj->get_icon_ref().empty()) {
|
||||
ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj->get_icon_ref().c_str());
|
||||
}
|
||||
LOG_ENTITY_ICON(tag, prefix, *obj);
|
||||
}
|
||||
|
||||
void Button::press() {
|
||||
|
||||
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;
|
||||
|
||||
optional<ClimateDeviceRestoreState> Climate::restore_state_() {
|
||||
this->rtc_ = global_preferences->make_preference<ClimateDeviceRestoreState>(this->get_preference_hash() ^
|
||||
RESTORE_STATE_VERSION);
|
||||
this->rtc_ = this->make_entity_preference<ClimateDeviceRestoreState>(RESTORE_STATE_VERSION);
|
||||
ClimateDeviceRestoreState recovered{};
|
||||
if (!this->rtc_.load(&recovered))
|
||||
return {};
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#include "cover.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/controller_registry.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
#include <strings.h>
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::cover {
|
||||
|
||||
static const char *const TAG = "cover";
|
||||
@@ -39,13 +39,13 @@ Cover::Cover() : position{COVER_OPEN} {}
|
||||
|
||||
CoverCall::CoverCall(Cover *parent) : parent_(parent) {}
|
||||
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();
|
||||
} else if (strcasecmp(command, "CLOSE") == 0) {
|
||||
} else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("CLOSE")) == 0) {
|
||||
this->set_command_close();
|
||||
} else if (strcasecmp(command, "STOP") == 0) {
|
||||
} else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("STOP")) == 0) {
|
||||
this->set_command_stop();
|
||||
} else if (strcasecmp(command, "TOGGLE") == 0) {
|
||||
} else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("TOGGLE")) == 0) {
|
||||
this->set_command_toggle();
|
||||
} else {
|
||||
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_() {
|
||||
this->rtc_ = global_preferences->make_preference<CoverRestoreState>(this->get_preference_hash());
|
||||
this->rtc_ = this->make_entity_preference<CoverRestoreState>();
|
||||
CoverRestoreState recovered{};
|
||||
if (!this->rtc_.load(&recovered))
|
||||
return {};
|
||||
|
||||
@@ -20,9 +20,7 @@ const extern float COVER_CLOSED;
|
||||
if (traits_.get_is_assumed_state()) { \
|
||||
ESP_LOGCONFIG(TAG, "%s Assumed State: YES", prefix); \
|
||||
} \
|
||||
if (!(obj)->get_device_class_ref().empty()) { \
|
||||
ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class_ref().c_str()); \
|
||||
} \
|
||||
LOG_ENTITY_DEVICE_CLASS(TAG, prefix, *(obj)); \
|
||||
}
|
||||
|
||||
class Cover;
|
||||
|
||||
@@ -15,9 +15,7 @@ namespace esphome::datetime {
|
||||
#define LOG_DATETIME_DATE(prefix, type, obj) \
|
||||
if ((obj) != nullptr) { \
|
||||
ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \
|
||||
if (!(obj)->get_icon_ref().empty()) { \
|
||||
ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \
|
||||
} \
|
||||
LOG_ENTITY_ICON(TAG, prefix, *(obj)); \
|
||||
}
|
||||
|
||||
class DateCall;
|
||||
|
||||
@@ -15,9 +15,7 @@ namespace esphome::datetime {
|
||||
#define LOG_DATETIME_DATETIME(prefix, type, obj) \
|
||||
if ((obj) != nullptr) { \
|
||||
ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \
|
||||
if (!(obj)->get_icon_ref().empty()) { \
|
||||
ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \
|
||||
} \
|
||||
LOG_ENTITY_ICON(TAG, prefix, *(obj)); \
|
||||
}
|
||||
|
||||
class DateTimeCall;
|
||||
|
||||
@@ -15,9 +15,7 @@ namespace esphome::datetime {
|
||||
#define LOG_DATETIME_TIME(prefix, type, obj) \
|
||||
if ((obj) != nullptr) { \
|
||||
ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \
|
||||
if (!(obj)->get_icon_ref().empty()) { \
|
||||
ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \
|
||||
} \
|
||||
LOG_ENTITY_ICON(TAG, prefix, *(obj)); \
|
||||
}
|
||||
|
||||
class TimeCall;
|
||||
|
||||
@@ -15,7 +15,7 @@ from esphome.const import (
|
||||
CONF_UPDATE_INTERVAL,
|
||||
SCHEDULER_DONT_RUN,
|
||||
)
|
||||
from esphome.core import CoroPriority, coroutine_with_priority
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
|
||||
@@ -63,13 +63,11 @@ def validate_auto_clear(value):
|
||||
return cv.boolean(value)
|
||||
|
||||
|
||||
def basic_display_schema(default_update_interval: str = "1s") -> cv.Schema:
|
||||
"""Create a basic display schema with configurable default update interval."""
|
||||
return cv.Schema(
|
||||
{
|
||||
cv.Exclusive(CONF_LAMBDA, CONF_LAMBDA): cv.lambda_,
|
||||
}
|
||||
).extend(cv.polling_component_schema(default_update_interval))
|
||||
BASIC_DISPLAY_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Exclusive(CONF_LAMBDA, CONF_LAMBDA): cv.lambda_,
|
||||
}
|
||||
).extend(cv.polling_component_schema("1s"))
|
||||
|
||||
|
||||
def _validate_test_card(config):
|
||||
@@ -83,41 +81,34 @@ def _validate_test_card(config):
|
||||
return config
|
||||
|
||||
|
||||
def full_display_schema(default_update_interval: str = "1s") -> cv.Schema:
|
||||
"""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.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(
|
||||
FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_ROTATION): validate_rotation,
|
||||
cv.Exclusive(CONF_PAGES, CONF_LAMBDA): cv.All(
|
||||
cv.ensure_list(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
DisplayOnPageChangeTrigger
|
||||
),
|
||||
cv.Optional(CONF_FROM): cv.use_id(DisplayPage),
|
||||
cv.Optional(CONF_TO): cv.use_id(DisplayPage),
|
||||
cv.GenerateID(): cv.declare_id(DisplayPage),
|
||||
cv.Required(CONF_LAMBDA): cv.lambda_,
|
||||
}
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_AUTO_CLEAR_ENABLED, default=CONF_UNSPECIFIED
|
||||
): validate_auto_clear,
|
||||
cv.Optional(CONF_SHOW_TEST_CARD): cv.boolean,
|
||||
}
|
||||
)
|
||||
schema.add_extra(_validate_test_card)
|
||||
return schema
|
||||
|
||||
|
||||
BASIC_DISPLAY_SCHEMA = basic_display_schema("1s")
|
||||
FULL_DISPLAY_SCHEMA = full_display_schema("1s")
|
||||
cv.Length(min=1),
|
||||
),
|
||||
cv.Optional(CONF_ON_PAGE_CHANGE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
DisplayOnPageChangeTrigger
|
||||
),
|
||||
cv.Optional(CONF_FROM): cv.use_id(DisplayPage),
|
||||
cv.Optional(CONF_TO): cv.use_id(DisplayPage),
|
||||
}
|
||||
),
|
||||
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):
|
||||
@@ -231,3 +222,8 @@ async def display_is_displaying_page_to_code(config, condition_id, template_arg,
|
||||
async def to_code(config):
|
||||
cg.add_global(display_ns.using)
|
||||
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")
|
||||
|
||||
@@ -41,7 +41,7 @@ void DutyTimeSensor::setup() {
|
||||
uint32_t seconds = 0;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@ from esphome.const import (
|
||||
CONF_TRANSFORM,
|
||||
CONF_UPDATE_INTERVAL,
|
||||
CONF_WIDTH,
|
||||
SCHEDULER_DONT_RUN,
|
||||
)
|
||||
from esphome.cpp_generator import RawExpression
|
||||
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):
|
||||
model = MODELS[config[CONF_MODEL]]
|
||||
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
|
||||
return (
|
||||
display.full_display_schema("60s")
|
||||
.extend(
|
||||
display.FULL_DISPLAY_SCHEMA.extend(
|
||||
spi.spi_device_schema(
|
||||
cs_pin_required=False,
|
||||
default_mode="MODE0",
|
||||
@@ -93,6 +94,9 @@ def model_schema(config):
|
||||
{
|
||||
cv.Optional(CONF_ROTATION, default=0): validate_rotation,
|
||||
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.Required(CONF_MIRROR_X): cv.boolean,
|
||||
@@ -146,22 +150,15 @@ def _final_validate(config):
|
||||
global_config = full_config.get()
|
||||
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
|
||||
and LVGL_DOMAIN not in global_config
|
||||
):
|
||||
config[CONF_SHOW_TEST_CARD] = True
|
||||
|
||||
interval = config[CONF_UPDATE_INTERVAL]
|
||||
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}"
|
||||
)
|
||||
if CONF_LAMBDA not in config and CONF_PAGES not in config:
|
||||
if LVGL_DOMAIN in global_config:
|
||||
if CONF_UPDATE_INTERVAL not in config:
|
||||
config[CONF_UPDATE_INTERVAL] = update_interval("never")
|
||||
else:
|
||||
# If no drawing methods are configured, and LVGL is not enabled, show a test card
|
||||
config[CONF_SHOW_TEST_CARD] = True
|
||||
elif CONF_UPDATE_INTERVAL not in config:
|
||||
config[CONF_UPDATE_INTERVAL] = update_interval("1min")
|
||||
return config
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@ import esphome.codegen as cg
|
||||
from esphome.components import i2c
|
||||
from esphome.components.audio_dac import AudioDac
|
||||
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"]
|
||||
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):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
@@ -17,24 +17,61 @@ static const char *const TAG = "es8156";
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
// 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));
|
||||
|
||||
// REG21 ANALOG SYSTEM: VSEL=0x1C (bias level ~120%), normal VREF ramp speed
|
||||
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));
|
||||
|
||||
// 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));
|
||||
|
||||
// REG23 ANALOG SYSTEM: Lowest bias (IBIAS_SW=0), VMIDLVL=VDDA/2, normal impedance
|
||||
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_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));
|
||||
|
||||
// REG19 EQ CONTROL 1: EQ disabled (EQ_ON=0), EQ_BAND_NUM=2
|
||||
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));
|
||||
|
||||
// REG09 MISC CONTROL 2: Default settings
|
||||
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));
|
||||
|
||||
// REG08 CLOCK OFF: Enable all internal clocks (0x3F = all clock gates open)
|
||||
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));
|
||||
// Then: CSM_ON=1 (enable chip state machine), RST_DIG=1
|
||||
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));
|
||||
}
|
||||
|
||||
|
||||
@@ -53,8 +53,10 @@ from .const import ( # noqa
|
||||
KEY_BOARD,
|
||||
KEY_COMPONENTS,
|
||||
KEY_ESP32,
|
||||
KEY_EXCLUDE_COMPONENTS,
|
||||
KEY_EXTRA_BUILD_FILES,
|
||||
KEY_FLASH_SIZE,
|
||||
KEY_FULL_CERT_BUNDLE,
|
||||
KEY_PATH,
|
||||
KEY_REF,
|
||||
KEY_REPO,
|
||||
@@ -85,6 +87,7 @@ IS_TARGET_PLATFORM = True
|
||||
CONF_ASSERTION_LEVEL = "assertion_level"
|
||||
CONF_COMPILER_OPTIMIZATION = "compiler_optimization"
|
||||
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_OTA_ROLLBACK = "enable_ota_rollback"
|
||||
CONF_EXECUTE_FROM_PSRAM = "execute_from_psram"
|
||||
@@ -113,6 +116,36 @@ COMPILER_OPTIMIZATIONS = {
|
||||
"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
|
||||
# Setting minimum revision to 3.0 or higher:
|
||||
# - Reduces flash size by excluding workaround code for older chip bugs
|
||||
@@ -202,6 +235,9 @@ def set_core_data(config):
|
||||
)
|
||||
CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] = {}
|
||||
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(
|
||||
config[CONF_FRAMEWORK][CONF_VERSION]
|
||||
)
|
||||
@@ -327,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):
|
||||
"""Add an extra script to the project."""
|
||||
key = f"{stage}:{filename}"
|
||||
@@ -670,14 +728,27 @@ CONF_FREERTOS_IN_IRAM = "freertos_in_iram"
|
||||
CONF_RINGBUF_IN_IRAM = "ringbuf_in_iram"
|
||||
CONF_HEAP_IN_IRAM = "heap_in_iram"
|
||||
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
|
||||
# Components that need VFS features can call require_vfs_select() or require_vfs_dir()
|
||||
# Components that need VFS features can call require_vfs_*() functions
|
||||
KEY_VFS_SELECT_REQUIRED = "vfs_select_required"
|
||||
KEY_VFS_DIR_REQUIRED = "vfs_dir_required"
|
||||
|
||||
# Ring buffer IRAM requirement tracking
|
||||
KEY_RINGBUF_IN_IRAM = "ringbuf_in_iram"
|
||||
KEY_VFS_TERMIOS_REQUIRED = "vfs_termios_required"
|
||||
# Feature requirement tracking - components can call require_* functions to re-enable
|
||||
# These are stored in CORE.data[KEY_ESP32] dict
|
||||
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:
|
||||
@@ -698,15 +769,62 @@ def require_vfs_dir() -> None:
|
||||
CORE.data[KEY_VFS_DIR_REQUIRED] = True
|
||||
|
||||
|
||||
def enable_ringbuf_in_iram() -> None:
|
||||
"""Keep ring buffer functions in IRAM instead of moving them to flash.
|
||||
def require_vfs_termios() -> None:
|
||||
"""Mark that VFS termios support is required by a component.
|
||||
|
||||
Call this from components that use esphome/core/ring_buffer.cpp and need
|
||||
the ring buffer functions to remain in IRAM for performance reasons
|
||||
(e.g., voice assistants, audio components).
|
||||
This prevents CONFIG_RINGBUF_PLACE_FUNCTIONS_INTO_FLASH from being enabled.
|
||||
Call this from components that use terminal I/O functions (usb_serial_jtag_vfs_*, etc.).
|
||||
This prevents CONFIG_VFS_SUPPORT_TERMIOS from being disabled.
|
||||
"""
|
||||
CORE.data[KEY_RINGBUF_IN_IRAM] = True
|
||||
CORE.data[KEY_VFS_TERMIOS_REQUIRED] = True
|
||||
|
||||
|
||||
def require_full_certificate_bundle() -> None:
|
||||
"""Request the full certificate bundle instead of the common-CAs-only bundle.
|
||||
|
||||
By default, ESPHome uses CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_CMN which
|
||||
includes only CAs with >1% market share (~51 KB smaller than full bundle).
|
||||
This covers ~99% of websites including Let's Encrypt, DigiCert, Google, Amazon.
|
||||
|
||||
Call this from components that need to connect to services using uncommon CAs.
|
||||
"""
|
||||
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:
|
||||
@@ -790,6 +908,22 @@ FRAMEWORK_SCHEMA = cv.Schema(
|
||||
min=8192, max=32768
|
||||
),
|
||||
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(
|
||||
@@ -979,6 +1113,19 @@ def _configure_lwip_max_sockets(conf: dict) -> None:
|
||||
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)
|
||||
async def _add_yaml_idf_components(components: list[ConfigType]):
|
||||
"""Add IDF components from YAML config with final priority to override code-added components."""
|
||||
@@ -1062,6 +1209,19 @@ async def to_code(config):
|
||||
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:
|
||||
cg.add_build_flag("-DUSE_ARDUINO")
|
||||
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO")
|
||||
@@ -1094,6 +1254,18 @@ async def to_code(config):
|
||||
|
||||
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_ESPTOOLPY_FLASHSIZE_{config[CONF_FLASH_SIZE]}", True
|
||||
@@ -1132,18 +1304,14 @@ async def to_code(config):
|
||||
add_idf_sdkconfig_option("CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH", True)
|
||||
|
||||
# Place ring buffer functions into flash instead of IRAM by default
|
||||
# This saves IRAM but may impact performance for audio/voice components.
|
||||
# Components that need ring buffer in IRAM call enable_ringbuf_in_iram().
|
||||
# Users can also set ringbuf_in_iram: true to force IRAM placement.
|
||||
# In ESP-IDF 6.0 flash placement becomes the default.
|
||||
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
|
||||
# This saves IRAM. In ESP-IDF 6.0 flash placement becomes the default.
|
||||
# Users can set ringbuf_in_iram: true as an escape hatch if they encounter issues.
|
||||
if conf[CONF_ADVANCED][CONF_RINGBUF_IN_IRAM]:
|
||||
# User requests ring buffer in IRAM
|
||||
# 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)
|
||||
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)
|
||||
|
||||
# Place heap functions into flash to save IRAM (~4-6KB savings)
|
||||
@@ -1171,6 +1339,11 @@ async def to_code(config):
|
||||
|
||||
# Apply LWIP optimization settings
|
||||
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
|
||||
# WiFi component handles its own optimization when AP mode is not used
|
||||
# When using Arduino with Ethernet, DHCP server functions must be available
|
||||
@@ -1209,11 +1382,18 @@ async def to_code(config):
|
||||
add_idf_sdkconfig_option("CONFIG_LIBC_LOCKS_PLACE_IN_IRAM", False)
|
||||
|
||||
# Disable VFS support for termios (terminal I/O functions)
|
||||
# ESPHome doesn't use termios functions on ESP32 (only used in host UART driver).
|
||||
# USB Serial JTAG VFS functions require termios support.
|
||||
# Components that need it (e.g., logger when USB_SERIAL_JTAG is supported but not selected
|
||||
# as the logger output) call require_vfs_termios().
|
||||
# Saves approximately 1.8KB of flash when disabled (default).
|
||||
add_idf_sdkconfig_option(
|
||||
"CONFIG_VFS_SUPPORT_TERMIOS", not advanced[CONF_DISABLE_VFS_SUPPORT_TERMIOS]
|
||||
)
|
||||
if CORE.data.get(KEY_VFS_TERMIOS_REQUIRED, False):
|
||||
# Component requires VFS termios - force enable regardless of user setting
|
||||
add_idf_sdkconfig_option("CONFIG_VFS_SUPPORT_TERMIOS", True)
|
||||
else:
|
||||
# No component needs it - allow user to control (default: disabled)
|
||||
add_idf_sdkconfig_option(
|
||||
"CONFIG_VFS_SUPPORT_TERMIOS", not advanced[CONF_DISABLE_VFS_SUPPORT_TERMIOS]
|
||||
)
|
||||
|
||||
# Disable VFS support for select() with file descriptors
|
||||
# ESPHome only uses select() with sockets via lwip_select(), which still works.
|
||||
@@ -1292,6 +1472,61 @@ async def to_code(config):
|
||||
|
||||
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():
|
||||
add_idf_sdkconfig_option(name, RawSdkconfigValue(value))
|
||||
|
||||
@@ -1300,6 +1535,11 @@ async def to_code(config):
|
||||
if 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 = {
|
||||
"2MB": 0x0C0000, # 768 KB
|
||||
|
||||
@@ -175,6 +175,32 @@ ESP32_BOARD_PINS = {
|
||||
"LED": 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": {
|
||||
"BUTTON": 0,
|
||||
"A0": 18,
|
||||
|
||||
@@ -6,12 +6,14 @@ KEY_FLASH_SIZE = "flash_size"
|
||||
KEY_VARIANT = "variant"
|
||||
KEY_SDKCONFIG_OPTIONS = "sdkconfig_options"
|
||||
KEY_COMPONENTS = "components"
|
||||
KEY_EXCLUDE_COMPONENTS = "exclude_components"
|
||||
KEY_REPO = "repo"
|
||||
KEY_REF = "ref"
|
||||
KEY_REFRESH = "refresh"
|
||||
KEY_PATH = "path"
|
||||
KEY_SUBMODULES = "submodules"
|
||||
KEY_EXTRA_BUILD_FILES = "extra_build_files"
|
||||
KEY_FULL_CERT_BUNDLE = "full_cert_bundle"
|
||||
|
||||
VARIANT_ESP32 = "ESP32"
|
||||
VARIANT_ESP32C2 = "ESP32C2"
|
||||
|
||||
@@ -85,6 +85,7 @@ void ESP32InternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpi
|
||||
break;
|
||||
}
|
||||
gpio_set_intr_type(this->get_pin_num(), idf_type);
|
||||
gpio_intr_enable(this->get_pin_num());
|
||||
if (!isr_service_installed) {
|
||||
auto res = gpio_install_isr_service(ESP_INTR_FLAG_LEVEL3);
|
||||
if (res != ESP_OK) {
|
||||
@@ -94,7 +95,6 @@ void ESP32InternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpi
|
||||
isr_service_installed = true;
|
||||
}
|
||||
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 {
|
||||
|
||||
@@ -19,7 +19,16 @@ static constexpr size_t KEY_BUFFER_SIZE = 12;
|
||||
|
||||
struct NVSData {
|
||||
uint32_t key;
|
||||
SmallInlineBuffer<8> data; // Most prefs fit in 8 bytes (covers fan, cover, select, etc.)
|
||||
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)
|
||||
@@ -32,14 +41,14 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend {
|
||||
// try find in pending saves and update that
|
||||
for (auto &obj : s_pending_save) {
|
||||
if (obj.key == this->key) {
|
||||
obj.data.set(data, len);
|
||||
obj.set_data(data, len);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
NVSData save{};
|
||||
save.key = this->key;
|
||||
save.data.set(data, len);
|
||||
s_pending_save.push_back(std::move(save));
|
||||
save.set_data(data, len);
|
||||
s_pending_save.emplace_back(std::move(save));
|
||||
ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len);
|
||||
return true;
|
||||
}
|
||||
@@ -47,11 +56,11 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend {
|
||||
// try find in pending saves and load from that
|
||||
for (auto &obj : s_pending_save) {
|
||||
if (obj.key == this->key) {
|
||||
if (obj.data.size() != len) {
|
||||
if (obj.len != len) {
|
||||
// size mismatch
|
||||
return false;
|
||||
}
|
||||
memcpy(data, obj.data.data(), len);
|
||||
memcpy(data, obj.data.get(), len);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -127,10 +136,10 @@ class ESP32Preferences : public ESPPreferences {
|
||||
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
|
||||
ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str);
|
||||
if (this->is_changed_(this->nvs_handle, save, key_str)) {
|
||||
esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.data(), save.data.size());
|
||||
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, 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.len);
|
||||
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++;
|
||||
last_err = err;
|
||||
last_key = save.key;
|
||||
@@ -138,7 +147,7 @@ class ESP32Preferences : public ESPPreferences {
|
||||
}
|
||||
written++;
|
||||
} 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++;
|
||||
}
|
||||
s_pending_save.erase(s_pending_save.begin() + i);
|
||||
@@ -169,7 +178,7 @@ class ESP32Preferences : public ESPPreferences {
|
||||
return true;
|
||||
}
|
||||
// Check size first before allocating memory
|
||||
if (actual_len != to_save.data.size()) {
|
||||
if (actual_len != to_save.len) {
|
||||
return true;
|
||||
}
|
||||
// Most preferences are small, use stack buffer with heap fallback for large ones
|
||||
@@ -179,7 +188,7 @@ class ESP32Preferences : public ESPPreferences {
|
||||
ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err));
|
||||
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 {
|
||||
|
||||
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) {
|
||||
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_->set_manufacturer_data(data);
|
||||
this->advertising_start();
|
||||
|
||||
@@ -118,7 +118,6 @@ class ESP32BLE : public Component {
|
||||
void advertising_start();
|
||||
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(std::span<const uint8_t> data);
|
||||
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_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) {
|
||||
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;
|
||||
this->advertising_data_.p_manufacturer_data = nullptr;
|
||||
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_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(std::span<const uint8_t> data);
|
||||
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(std::span<const uint8_t> data);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "esp32_ble_beacon.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
|
||||
@@ -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)
|
||||
new Trigger<std::vector<uint8_t>, uint16_t>();
|
||||
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:
|
||||
// 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
|
||||
// Convert span to vector for trigger
|
||||
on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id);
|
||||
});
|
||||
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)
|
||||
new Trigger<std::vector<uint8_t>, uint16_t>();
|
||||
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:
|
||||
// 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
|
||||
// Convert span to vector for trigger
|
||||
on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id);
|
||||
});
|
||||
return on_write_trigger;
|
||||
|
||||
@@ -12,6 +12,7 @@ from esphome.const import (
|
||||
KEY_FRAMEWORK_VERSION,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.cpp_generator import add_define
|
||||
|
||||
CODEOWNERS = ["@swoboda1337"]
|
||||
|
||||
@@ -42,6 +43,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
add_define("USE_ESP32_HOSTED")
|
||||
if config[CONF_ACTIVE_HIGH]:
|
||||
esp32.add_idf_sdkconfig_option(
|
||||
"CONFIG_ESP_HOSTED_SDIO_RESET_ACTIVE_HIGH",
|
||||
|
||||
@@ -5,6 +5,7 @@ from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import esp32, light
|
||||
from esphome.components.const import CONF_USE_PSRAM
|
||||
from esphome.components.esp32 import include_builtin_idf_component
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_CHIPSET,
|
||||
@@ -129,6 +130,9 @@ CONFIG_SCHEMA = cv.All(
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
# Re-enable ESP-IDF's RMT driver (excluded by default to save compile time)
|
||||
include_builtin_idf_component("esp_driver_rmt")
|
||||
|
||||
var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
|
||||
await light.register_light(var, config)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
@@ -6,6 +6,7 @@ from esphome.components.esp32 import (
|
||||
VARIANT_ESP32S3,
|
||||
get_esp32_variant,
|
||||
gpio,
|
||||
include_builtin_idf_component,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
@@ -266,6 +267,9 @@ CONFIG_SCHEMA = cv.All(
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
# Re-enable ESP-IDF's touch sensor driver (excluded by default to save compile time)
|
||||
include_builtin_idf_component("esp_driver_touch_sens")
|
||||
|
||||
touch = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(touch, config)
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ from esphome.components.esp32 import (
|
||||
add_idf_component,
|
||||
add_idf_sdkconfig_option,
|
||||
get_esp32_variant,
|
||||
include_builtin_idf_component,
|
||||
)
|
||||
from esphome.components.network import ip_address_literal
|
||||
from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface
|
||||
@@ -419,6 +420,9 @@ async def to_code(config):
|
||||
# Also disable WiFi/BT coexistence since WiFi is disabled
|
||||
add_idf_sdkconfig_option("CONFIG_SW_COEXIST_ENABLE", False)
|
||||
|
||||
# Re-enable ESP-IDF's Ethernet driver (excluded by default to save compile time)
|
||||
include_builtin_idf_component("esp_eth")
|
||||
|
||||
if config[CONF_TYPE] == "LAN8670":
|
||||
# Add LAN867x 10BASE-T1S PHY support component
|
||||
add_idf_component(name="espressif/lan867x", ref="2.0.0")
|
||||
|
||||
@@ -802,8 +802,8 @@ void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) {
|
||||
ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed");
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(PHY_REG_SIZE)];
|
||||
ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE));
|
||||
#endif
|
||||
ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE));
|
||||
|
||||
/*
|
||||
* Bit 7 is `RMII Reference Clock Select`. Default is `0`.
|
||||
@@ -820,10 +820,8 @@ void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) {
|
||||
ESPHL_ERROR_CHECK(err, "Write PHY Control 2 failed");
|
||||
err = mac->read_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, &(phy_control_2));
|
||||
ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed");
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s",
|
||||
format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#endif // USE_ETHERNET_KSZ8081
|
||||
|
||||
@@ -16,12 +16,8 @@ namespace event {
|
||||
#define LOG_EVENT(prefix, type, obj) \
|
||||
if ((obj) != nullptr) { \
|
||||
ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \
|
||||
if (!(obj)->get_icon_ref().empty()) { \
|
||||
ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \
|
||||
} \
|
||||
if (!(obj)->get_device_class_ref().empty()) { \
|
||||
ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class_ref().c_str()); \
|
||||
} \
|
||||
LOG_ENTITY_ICON(TAG, prefix, *(obj)); \
|
||||
LOG_ENTITY_DEVICE_CLASS(TAG, prefix, *(obj)); \
|
||||
}
|
||||
|
||||
class Event : public EntityBase, public EntityBase_DeviceClass {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
#include <cinttypes>
|
||||
|
||||
@@ -19,7 +20,8 @@ static bool was_power_cycled() {
|
||||
#endif
|
||||
#ifdef USE_ESP8266
|
||||
auto reset_reason = EspClass::getResetReason();
|
||||
return strcasecmp(reset_reason.c_str(), "power On") == 0 || strcasecmp(reset_reason.c_str(), "external system") == 0;
|
||||
return ESPHOME_strcasecmp_P(reset_reason.c_str(), ESPHOME_PSTR("power On")) == 0 ||
|
||||
ESPHOME_strcasecmp_P(reset_reason.c_str(), ESPHOME_PSTR("external system")) == 0;
|
||||
#endif
|
||||
#ifdef USE_LIBRETINY
|
||||
auto reason = lt_get_reboot_reason();
|
||||
|
||||
@@ -71,7 +71,7 @@ void FanCall::validate_() {
|
||||
auto traits = this->parent_.get_traits();
|
||||
|
||||
if (this->speed_.has_value()) {
|
||||
this->speed_ = clamp(*this->speed_, 1, static_cast<int>(traits.supported_speed_count()));
|
||||
this->speed_ = clamp(*this->speed_, 1, traits.supported_speed_count());
|
||||
|
||||
// https://developers.home-assistant.io/docs/core/entity/fan/#preset-modes
|
||||
// "Manually setting a speed must disable any set preset mode"
|
||||
@@ -227,8 +227,7 @@ void Fan::publish_state() {
|
||||
constexpr uint32_t RESTORE_STATE_VERSION = 0x71700ABA;
|
||||
optional<FanRestoreState> Fan::restore_state_() {
|
||||
FanRestoreState recovered{};
|
||||
this->rtc_ =
|
||||
global_preferences->make_preference<FanRestoreState>(this->get_preference_hash() ^ RESTORE_STATE_VERSION);
|
||||
this->rtc_ = this->make_entity_preference<FanRestoreState>(RESTORE_STATE_VERSION);
|
||||
bool restored = this->rtc_.load(&recovered);
|
||||
|
||||
switch (this->restore_mode_) {
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace fan {
|
||||
class FanTraits {
|
||||
public:
|
||||
FanTraits() = default;
|
||||
FanTraits(bool oscillation, bool speed, bool direction, uint8_t speed_count)
|
||||
FanTraits(bool oscillation, bool speed, bool direction, int speed_count)
|
||||
: oscillation_(oscillation), speed_(speed), direction_(direction), speed_count_(speed_count) {}
|
||||
|
||||
/// Return if this fan supports oscillation.
|
||||
@@ -23,9 +23,9 @@ class FanTraits {
|
||||
/// Set whether this fan supports speed levels.
|
||||
void set_speed(bool speed) { this->speed_ = speed; }
|
||||
/// Return how many speed levels the fan has
|
||||
uint8_t supported_speed_count() const { return this->speed_count_; }
|
||||
int supported_speed_count() const { return this->speed_count_; }
|
||||
/// Set how many speed levels this fan has.
|
||||
void set_supported_speed_count(uint8_t speed_count) { this->speed_count_ = speed_count; }
|
||||
void set_supported_speed_count(int speed_count) { this->speed_count_ = speed_count; }
|
||||
/// Return if this fan supports changing direction
|
||||
bool supports_direction() const { return this->direction_; }
|
||||
/// Set whether this fan supports changing direction
|
||||
@@ -64,7 +64,7 @@ class FanTraits {
|
||||
bool oscillation_{false};
|
||||
bool speed_{false};
|
||||
bool direction_{false};
|
||||
uint8_t speed_count_{};
|
||||
int speed_count_{};
|
||||
std::vector<const char *> preset_modes_{};
|
||||
};
|
||||
|
||||
|
||||
@@ -350,8 +350,7 @@ ClimateTraits HaierClimateBase::traits() { return traits_; }
|
||||
|
||||
void HaierClimateBase::initialization() {
|
||||
constexpr uint32_t restore_settings_version = 0xA77D21EF;
|
||||
this->base_rtc_ =
|
||||
global_preferences->make_preference<HaierBaseSettings>(this->get_preference_hash() ^ restore_settings_version);
|
||||
this->base_rtc_ = this->make_entity_preference<HaierBaseSettings>(restore_settings_version);
|
||||
HaierBaseSettings recovered;
|
||||
if (!this->base_rtc_.load(&recovered)) {
|
||||
recovered = {false, true};
|
||||
|
||||
@@ -515,8 +515,7 @@ haier_protocol::HaierMessage HonClimate::get_power_message(bool state) {
|
||||
void HonClimate::initialization() {
|
||||
HaierClimateBase::initialization();
|
||||
constexpr uint32_t restore_settings_version = 0x57EB59DDUL;
|
||||
this->hon_rtc_ =
|
||||
global_preferences->make_preference<HonSettings>(this->get_preference_hash() ^ restore_settings_version);
|
||||
this->hon_rtc_ = this->make_entity_preference<HonSettings>(restore_settings_version);
|
||||
HonSettings recovered;
|
||||
if (this->hon_rtc_.load(&recovered)) {
|
||||
this->settings_ = recovered;
|
||||
|
||||
@@ -39,7 +39,7 @@ CONFIG_SCHEMA = (
|
||||
cv.Optional(CONF_DECAY_MODE, default="SLOW"): cv.enum(
|
||||
DECAY_MODE_OPTIONS, upper=True
|
||||
),
|
||||
cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1, max=255),
|
||||
cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1),
|
||||
cv.Optional(CONF_ENABLE_PIN): cv.use_id(output.FloatOutput),
|
||||
cv.Optional(CONF_PRESET_MODES): validate_preset_modes,
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ enum DecayMode {
|
||||
|
||||
class HBridgeFan : public Component, public fan::Fan {
|
||||
public:
|
||||
HBridgeFan(uint8_t speed_count, DecayMode decay_mode) : speed_count_(speed_count), decay_mode_(decay_mode) {}
|
||||
HBridgeFan(int speed_count, DecayMode decay_mode) : speed_count_(speed_count), decay_mode_(decay_mode) {}
|
||||
|
||||
void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; }
|
||||
void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; }
|
||||
@@ -33,7 +33,7 @@ class HBridgeFan : public Component, public fan::Fan {
|
||||
output::FloatOutput *pin_b_;
|
||||
output::FloatOutput *enable_{nullptr};
|
||||
output::BinaryOutput *oscillating_{nullptr};
|
||||
uint8_t speed_count_{};
|
||||
int speed_count_{};
|
||||
DecayMode decay_mode_{DECAY_MODE_SLOW};
|
||||
fan::FanTraits traits_;
|
||||
std::vector<const char *> preset_modes_{};
|
||||
|
||||
@@ -126,7 +126,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
),
|
||||
cv.Optional(CONF_CA_CERTIFICATE_PATH): cv.All(
|
||||
cv.file_,
|
||||
cv.only_on(PLATFORM_HOST),
|
||||
cv.Any(cv.only_on(PLATFORM_HOST), cv.only_on_esp32),
|
||||
),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
@@ -155,12 +155,32 @@ async def to_code(config):
|
||||
cg.add(var.set_watchdog_timeout(timeout_ms))
|
||||
|
||||
if CORE.is_esp32:
|
||||
# Re-enable ESP-IDF's HTTP client (excluded by default to save compile time)
|
||||
esp32.include_builtin_idf_component("esp_http_client")
|
||||
|
||||
cg.add(var.set_buffer_size_rx(config[CONF_BUFFER_SIZE_RX]))
|
||||
cg.add(var.set_buffer_size_tx(config[CONF_BUFFER_SIZE_TX]))
|
||||
cg.add(var.set_verify_ssl(config[CONF_VERIFY_SSL]))
|
||||
|
||||
if config.get(CONF_VERIFY_SSL):
|
||||
esp32.add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True)
|
||||
if ca_cert_path := config.get(CONF_CA_CERTIFICATE_PATH):
|
||||
with open(ca_cert_path, encoding="utf-8") as f:
|
||||
ca_cert_content = f.read()
|
||||
cg.add(var.set_ca_certificate(ca_cert_content))
|
||||
else:
|
||||
# Uses the certificate bundle configured in esp32 component.
|
||||
# By default, ESPHome uses the CMN (common CAs) bundle which covers
|
||||
# ~99% of websites including GitHub, Let's Encrypt, DigiCert, etc.
|
||||
# If connecting to services with uncommon CAs, components can call:
|
||||
# esp32.require_full_certificate_bundle()
|
||||
# Or users can set in their config:
|
||||
# esp32:
|
||||
# framework:
|
||||
# advanced:
|
||||
# use_full_certificate_bundle: true
|
||||
esp32.add_idf_sdkconfig_option(
|
||||
"CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True
|
||||
)
|
||||
|
||||
esp32.add_idf_sdkconfig_option(
|
||||
"CONFIG_ESP_TLS_INSECURE",
|
||||
|
||||
@@ -131,6 +131,10 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur
|
||||
}
|
||||
}
|
||||
|
||||
// HTTPClient::getSize() returns -1 for chunked transfer encoding (no Content-Length).
|
||||
// When cast to size_t, -1 becomes SIZE_MAX (4294967295 on 32-bit).
|
||||
// The read() method handles this: bytes_read_ can never reach SIZE_MAX, so the
|
||||
// early return check (bytes_read_ >= content_length) will never trigger.
|
||||
int content_length = container->client_.getSize();
|
||||
ESP_LOGD(TAG, "Content-Length: %d", content_length);
|
||||
container->content_length = (size_t) content_length;
|
||||
@@ -167,17 +171,23 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
|
||||
}
|
||||
|
||||
int available_data = stream_ptr->available();
|
||||
int bufsize = std::min(max_len, std::min(this->content_length - this->bytes_read_, (size_t) available_data));
|
||||
// For chunked transfer encoding, HTTPClient::getSize() returns -1, which becomes SIZE_MAX when
|
||||
// cast to size_t. SIZE_MAX - bytes_read_ is still huge, so it won't limit the read.
|
||||
size_t remaining = (this->content_length > 0) ? (this->content_length - this->bytes_read_) : max_len;
|
||||
int bufsize = std::min(max_len, std::min(remaining, (size_t) available_data));
|
||||
|
||||
if (bufsize == 0) {
|
||||
this->duration_ms += (millis() - start);
|
||||
// Check if we've read all expected content
|
||||
if (this->bytes_read_ >= this->content_length) {
|
||||
// Check if we've read all expected content (only valid when content_length is known and not SIZE_MAX)
|
||||
// For chunked encoding (content_length == SIZE_MAX), we can't use this check
|
||||
if (this->content_length > 0 && this->bytes_read_ >= this->content_length) {
|
||||
return 0; // All content read successfully
|
||||
}
|
||||
// No data available - check if connection is still open
|
||||
// For chunked encoding, !connected() after reading means EOF (all chunks received)
|
||||
// For known content_length with bytes_read_ < content_length, it means connection dropped
|
||||
if (!stream_ptr->connected()) {
|
||||
return HTTP_ERROR_CONNECTION_CLOSED; // Connection closed prematurely
|
||||
return HTTP_ERROR_CONNECTION_CLOSED; // Connection closed or EOF for chunked
|
||||
}
|
||||
return 0; // No data yet, caller should retry
|
||||
}
|
||||
|
||||
@@ -27,8 +27,9 @@ void HttpRequestIDF::dump_config() {
|
||||
HttpRequestComponent::dump_config();
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Buffer Size RX: %u\n"
|
||||
" Buffer Size TX: %u",
|
||||
this->buffer_size_rx_, this->buffer_size_tx_);
|
||||
" Buffer Size TX: %u\n"
|
||||
" Custom CA Certificate: %s",
|
||||
this->buffer_size_rx_, this->buffer_size_tx_, YESNO(this->ca_certificate_ != nullptr));
|
||||
}
|
||||
|
||||
esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) {
|
||||
@@ -88,11 +89,15 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
|
||||
config.disable_auto_redirect = !this->follow_redirects_;
|
||||
config.max_redirection_count = this->redirect_limit_;
|
||||
config.auth_type = HTTP_AUTH_TYPE_BASIC;
|
||||
#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
|
||||
if (secure && this->verify_ssl_) {
|
||||
config.crt_bundle_attach = esp_crt_bundle_attach;
|
||||
}
|
||||
if (this->ca_certificate_ != nullptr) {
|
||||
config.cert_pem = this->ca_certificate_;
|
||||
#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
|
||||
} else {
|
||||
config.crt_bundle_attach = esp_crt_bundle_attach;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
if (this->useragent_ != nullptr) {
|
||||
config.user_agent = this->useragent_;
|
||||
@@ -152,6 +157,8 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
|
||||
}
|
||||
|
||||
container->feed_wdt();
|
||||
// esp_http_client_fetch_headers() returns 0 for chunked transfer encoding (no Content-Length header).
|
||||
// The read() method handles content_length == 0 specially to support chunked responses.
|
||||
container->content_length = esp_http_client_fetch_headers(client);
|
||||
container->feed_wdt();
|
||||
container->status_code = esp_http_client_get_status_code(client);
|
||||
@@ -220,14 +227,22 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
|
||||
//
|
||||
// We normalize to HttpContainer::read() contract:
|
||||
// > 0: bytes read
|
||||
// 0: no data yet / all content read (caller should check bytes_read vs content_length)
|
||||
// 0: all content read (only returned when content_length is known and fully read)
|
||||
// < 0: error/connection closed
|
||||
//
|
||||
// Note on chunked transfer encoding:
|
||||
// esp_http_client_fetch_headers() returns 0 for chunked responses (no Content-Length header).
|
||||
// We handle this by skipping the content_length check when content_length is 0,
|
||||
// allowing esp_http_client_read() to handle chunked decoding internally and signal EOF
|
||||
// by returning 0.
|
||||
int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
|
||||
const uint32_t start = millis();
|
||||
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
|
||||
|
||||
// Check if we've already read all expected content
|
||||
if (this->bytes_read_ >= this->content_length) {
|
||||
// Skip this check when content_length is 0 (chunked transfer encoding or unknown length)
|
||||
// For chunked responses, esp_http_client_read() will return 0 when all data is received
|
||||
if (this->content_length > 0 && this->bytes_read_ >= this->content_length) {
|
||||
return 0; // All content read successfully
|
||||
}
|
||||
|
||||
@@ -242,7 +257,13 @@ int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
|
||||
return read_len_or_error;
|
||||
}
|
||||
|
||||
// Connection closed by server before all content received
|
||||
// esp_http_client_read() returns 0 in two cases:
|
||||
// 1. Known content_length: connection closed before all data received (error)
|
||||
// 2. Chunked encoding (content_length == 0): end of stream reached (EOF)
|
||||
// For case 1, returning HTTP_ERROR_CONNECTION_CLOSED is correct.
|
||||
// For case 2, 0 indicates that all chunked data has already been delivered
|
||||
// in previous successful read() calls, so treating this as a closed
|
||||
// connection does not cause any loss of response data.
|
||||
if (read_len_or_error == 0) {
|
||||
return HTTP_ERROR_CONNECTION_CLOSED;
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ class HttpRequestIDF : public HttpRequestComponent {
|
||||
void set_buffer_size_rx(uint16_t buffer_size_rx) { this->buffer_size_rx_ = buffer_size_rx; }
|
||||
void set_buffer_size_tx(uint16_t buffer_size_tx) { this->buffer_size_tx_ = buffer_size_tx; }
|
||||
void set_verify_ssl(bool verify_ssl) { this->verify_ssl_ = verify_ssl; }
|
||||
void set_ca_certificate(const char *ca_certificate) { this->ca_certificate_ = ca_certificate; }
|
||||
|
||||
protected:
|
||||
std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, const std::string &body,
|
||||
@@ -44,6 +45,7 @@ class HttpRequestIDF : public HttpRequestComponent {
|
||||
uint16_t buffer_size_rx_{};
|
||||
uint16_t buffer_size_tx_{};
|
||||
bool verify_ssl_{true};
|
||||
const char *ca_certificate_{nullptr};
|
||||
|
||||
/// @brief Monitors the http client events to gather response headers
|
||||
static esp_err_t http_event_handler(esp_http_client_event_t *evt);
|
||||
|
||||
@@ -82,7 +82,7 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
|
||||
uint32_t last_progress = 0;
|
||||
uint32_t update_start_time = millis();
|
||||
md5::MD5Digest md5_receive;
|
||||
std::unique_ptr<char[]> md5_receive_str(new char[33]);
|
||||
char md5_receive_str[33];
|
||||
|
||||
if (this->md5_expected_.empty() && !this->http_get_md5_()) {
|
||||
return OTA_MD5_INVALID;
|
||||
@@ -176,14 +176,14 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
|
||||
|
||||
// verify MD5 is as expected and act accordingly
|
||||
md5_receive.calculate();
|
||||
md5_receive.get_hex(md5_receive_str.get());
|
||||
this->md5_computed_ = md5_receive_str.get();
|
||||
md5_receive.get_hex(md5_receive_str);
|
||||
this->md5_computed_ = md5_receive_str;
|
||||
if (strncmp(this->md5_computed_.c_str(), this->md5_expected_.c_str(), MD5_SIZE) != 0) {
|
||||
ESP_LOGE(TAG, "MD5 computed: %s - Aborting due to MD5 mismatch", this->md5_computed_.c_str());
|
||||
this->cleanup_(std::move(backend), container);
|
||||
return ota::OTA_RESPONSE_ERROR_MD5_MISMATCH;
|
||||
} else {
|
||||
backend->set_update_md5(md5_receive_str.get());
|
||||
backend->set_update_md5(md5_receive_str);
|
||||
}
|
||||
|
||||
container->end();
|
||||
|
||||
@@ -2,6 +2,25 @@
|
||||
|
||||
from . import BoardConfig
|
||||
|
||||
# Huidu HD-WF1
|
||||
BoardConfig(
|
||||
"huidu-hd-wf1",
|
||||
r1_pin=2,
|
||||
g1_pin=6,
|
||||
b1_pin=3,
|
||||
r2_pin=4,
|
||||
g2_pin=8,
|
||||
b2_pin=5,
|
||||
a_pin=39,
|
||||
b_pin=38,
|
||||
c_pin=37,
|
||||
d_pin=36,
|
||||
e_pin=12,
|
||||
lat_pin=33,
|
||||
oe_pin=35,
|
||||
clk_pin=34,
|
||||
)
|
||||
|
||||
# Huidu HD-WF2
|
||||
BoardConfig(
|
||||
"huidu-hd-wf2",
|
||||
|
||||
@@ -587,7 +587,7 @@ def _build_config_struct(
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
add_idf_component(
|
||||
name="esphome/esp-hub75",
|
||||
ref="0.3.0",
|
||||
ref="0.3.2",
|
||||
)
|
||||
|
||||
# Set compile-time configuration via build flags (so external library sees them)
|
||||
|
||||
@@ -119,7 +119,7 @@ void IDFI2CBus::dump_config() {
|
||||
if (s.second) {
|
||||
ESP_LOGCONFIG(TAG, "Found device at address 0x%02X", s.first);
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, "Unknown error at address 0x%02X", s.first);
|
||||
ESP_LOGE(TAG, "Unknown error at address 0x%02X", s.first);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -185,7 +185,7 @@ ErrorCode IDFI2CBus::write_readv(uint8_t address, const uint8_t *write_buffer, s
|
||||
}
|
||||
jobs[num_jobs++].command = I2C_MASTER_CMD_STOP;
|
||||
ESP_LOGV(TAG, "Sending %zu jobs", num_jobs);
|
||||
esp_err_t err = i2c_master_execute_defined_operations(this->dev_, jobs, num_jobs, 20);
|
||||
esp_err_t err = i2c_master_execute_defined_operations(this->dev_, jobs, num_jobs, 100);
|
||||
if (err == ESP_ERR_INVALID_STATE) {
|
||||
ESP_LOGV(TAG, "TX to %02X failed: not acked", address);
|
||||
return ERROR_NOT_ACKNOWLEDGED;
|
||||
|
||||
@@ -2,8 +2,8 @@ from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.esp32 import (
|
||||
add_idf_sdkconfig_option,
|
||||
enable_ringbuf_in_iram,
|
||||
get_esp32_variant,
|
||||
include_builtin_idf_component,
|
||||
)
|
||||
from esphome.components.esp32.const import (
|
||||
VARIANT_ESP32,
|
||||
@@ -275,15 +275,16 @@ FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
# Re-enable ESP-IDF's I2S driver (excluded by default to save compile time)
|
||||
include_builtin_idf_component("esp_driver_i2s")
|
||||
|
||||
if use_legacy():
|
||||
cg.add_define("USE_I2S_LEGACY")
|
||||
|
||||
# Helps avoid callbacks being skipped due to processor load
|
||||
add_idf_sdkconfig_option("CONFIG_I2S_ISR_IRAM_SAFE", True)
|
||||
|
||||
# Keep ring buffer functions in IRAM for audio performance
|
||||
enable_ringbuf_in_iram()
|
||||
|
||||
cg.add(var.set_lrclk_pin(config[CONF_I2S_LRCLK_PIN]))
|
||||
if CONF_I2S_BCLK_PIN in config:
|
||||
cg.add(var.set_bclk_pin(config[CONF_I2S_BCLK_PIN]))
|
||||
|
||||
@@ -267,26 +267,16 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
|
||||
for (auto &scan : results) {
|
||||
if (scan.get_is_hidden())
|
||||
continue;
|
||||
const char *ssid_cstr = scan.get_ssid().c_str();
|
||||
// Check if we've already sent this SSID
|
||||
bool duplicate = false;
|
||||
for (const auto &seen : networks) {
|
||||
if (strcmp(seen.c_str(), ssid_cstr) == 0) {
|
||||
duplicate = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (duplicate)
|
||||
const std::string &ssid = scan.get_ssid();
|
||||
if (std::find(networks.begin(), networks.end(), ssid) != networks.end())
|
||||
continue;
|
||||
// Only allocate std::string after confirming it's not a duplicate
|
||||
std::string ssid(ssid_cstr);
|
||||
// Send each ssid separately to avoid overflowing the buffer
|
||||
char rssi_buf[5]; // int8_t: -128 to 127, max 4 chars + null
|
||||
*int8_to_str(rssi_buf, scan.get_rssi()) = '\0';
|
||||
std::vector<uint8_t> data =
|
||||
improv::build_rpc_response(improv::GET_WIFI_NETWORKS, {ssid, rssi_buf, YESNO(scan.get_with_auth())}, false);
|
||||
this->send_response_(data);
|
||||
networks.push_back(std::move(ssid));
|
||||
networks.push_back(ssid);
|
||||
}
|
||||
// Send empty response to signify the end of the list.
|
||||
std::vector<uint8_t> data =
|
||||
|
||||
@@ -10,7 +10,7 @@ static const char *const TAG = "integration";
|
||||
|
||||
void IntegrationSensor::setup() {
|
||||
if (this->restore_) {
|
||||
this->pref_ = global_preferences->make_preference<float>(this->get_preference_hash());
|
||||
this->pref_ = this->make_entity_preference<float>();
|
||||
float preference_value = 0;
|
||||
this->pref_.load(&preference_value);
|
||||
this->result_ = preference_value;
|
||||
|
||||
@@ -11,7 +11,7 @@ static const char *const TAG = "kuntze";
|
||||
static const uint8_t CMD_READ_REG = 0x03;
|
||||
static const uint16_t REGISTER[] = {4136, 4160, 4680, 6000, 4688, 4728, 5832};
|
||||
|
||||
// Maximum bytes to log for Modbus responses (2 registers = 4 bytes, plus byte count = 5 bytes)
|
||||
// Maximum bytes to log for Modbus responses (2 registers = 4, plus count = 5)
|
||||
static constexpr size_t KUNTZE_MAX_LOG_BYTES = 8;
|
||||
|
||||
void Kuntze::on_modbus_data(const std::vector<uint8_t> &data) {
|
||||
|
||||
@@ -184,7 +184,7 @@ static inline bool validate_header_footer(const uint8_t *header_footer, const ui
|
||||
void LD2450Component::setup() {
|
||||
#ifdef USE_NUMBER
|
||||
if (this->presence_timeout_number_ != nullptr) {
|
||||
this->pref_ = global_preferences->make_preference<float>(this->presence_timeout_number_->get_preference_hash());
|
||||
this->pref_ = this->presence_timeout_number_->make_entity_preference<float>();
|
||||
this->set_presence_timeout();
|
||||
}
|
||||
#endif
|
||||
@@ -451,7 +451,7 @@ void LD2450Component::handle_periodic_data_() {
|
||||
int16_t ty = 0;
|
||||
int16_t td = 0;
|
||||
int16_t ts = 0;
|
||||
int16_t angle = 0;
|
||||
float angle = 0;
|
||||
uint8_t index = 0;
|
||||
Direction direction{DIRECTION_UNDEFINED};
|
||||
bool is_moving = false;
|
||||
|
||||
@@ -143,6 +143,7 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
|
||||
],
|
||||
icon=ICON_FORMAT_TEXT_ROTATION_ANGLE_UP,
|
||||
unit_of_measurement=UNIT_DEGREES,
|
||||
accuracy_decimals=1,
|
||||
),
|
||||
cv.Optional(CONF_DISTANCE): sensor.sensor_schema(
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
|
||||
@@ -191,10 +191,17 @@ def _notify_old_style(config):
|
||||
|
||||
|
||||
# The dev and latest branches will be at *least* this version, which is what matters.
|
||||
# Use GitHub releases directly to avoid PlatformIO moderation delays.
|
||||
ARDUINO_VERSIONS = {
|
||||
"dev": (cv.Version(1, 9, 2), "https://github.com/libretiny-eu/libretiny.git"),
|
||||
"latest": (cv.Version(1, 9, 2), "libretiny"),
|
||||
"recommended": (cv.Version(1, 9, 2), None),
|
||||
"dev": (cv.Version(1, 11, 0), "https://github.com/libretiny-eu/libretiny.git"),
|
||||
"latest": (
|
||||
cv.Version(1, 11, 0),
|
||||
"https://github.com/libretiny-eu/libretiny.git#v1.11.0",
|
||||
),
|
||||
"recommended": (
|
||||
cv.Version(1, 11, 0),
|
||||
"https://github.com/libretiny-eu/libretiny.git#v1.11.0",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -18,7 +18,16 @@ static constexpr size_t KEY_BUFFER_SIZE = 12;
|
||||
|
||||
struct NVSData {
|
||||
uint32_t key;
|
||||
SmallInlineBuffer<8> data; // Most prefs fit in 8 bytes (covers fan, cover, select, etc.)
|
||||
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)
|
||||
@@ -33,14 +42,14 @@ class LibreTinyPreferenceBackend : public ESPPreferenceBackend {
|
||||
// try find in pending saves and update that
|
||||
for (auto &obj : s_pending_save) {
|
||||
if (obj.key == this->key) {
|
||||
obj.data.set(data, len);
|
||||
obj.set_data(data, len);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
NVSData save{};
|
||||
save.key = this->key;
|
||||
save.data.set(data, len);
|
||||
s_pending_save.push_back(std::move(save));
|
||||
save.set_data(data, len);
|
||||
s_pending_save.emplace_back(std::move(save));
|
||||
ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len);
|
||||
return true;
|
||||
}
|
||||
@@ -49,11 +58,11 @@ class LibreTinyPreferenceBackend : public ESPPreferenceBackend {
|
||||
// try find in pending saves and load from that
|
||||
for (auto &obj : s_pending_save) {
|
||||
if (obj.key == this->key) {
|
||||
if (obj.data.size() != len) {
|
||||
if (obj.len != len) {
|
||||
// size mismatch
|
||||
return false;
|
||||
}
|
||||
memcpy(data, obj.data.data(), len);
|
||||
memcpy(data, obj.data.get(), len);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -117,11 +126,11 @@ class LibreTinyPreferences : public ESPPreferences {
|
||||
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
|
||||
ESP_LOGVV(TAG, "Checking if FDB data %s has changed", key_str);
|
||||
if (this->is_changed_(&this->db, save, key_str)) {
|
||||
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size());
|
||||
fdb_blob_make(&this->blob, save.data.data(), save.data.size());
|
||||
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.len);
|
||||
fdb_blob_make(&this->blob, save.data.get(), save.len);
|
||||
fdb_err_t err = fdb_kv_set_blob(&this->db, key_str, &this->blob);
|
||||
if (err != FDB_NO_ERR) {
|
||||
ESP_LOGV(TAG, "fdb_kv_set_blob('%s', len=%zu) failed: %d", key_str, save.data.size(), err);
|
||||
ESP_LOGV(TAG, "fdb_kv_set_blob('%s', len=%zu) failed: %d", key_str, save.len, err);
|
||||
failed++;
|
||||
last_err = err;
|
||||
last_key = save.key;
|
||||
@@ -129,7 +138,7 @@ class LibreTinyPreferences : public ESPPreferences {
|
||||
}
|
||||
written++;
|
||||
} else {
|
||||
ESP_LOGD(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.data.size());
|
||||
ESP_LOGD(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.len);
|
||||
cached++;
|
||||
}
|
||||
s_pending_save.erase(s_pending_save.begin() + i);
|
||||
@@ -153,7 +162,7 @@ class LibreTinyPreferences : public ESPPreferences {
|
||||
}
|
||||
|
||||
// Check size first - if different, data has changed
|
||||
if (kv.value_len != to_save.data.size()) {
|
||||
if (kv.value_len != to_save.len) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -167,7 +176,7 @@ class LibreTinyPreferences : public ESPPreferences {
|
||||
}
|
||||
|
||||
// Compare the actual data
|
||||
return memcmp(to_save.data.data(), stored_data.get(), kv.value_len) != 0;
|
||||
return memcmp(to_save.data.get(), stored_data.get(), kv.value_len) != 0;
|
||||
}
|
||||
|
||||
bool reset() override {
|
||||
|
||||
@@ -7,9 +7,7 @@ namespace esphome::light {
|
||||
|
||||
class AddressableLightWrapper : public light::AddressableLight {
|
||||
public:
|
||||
explicit AddressableLightWrapper(light::LightState *light_state) : light_state_(light_state) {
|
||||
this->wrapper_state_ = new uint8_t[5]; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
}
|
||||
explicit AddressableLightWrapper(light::LightState *light_state) : light_state_(light_state) {}
|
||||
|
||||
int32_t size() const override { return 1; }
|
||||
|
||||
@@ -118,7 +116,7 @@ class AddressableLightWrapper : public light::AddressableLight {
|
||||
}
|
||||
|
||||
light::LightState *light_state_;
|
||||
uint8_t *wrapper_state_;
|
||||
mutable uint8_t wrapper_state_[5]{};
|
||||
ColorMode color_mode_{ColorMode::UNKNOWN};
|
||||
};
|
||||
|
||||
|
||||
@@ -391,7 +391,10 @@ void LightCall::transform_parameters_() {
|
||||
min_mireds > 0.0f && max_mireds > 0.0f) {
|
||||
ESP_LOGD(TAG, "'%s': setting cold/warm white channels using white/color temperature values",
|
||||
this->parent_->get_name().c_str());
|
||||
if (this->has_color_temperature()) {
|
||||
// Only compute cold_white/warm_white from color_temperature if they're not already explicitly set.
|
||||
// This is important for state restoration, where both color_temperature and cold_white/warm_white
|
||||
// are restored from flash - we want to preserve the saved cold_white/warm_white values.
|
||||
if (this->has_color_temperature() && !this->has_cold_white() && !this->has_warm_white()) {
|
||||
const float color_temp = clamp(this->color_temperature_, min_mireds, max_mireds);
|
||||
const float range = max_mireds - min_mireds;
|
||||
const float ww_fraction = (color_temp - min_mireds) / range;
|
||||
|
||||
@@ -44,7 +44,7 @@ void LightState::setup() {
|
||||
case LIGHT_RESTORE_DEFAULT_ON:
|
||||
case LIGHT_RESTORE_INVERTED_DEFAULT_OFF:
|
||||
case LIGHT_RESTORE_INVERTED_DEFAULT_ON:
|
||||
this->rtc_ = global_preferences->make_preference<LightStateRTCState>(this->get_preference_hash());
|
||||
this->rtc_ = this->make_entity_preference<LightStateRTCState>();
|
||||
// Attempt to load from preferences, else fall back to default values
|
||||
if (!this->rtc_.load(&recovered)) {
|
||||
recovered.state = (this->restore_mode_ == LIGHT_RESTORE_DEFAULT_ON ||
|
||||
@@ -57,7 +57,7 @@ void LightState::setup() {
|
||||
break;
|
||||
case LIGHT_RESTORE_AND_OFF:
|
||||
case LIGHT_RESTORE_AND_ON:
|
||||
this->rtc_ = global_preferences->make_preference<LightStateRTCState>(this->get_preference_hash());
|
||||
this->rtc_ = this->make_entity_preference<LightStateRTCState>();
|
||||
this->rtc_.load(&recovered);
|
||||
recovered.state = (this->restore_mode_ == LIGHT_RESTORE_AND_ON);
|
||||
break;
|
||||
|
||||
@@ -14,9 +14,7 @@ class Lock;
|
||||
#define LOG_LOCK(prefix, type, obj) \
|
||||
if ((obj) != nullptr) { \
|
||||
ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \
|
||||
if (!(obj)->get_icon_ref().empty()) { \
|
||||
ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \
|
||||
} \
|
||||
LOG_ENTITY_ICON(TAG, prefix, *(obj)); \
|
||||
if ((obj)->traits.get_assumed_state()) { \
|
||||
ESP_LOGCONFIG(TAG, "%s Assumed State: YES", prefix); \
|
||||
} \
|
||||
|
||||
@@ -16,6 +16,8 @@ from esphome.components.esp32 import (
|
||||
VARIANT_ESP32S3,
|
||||
add_idf_sdkconfig_option,
|
||||
get_esp32_variant,
|
||||
require_usb_serial_jtag_secondary,
|
||||
require_vfs_termios,
|
||||
)
|
||||
from esphome.components.libretiny import get_libretiny_component, get_libretiny_family
|
||||
from esphome.components.libretiny.const import (
|
||||
@@ -397,9 +399,15 @@ async def to_code(config):
|
||||
elif config[CONF_HARDWARE_UART] == USB_SERIAL_JTAG:
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG", True)
|
||||
cg.add_define("USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG")
|
||||
# Define platform support flags for components that need auto-detection
|
||||
try:
|
||||
uart_selection(USB_SERIAL_JTAG)
|
||||
cg.add_define("USE_LOGGER_USB_SERIAL_JTAG")
|
||||
# USB Serial JTAG code is compiled when platform supports it.
|
||||
# Enable secondary USB serial JTAG console so the VFS functions are available.
|
||||
if CORE.is_esp32 and config[CONF_HARDWARE_UART] != USB_SERIAL_JTAG:
|
||||
require_usb_serial_jtag_secondary()
|
||||
require_vfs_termios()
|
||||
except cv.Invalid:
|
||||
pass
|
||||
try:
|
||||
|
||||
@@ -234,7 +234,6 @@ class Logger : public Component {
|
||||
#endif
|
||||
|
||||
protected:
|
||||
void write_msg_(const char *msg, size_t len);
|
||||
// RAII guard for recursion flags - sets flag on construction, clears on destruction
|
||||
class RecursionGuard {
|
||||
public:
|
||||
@@ -261,6 +260,7 @@ class Logger : public Component {
|
||||
#endif
|
||||
#endif
|
||||
void process_messages_();
|
||||
void write_msg_(const char *msg, size_t len);
|
||||
|
||||
// Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator
|
||||
// It's the caller's responsibility to initialize buffer_at (typically to 0)
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
#ifdef USE_ESP8266
|
||||
#include "logger.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
static const char *const TAG = "logger";
|
||||
|
||||
void Logger::pre_setup() {
|
||||
if (this->baud_rate_ > 0) {
|
||||
switch (this->uart_) {
|
||||
case UART_SELECTION_UART0:
|
||||
case UART_SELECTION_UART0_SWAP:
|
||||
this->hw_serial_ = &Serial;
|
||||
Serial.begin(this->baud_rate_);
|
||||
if (this->uart_ == UART_SELECTION_UART0_SWAP) {
|
||||
Serial.swap();
|
||||
}
|
||||
Serial.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE);
|
||||
break;
|
||||
case UART_SELECTION_UART1:
|
||||
this->hw_serial_ = &Serial1;
|
||||
Serial1.begin(this->baud_rate_);
|
||||
Serial1.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
uart_set_debug(UART_NO);
|
||||
}
|
||||
|
||||
global_logger = this;
|
||||
|
||||
ESP_LOGI(TAG, "Log initialized");
|
||||
}
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
|
||||
|
||||
const LogString *Logger::get_uart_selection_() {
|
||||
switch (this->uart_) {
|
||||
case UART_SELECTION_UART0:
|
||||
return LOG_STR("UART0");
|
||||
case UART_SELECTION_UART1:
|
||||
return LOG_STR("UART1");
|
||||
case UART_SELECTION_UART0_SWAP:
|
||||
default:
|
||||
return LOG_STR("UART0_SWAP");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::logger
|
||||
#endif
|
||||
@@ -1,22 +0,0 @@
|
||||
#if defined(USE_HOST)
|
||||
#include "logger.h"
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg) {
|
||||
time_t rawtime;
|
||||
struct tm *timeinfo;
|
||||
char buffer[80];
|
||||
|
||||
time(&rawtime);
|
||||
timeinfo = localtime(&rawtime);
|
||||
strftime(buffer, sizeof buffer, "[%H:%M:%S]", timeinfo);
|
||||
fputs(buffer, stdout);
|
||||
puts(msg);
|
||||
}
|
||||
|
||||
void Logger::pre_setup() { global_logger = this; }
|
||||
|
||||
} // namespace esphome::logger
|
||||
|
||||
#endif
|
||||
@@ -1,70 +0,0 @@
|
||||
#ifdef USE_LIBRETINY
|
||||
#include "logger.h"
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
static const char *const TAG = "logger";
|
||||
|
||||
void Logger::pre_setup() {
|
||||
if (this->baud_rate_ > 0) {
|
||||
switch (this->uart_) {
|
||||
#if LT_HW_UART0
|
||||
case UART_SELECTION_UART0:
|
||||
this->hw_serial_ = &Serial0;
|
||||
Serial0.begin(this->baud_rate_);
|
||||
break;
|
||||
#endif
|
||||
#if LT_HW_UART1
|
||||
case UART_SELECTION_UART1:
|
||||
this->hw_serial_ = &Serial1;
|
||||
Serial1.begin(this->baud_rate_);
|
||||
break;
|
||||
#endif
|
||||
#if LT_HW_UART2
|
||||
case UART_SELECTION_UART2:
|
||||
this->hw_serial_ = &Serial2;
|
||||
Serial2.begin(this->baud_rate_);
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
this->hw_serial_ = &Serial;
|
||||
Serial.begin(this->baud_rate_);
|
||||
if (this->uart_ != UART_SELECTION_DEFAULT) {
|
||||
ESP_LOGW(TAG, " The chosen logger UART port is not available on this board."
|
||||
"The default port was used instead.");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// change lt_log() port to match default Serial
|
||||
if (this->uart_ == UART_SELECTION_DEFAULT) {
|
||||
this->uart_ = (UARTSelection) (LT_UART_DEFAULT_SERIAL + 1);
|
||||
lt_log_set_port(LT_UART_DEFAULT_SERIAL);
|
||||
} else {
|
||||
lt_log_set_port(this->uart_ - 1);
|
||||
}
|
||||
}
|
||||
|
||||
global_logger = this;
|
||||
ESP_LOGI(TAG, "Log initialized");
|
||||
}
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
|
||||
|
||||
const LogString *Logger::get_uart_selection_() {
|
||||
switch (this->uart_) {
|
||||
case UART_SELECTION_DEFAULT:
|
||||
return LOG_STR("DEFAULT");
|
||||
case UART_SELECTION_UART0:
|
||||
return LOG_STR("UART0");
|
||||
case UART_SELECTION_UART1:
|
||||
return LOG_STR("UART1");
|
||||
case UART_SELECTION_UART2:
|
||||
default:
|
||||
return LOG_STR("UART2");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::logger
|
||||
|
||||
#endif // USE_LIBRETINY
|
||||
@@ -1,48 +0,0 @@
|
||||
#ifdef USE_RP2040
|
||||
#include "logger.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
static const char *const TAG = "logger";
|
||||
|
||||
void Logger::pre_setup() {
|
||||
if (this->baud_rate_ > 0) {
|
||||
switch (this->uart_) {
|
||||
case UART_SELECTION_UART0:
|
||||
this->hw_serial_ = &Serial1;
|
||||
Serial1.begin(this->baud_rate_);
|
||||
break;
|
||||
case UART_SELECTION_UART1:
|
||||
this->hw_serial_ = &Serial2;
|
||||
Serial2.begin(this->baud_rate_);
|
||||
break;
|
||||
case UART_SELECTION_USB_CDC:
|
||||
this->hw_serial_ = &Serial;
|
||||
Serial.begin(this->baud_rate_);
|
||||
break;
|
||||
}
|
||||
}
|
||||
global_logger = this;
|
||||
ESP_LOGI(TAG, "Log initialized");
|
||||
}
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
|
||||
|
||||
const LogString *Logger::get_uart_selection_() {
|
||||
switch (this->uart_) {
|
||||
case UART_SELECTION_UART0:
|
||||
return LOG_STR("UART0");
|
||||
case UART_SELECTION_UART1:
|
||||
return LOG_STR("UART1");
|
||||
#ifdef USE_LOGGER_USB_CDC
|
||||
case UART_SELECTION_USB_CDC:
|
||||
return LOG_STR("USB_CDC");
|
||||
#endif
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::logger
|
||||
#endif // USE_RP2040
|
||||
@@ -1,96 +0,0 @@
|
||||
#ifdef USE_ZEPHYR
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "logger.h"
|
||||
|
||||
#include <zephyr/device.h>
|
||||
#include <zephyr/drivers/uart.h>
|
||||
#include <zephyr/usb/usb_device.h>
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
static const char *const TAG = "logger";
|
||||
|
||||
#ifdef USE_LOGGER_USB_CDC
|
||||
void Logger::loop() {
|
||||
if (this->uart_ != UART_SELECTION_USB_CDC || nullptr == this->uart_dev_) {
|
||||
return;
|
||||
}
|
||||
static bool opened = false;
|
||||
uint32_t dtr = 0;
|
||||
uart_line_ctrl_get(this->uart_dev_, UART_LINE_CTRL_DTR, &dtr);
|
||||
|
||||
/* Poll if the DTR flag was set, optional */
|
||||
if (opened == dtr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!opened) {
|
||||
App.schedule_dump_config();
|
||||
}
|
||||
opened = !opened;
|
||||
}
|
||||
#endif
|
||||
|
||||
void Logger::pre_setup() {
|
||||
if (this->baud_rate_ > 0) {
|
||||
static const struct device *uart_dev = nullptr;
|
||||
switch (this->uart_) {
|
||||
case UART_SELECTION_UART0:
|
||||
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(uart0));
|
||||
break;
|
||||
case UART_SELECTION_UART1:
|
||||
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(uart1));
|
||||
break;
|
||||
#ifdef USE_LOGGER_USB_CDC
|
||||
case UART_SELECTION_USB_CDC:
|
||||
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(cdc_acm_uart0));
|
||||
if (device_is_ready(uart_dev)) {
|
||||
usb_enable(nullptr);
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
}
|
||||
if (!device_is_ready(uart_dev)) {
|
||||
ESP_LOGE(TAG, "%s is not ready.", LOG_STR_ARG(get_uart_selection_()));
|
||||
} else {
|
||||
this->uart_dev_ = uart_dev;
|
||||
}
|
||||
}
|
||||
global_logger = this;
|
||||
ESP_LOGI(TAG, "Log initialized");
|
||||
}
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg) {
|
||||
#ifdef CONFIG_PRINTK
|
||||
printk("%s\n", msg);
|
||||
#endif
|
||||
if (nullptr == this->uart_dev_) {
|
||||
return;
|
||||
}
|
||||
while (*msg) {
|
||||
uart_poll_out(this->uart_dev_, *msg);
|
||||
++msg;
|
||||
}
|
||||
uart_poll_out(this->uart_dev_, '\n');
|
||||
}
|
||||
|
||||
const LogString *Logger::get_uart_selection_() {
|
||||
switch (this->uart_) {
|
||||
case UART_SELECTION_UART0:
|
||||
return LOG_STR("UART0");
|
||||
case UART_SELECTION_UART1:
|
||||
return LOG_STR("UART1");
|
||||
#ifdef USE_LOGGER_USB_CDC
|
||||
case UART_SELECTION_USB_CDC:
|
||||
return LOG_STR("USB_CDC");
|
||||
#endif
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::logger
|
||||
|
||||
#endif
|
||||
@@ -21,7 +21,7 @@ class LVGLNumber : public number::Number, public Component {
|
||||
void setup() override {
|
||||
float value = this->value_lambda_();
|
||||
if (this->restore_) {
|
||||
this->pref_ = global_preferences->make_preference<float>(this->get_preference_hash());
|
||||
this->pref_ = this->make_entity_preference<float>();
|
||||
if (this->pref_.load(&value)) {
|
||||
this->control_lambda_(value);
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ class LVGLSelect : public select::Select, public Component {
|
||||
this->set_options_();
|
||||
if (this->restore_) {
|
||||
size_t index;
|
||||
this->pref_ = global_preferences->make_preference<size_t>(this->get_preference_hash());
|
||||
this->pref_ = this->make_entity_preference<size_t>();
|
||||
if (this->pref_.load(&index))
|
||||
this->widget_->set_selected_index(index, LV_ANIM_OFF);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ class LabelType(WidgetType):
|
||||
|
||||
async def to_code(self, w: Widget, config):
|
||||
"""For a text object, create and set text"""
|
||||
if value := config.get(CONF_TEXT):
|
||||
if (value := config.get(CONF_TEXT)) is not None:
|
||||
await w.set_property(CONF_TEXT, await lv_text.process(value))
|
||||
await w.set_property(CONF_LONG_MODE, config)
|
||||
await w.set_property(CONF_RECOLOR, config)
|
||||
|
||||
@@ -56,7 +56,7 @@ void MCP23016::pin_mode(uint8_t pin, gpio::Flags flags) {
|
||||
this->update_reg_(pin, false, iodir);
|
||||
}
|
||||
}
|
||||
float MCP23016::get_setup_priority() const { return setup_priority::IO; }
|
||||
float MCP23016::get_setup_priority() const { return setup_priority::HARDWARE; }
|
||||
bool MCP23016::read_reg_(uint8_t reg, uint8_t *value) {
|
||||
if (this->is_failed())
|
||||
return false;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user