Merge pull request #14062 from esphome/bump-2026.2.0

2026.2.0
This commit is contained in:
Jesse Hills
2026-02-19 11:36:00 +13:00
committed by GitHub
1013 changed files with 23001 additions and 10077 deletions
+1 -1
View File
@@ -1 +1 @@
d272a88e8ca28ae9340a9a03295a566432a52cb696501908f57764475bf7ca65 ce05c28e9dc0b12c4f6e7454986ffea5123ac974a949da841be698c535f2083e
+96
View File
@@ -0,0 +1,96 @@
---
name: pr-workflow
description: Create pull requests for esphome. Use when creating PRs, submitting changes, or preparing contributions.
allowed-tools: Read, Bash, Glob, Grep
---
# ESPHome PR Workflow
When creating a pull request for esphome, follow these steps:
## 1. Create Branch from Upstream
Always base your branch on **upstream** (not origin/fork) to ensure you have the latest code:
```bash
git fetch upstream
git checkout -b <branch-name> upstream/dev
```
## 2. Read the PR Template
Before creating a PR, read `.github/PULL_REQUEST_TEMPLATE.md` to understand required fields.
## 3. Create the PR
Use `gh pr create` with the **full template** filled in. Never skip or abbreviate sections.
Required fields:
- **What does this implement/fix?**: Brief description of changes
- **Types of changes**: Check ONE appropriate box (Bugfix, New feature, Breaking change, etc.)
- **Related issue**: Use `fixes <link>` syntax if applicable
- **Pull request in esphome-docs**: Link if docs are needed
- **Test Environment**: Check platforms you tested on
- **Example config.yaml**: Include working example YAML
- **Checklist**: Verify code is tested and tests added
## 4. Example PR Body
```markdown
# What does this implement/fix?
<describe your changes here>
## Types of changes
- [ ] Bugfix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Developer breaking change (an API change that could break external components)
- [ ] Code quality improvements to existing code or addition of tests
- [ ] Other
**Related issue or feature (if applicable):**
- fixes https://github.com/esphome/esphome/issues/XXX
**Pull request in [esphome-docs](https://github.com/esphome/esphome-docs) with documentation (if applicable):**
- esphome/esphome-docs#XXX
## Test Environment
- [x] ESP32
- [x] ESP32 IDF
- [ ] ESP8266
- [ ] RP2040
- [ ] BK72xx
- [ ] RTL87xx
- [ ] LN882x
- [ ] nRF52840
## Example entry for `config.yaml`:
```yaml
# Example config.yaml
component_name:
id: my_component
option: value
```
## Checklist:
- [x] The code change is tested and works locally.
- [x] Tests have been added to verify that the new code works (under `tests/` folder).
If user exposed functionality or configuration variables are added/changed:
- [ ] Documentation added/updated in [esphome-docs](https://github.com/esphome/esphome-docs).
```
## 5. Push and Create PR
```bash
git push -u origin <branch-name>
gh pr create --repo esphome/esphome --base dev --title "[component] Brief description"
```
Title should be prefixed with the component name in brackets, e.g. `[safe_mode] Add feature`.
+2 -2
View File
@@ -47,7 +47,7 @@ runs:
- name: Build and push to ghcr by digest - name: Build and push to ghcr by digest
id: build-ghcr id: build-ghcr
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 uses: docker/build-push-action@601a80b39c9405e50806ae38af30926f9d957c47 # v6.19.1
env: env:
DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false DOCKER_BUILD_RECORD_UPLOAD: false
@@ -73,7 +73,7 @@ runs:
- name: Build and push to dockerhub by digest - name: Build and push to dockerhub by digest
id: build-dockerhub id: build-dockerhub
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 uses: docker/build-push-action@601a80b39c9405e50806ae38af30926f9d957c47 # v6.19.1
env: env:
DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false DOCKER_BUILD_RECORD_UPLOAD: false
+2 -2
View File
@@ -17,12 +17,12 @@ runs:
steps: steps:
- name: Set up Python ${{ inputs.python-version }} - name: Set up Python ${{ inputs.python-version }}
id: python id: python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with: with:
python-version: ${{ inputs.python-version }} python-version: ${{ inputs.python-version }}
- name: Restore Python virtual environment - name: Restore Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with: with:
path: venv path: venv
# yamllint disable-line rule:line-length # yamllint disable-line rule:line-length
@@ -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
View 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
View 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
View 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
View 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
};
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -21,9 +21,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Python - name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with: with:
python-version: "3.11" python-version: "3.11"
+2 -2
View File
@@ -21,10 +21,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Python - name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with: with:
python-version: "3.11" python-version: "3.11"
+2 -2
View File
@@ -43,9 +43,9 @@ jobs:
- "docker" - "docker"
# - "lint" # - "lint"
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Python - name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with: with:
python-version: "3.11" python-version: "3.11"
- name: Set up Docker Buildx - name: Set up Docker Buildx
@@ -49,7 +49,7 @@ jobs:
- name: Check out code from base repository - name: Check out code from base repository
if: steps.pr.outputs.skip != 'true' if: steps.pr.outputs.skip != 'true'
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
# Always check out from the base repository (esphome/esphome), never from forks # Always check out from the base repository (esphome/esphome), never from forks
# Use the PR's target branch to ensure we run trusted code from the main repo # Use the PR's target branch to ensure we run trusted code from the main repo
+33 -32
View File
@@ -36,18 +36,18 @@ jobs:
cache-key: ${{ steps.cache-key.outputs.key }} cache-key: ${{ steps.cache-key.outputs.key }}
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Generate cache-key - name: Generate cache-key
id: cache-key id: cache-key
run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment - name: Restore Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with: with:
path: venv path: venv
# yamllint disable-line rule:line-length # yamllint disable-line rule:line-length
@@ -70,7 +70,7 @@ jobs:
if: needs.determine-jobs.outputs.python-linters == 'true' if: needs.determine-jobs.outputs.python-linters == 'true'
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Restore Python - name: Restore Python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
with: with:
@@ -91,7 +91,7 @@ jobs:
- common - common
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Restore Python - name: Restore Python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
with: with:
@@ -115,6 +115,7 @@ jobs:
python-version: python-version:
- "3.11" - "3.11"
- "3.13" - "3.13"
- "3.14"
os: os:
- ubuntu-latest - ubuntu-latest
- macOS-latest - macOS-latest
@@ -132,7 +133,7 @@ jobs:
- common - common
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Restore Python - name: Restore Python
id: restore-python id: restore-python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
@@ -157,7 +158,7 @@ jobs:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
- name: Save Python virtual environment cache - name: Save Python virtual environment cache
if: github.ref == 'refs/heads/dev' if: github.ref == 'refs/heads/dev'
uses: actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with: with:
path: venv path: venv
key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }} key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
@@ -183,7 +184,7 @@ jobs:
component-test-batches: ${{ steps.determine.outputs.component-test-batches }} component-test-batches: ${{ steps.determine.outputs.component-test-batches }}
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
# Fetch enough history to find the merge base # Fetch enough history to find the merge base
fetch-depth: 2 fetch-depth: 2
@@ -193,7 +194,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }} cache-key: ${{ needs.common.outputs.cache-key }}
- name: Restore components graph cache - name: Restore components graph cache
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with: with:
path: .temp/components_graph.json path: .temp/components_graph.json
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }} key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
@@ -223,7 +224,7 @@ jobs:
echo "component-test-batches=$(echo "$output" | jq -c '.component_test_batches')" >> $GITHUB_OUTPUT echo "component-test-batches=$(echo "$output" | jq -c '.component_test_batches')" >> $GITHUB_OUTPUT
- name: Save components graph cache - name: Save components graph cache
if: github.ref == 'refs/heads/dev' if: github.ref == 'refs/heads/dev'
uses: actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with: with:
path: .temp/components_graph.json path: .temp/components_graph.json
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }} key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
@@ -237,15 +238,15 @@ jobs:
if: needs.determine-jobs.outputs.integration-tests == 'true' if: needs.determine-jobs.outputs.integration-tests == 'true'
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Python 3.13 - name: Set up Python 3.13
id: python id: python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with: with:
python-version: "3.13" python-version: "3.13"
- name: Restore Python virtual environment - name: Restore Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with: with:
path: venv path: venv
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }} key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
@@ -273,7 +274,7 @@ jobs:
if: github.event_name == 'pull_request' && (needs.determine-jobs.outputs.cpp-unit-tests-run-all == 'true' || needs.determine-jobs.outputs.cpp-unit-tests-components != '[]') if: github.event_name == 'pull_request' && (needs.determine-jobs.outputs.cpp-unit-tests-run-all == 'true' || needs.determine-jobs.outputs.cpp-unit-tests-components != '[]')
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Restore Python - name: Restore Python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
@@ -321,7 +322,7 @@ jobs:
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
# Need history for HEAD~1 to work for checking changed files # Need history for HEAD~1 to work for checking changed files
fetch-depth: 2 fetch-depth: 2
@@ -334,14 +335,14 @@ jobs:
- name: Cache platformio - name: Cache platformio
if: github.ref == 'refs/heads/dev' if: github.ref == 'refs/heads/dev'
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with: with:
path: ~/.platformio path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }} key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
- name: Cache platformio - name: Cache platformio
if: github.ref != 'refs/heads/dev' if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with: with:
path: ~/.platformio path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }} key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
@@ -400,7 +401,7 @@ jobs:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
# Need history for HEAD~1 to work for checking changed files # Need history for HEAD~1 to work for checking changed files
fetch-depth: 2 fetch-depth: 2
@@ -413,14 +414,14 @@ jobs:
- name: Cache platformio - name: Cache platformio
if: github.ref == 'refs/heads/dev' if: github.ref == 'refs/heads/dev'
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with: with:
path: ~/.platformio path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }} key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
- name: Cache platformio - name: Cache platformio
if: github.ref != 'refs/heads/dev' if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with: with:
path: ~/.platformio path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }} key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
@@ -489,7 +490,7 @@ jobs:
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
# Need history for HEAD~1 to work for checking changed files # Need history for HEAD~1 to work for checking changed files
fetch-depth: 2 fetch-depth: 2
@@ -502,14 +503,14 @@ jobs:
- name: Cache platformio - name: Cache platformio
if: github.ref == 'refs/heads/dev' if: github.ref == 'refs/heads/dev'
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with: with:
path: ~/.platformio path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }} key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
- name: Cache platformio - name: Cache platformio
if: github.ref != 'refs/heads/dev' if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with: with:
path: ~/.platformio path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }} key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
@@ -577,7 +578,7 @@ jobs:
version: 1.0 version: 1.0
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Restore Python - name: Restore Python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
with: with:
@@ -662,7 +663,7 @@ jobs:
if: github.event_name == 'pull_request' && !startsWith(github.base_ref, 'beta') && !startsWith(github.base_ref, 'release') if: github.event_name == 'pull_request' && !startsWith(github.base_ref, 'beta') && !startsWith(github.base_ref, 'release')
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Restore Python - name: Restore Python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
with: with:
@@ -688,7 +689,7 @@ jobs:
skip: ${{ steps.check-script.outputs.skip }} skip: ${{ steps.check-script.outputs.skip }}
steps: steps:
- name: Check out target branch - name: Check out target branch
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
ref: ${{ github.base_ref }} ref: ${{ github.base_ref }}
@@ -735,7 +736,7 @@ jobs:
- name: Restore cached memory analysis - name: Restore cached memory analysis
id: cache-memory-analysis id: cache-memory-analysis
if: steps.check-script.outputs.skip != 'true' if: steps.check-script.outputs.skip != 'true'
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with: with:
path: memory-analysis-target.json path: memory-analysis-target.json
key: ${{ steps.cache-key.outputs.cache-key }} key: ${{ steps.cache-key.outputs.cache-key }}
@@ -759,7 +760,7 @@ jobs:
- name: Cache platformio - name: Cache platformio
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with: with:
path: ~/.platformio path: ~/.platformio
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }} key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
@@ -800,7 +801,7 @@ jobs:
- name: Save memory analysis to cache - name: Save memory analysis to cache
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success' if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success'
uses: actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with: with:
path: memory-analysis-target.json path: memory-analysis-target.json
key: ${{ steps.cache-key.outputs.cache-key }} key: ${{ steps.cache-key.outputs.cache-key }}
@@ -840,14 +841,14 @@ jobs:
flash_usage: ${{ steps.extract.outputs.flash_usage }} flash_usage: ${{ steps.extract.outputs.flash_usage }}
steps: steps:
- name: Check out PR branch - name: Check out PR branch
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Restore Python - name: Restore Python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }} cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache platformio - name: Cache platformio
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with: with:
path: ~/.platformio path: ~/.platformio
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }} key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
@@ -908,7 +909,7 @@ jobs:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}
steps: steps:
- name: Check out code - name: Check out code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Restore Python - name: Restore Python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
with: with:
+3 -3
View File
@@ -54,11 +54,11 @@ jobs:
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10 uses: github/codeql-action/init@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }} build-mode: ${{ matrix.build-mode }}
@@ -86,6 +86,6 @@ jobs:
exit 1 exit 1
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10 uses: github/codeql-action/analyze@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
with: with:
category: "/language:${{matrix.language}}" category: "/language:${{matrix.language}}"
+10 -10
View File
@@ -20,7 +20,7 @@ jobs:
branch_build: ${{ steps.tag.outputs.branch_build }} branch_build: ${{ steps.tag.outputs.branch_build }}
deploy_env: ${{ steps.tag.outputs.deploy_env }} deploy_env: ${{ steps.tag.outputs.deploy_env }}
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Get tag - name: Get tag
id: tag id: tag
# yamllint disable rule:line-length # yamllint disable rule:line-length
@@ -60,9 +60,9 @@ jobs:
contents: read contents: read
id-token: write id-token: write
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Python - name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with: with:
python-version: "3.x" python-version: "3.x"
- name: Build - name: Build
@@ -92,9 +92,9 @@ jobs:
os: "ubuntu-24.04-arm" os: "ubuntu-24.04-arm"
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Python - name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with: with:
python-version: "3.11" python-version: "3.11"
@@ -102,12 +102,12 @@ jobs:
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Log in to docker hub - name: Log in to docker hub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with: with:
username: ${{ secrets.DOCKER_USER }} username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the GitHub container registry - name: Log in to the GitHub container registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
@@ -168,7 +168,7 @@ jobs:
- ghcr - ghcr
- dockerhub - dockerhub
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Download digests - name: Download digests
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
@@ -182,13 +182,13 @@ jobs:
- name: Log in to docker hub - name: Log in to docker hub
if: matrix.registry == 'dockerhub' if: matrix.registry == 'dockerhub'
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with: with:
username: ${{ secrets.DOCKER_USER }} username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the GitHub container registry - name: Log in to the GitHub container registry
if: matrix.registry == 'ghcr' if: matrix.registry == 'ghcr'
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
+4 -4
View File
@@ -13,16 +13,16 @@ jobs:
if: github.repository == 'esphome/esphome' if: github.repository == 'esphome/esphome'
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Checkout Home Assistant - name: Checkout Home Assistant
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
repository: home-assistant/core repository: home-assistant/core
path: lib/home-assistant path: lib/home-assistant
- name: Setup Python - name: Setup Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with: with:
python-version: 3.13 python-version: 3.13
@@ -41,7 +41,7 @@ jobs:
python script/run-in-env.py pre-commit run --all-files python script/run-in-env.py pre-commit run --all-files
- name: Commit changes - name: Commit changes
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0 uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with: with:
commit-message: "Synchronise Device Classes from Home Assistant" commit-message: "Synchronise Device Classes from Home Assistant"
committer: esphomebot <esphome@openhomefoundation.org> committer: esphomebot <esphome@openhomefoundation.org>
+1 -1
View File
@@ -11,7 +11,7 @@ ci:
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: v0.14.11 rev: v0.15.0
hooks: hooks:
# Run the linter. # Run the linter.
- id: ruff - id: ruff
+5 -1
View File
@@ -88,7 +88,8 @@ esphome/components/bmp3xx/* @latonita
esphome/components/bmp3xx_base/* @latonita @martgras esphome/components/bmp3xx_base/* @latonita @martgras
esphome/components/bmp3xx_i2c/* @latonita esphome/components/bmp3xx_i2c/* @latonita
esphome/components/bmp3xx_spi/* @latonita esphome/components/bmp3xx_spi/* @latonita
esphome/components/bmp581/* @kahrendt esphome/components/bmp581_base/* @danielkent-net @kahrendt
esphome/components/bmp581_i2c/* @danielkent-net @kahrendt
esphome/components/bp1658cj/* @Cossid esphome/components/bp1658cj/* @Cossid
esphome/components/bp5758d/* @Cossid esphome/components/bp5758d/* @Cossid
esphome/components/bthome_mithermometer/* @nagyrobi esphome/components/bthome_mithermometer/* @nagyrobi
@@ -103,6 +104,7 @@ esphome/components/cc1101/* @gabest11 @lygris
esphome/components/ccs811/* @habbie esphome/components/ccs811/* @habbie
esphome/components/cd74hc4067/* @asoehlke esphome/components/cd74hc4067/* @asoehlke
esphome/components/ch422g/* @clydebarrow @jesterret esphome/components/ch422g/* @clydebarrow @jesterret
esphome/components/ch423/* @dwmw2
esphome/components/chsc6x/* @kkosik20 esphome/components/chsc6x/* @kkosik20
esphome/components/climate/* @esphome/core esphome/components/climate/* @esphome/core
esphome/components/climate_ir/* @glmnet esphome/components/climate_ir/* @glmnet
@@ -132,6 +134,7 @@ esphome/components/dfplayer/* @glmnet
esphome/components/dfrobot_sen0395/* @niklasweber esphome/components/dfrobot_sen0395/* @niklasweber
esphome/components/dht/* @OttoWinter esphome/components/dht/* @OttoWinter
esphome/components/display_menu_base/* @numo68 esphome/components/display_menu_base/* @numo68
esphome/components/dlms_meter/* @SimonFischer04
esphome/components/dps310/* @kbx81 esphome/components/dps310/* @kbx81
esphome/components/ds1307/* @badbadc0ffee esphome/components/ds1307/* @badbadc0ffee
esphome/components/ds2484/* @mrk-its esphome/components/ds2484/* @mrk-its
@@ -481,6 +484,7 @@ esphome/components/switch/* @esphome/core
esphome/components/switch/binary_sensor/* @ssieb esphome/components/switch/binary_sensor/* @ssieb
esphome/components/sx126x/* @swoboda1337 esphome/components/sx126x/* @swoboda1337
esphome/components/sx127x/* @swoboda1337 esphome/components/sx127x/* @swoboda1337
esphome/components/sy6970/* @linkedupbits
esphome/components/syslog/* @clydebarrow esphome/components/syslog/* @clydebarrow
esphome/components/t6615/* @tylermenezes esphome/components/t6615/* @tylermenezes
esphome/components/tc74/* @sethgirvan esphome/components/tc74/* @sethgirvan
+1 -1
View File
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version # could be handy for archiving the generated documentation or if some version
# control system is used. # control system is used.
PROJECT_NUMBER = 2026.1.5 PROJECT_NUMBER = 2026.2.0
# Using the PROJECT_BRIEF tag one can provide an optional one line description # Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a # for a project that appears at the top of each page and should give viewer a
+3 -2
View File
@@ -9,7 +9,8 @@ FROM ghcr.io/esphome/docker-base:${BUILD_OS}-ha-addon-${BUILD_BASE_VERSION} AS b
ARG BUILD_TYPE ARG BUILD_TYPE
FROM base-source-${BUILD_TYPE} AS base FROM base-source-${BUILD_TYPE} AS base
RUN git config --system --add safe.directory "*" RUN git config --system --add safe.directory "*" \
&& git config --system advice.detachedHead false
# Install build tools for Python packages that require compilation # Install build tools for Python packages that require compilation
# (e.g., ruamel.yaml.clibz used by ESP-IDF's idf-component-manager) # (e.g., ruamel.yaml.clibz used by ESP-IDF's idf-component-manager)
@@ -23,7 +24,7 @@ RUN if command -v apk > /dev/null; then \
ENV PIP_DISABLE_PIP_VERSION_CHECK=1 ENV PIP_DISABLE_PIP_VERSION_CHECK=1
RUN pip install --no-cache-dir -U pip uv==0.6.14 RUN pip install --no-cache-dir -U pip uv==0.10.1
COPY requirements.txt / COPY requirements.txt /
+58 -14
View File
@@ -43,6 +43,7 @@ from esphome.const import (
CONF_SUBSTITUTIONS, CONF_SUBSTITUTIONS,
CONF_TOPIC, CONF_TOPIC,
ENV_NOGITIGNORE, ENV_NOGITIGNORE,
KEY_NATIVE_IDF,
PLATFORM_ESP32, PLATFORM_ESP32,
PLATFORM_ESP8266, PLATFORM_ESP8266,
PLATFORM_RP2040, PLATFORM_RP2040,
@@ -116,6 +117,7 @@ class ArgsProtocol(Protocol):
configuration: str configuration: str
name: str name: str
upload_speed: str | None upload_speed: str | None
native_idf: bool
def choose_prompt(options, purpose: str = None): def choose_prompt(options, purpose: str = None):
@@ -223,8 +225,13 @@ def choose_upload_log_host(
else: else:
resolved.append(device) resolved.append(device)
if not resolved: if not resolved:
if CORE.dashboard:
hint = "If you know the IP, set 'use_address' in your network config."
else:
hint = "If you know the IP, try --device <IP>"
raise EsphomeError( raise EsphomeError(
f"All specified devices {defaults} could not be resolved. Is the device connected to the network?" f"All specified devices {defaults} could not be resolved. "
f"Is the device connected to the network? {hint}"
) )
return resolved return resolved
@@ -500,12 +507,15 @@ def wrap_to_code(name, comp):
return wrapped return wrapped
def write_cpp(config: ConfigType) -> int: def write_cpp(config: ConfigType, native_idf: bool = False) -> int:
if not get_bool_env(ENV_NOGITIGNORE): if not get_bool_env(ENV_NOGITIGNORE):
writer.write_gitignore() writer.write_gitignore()
# Store native_idf flag so esp32 component can check it
CORE.data[KEY_NATIVE_IDF] = native_idf
generate_cpp_contents(config) generate_cpp_contents(config)
return write_cpp_file() return write_cpp_file(native_idf=native_idf)
def generate_cpp_contents(config: ConfigType) -> None: def generate_cpp_contents(config: ConfigType) -> None:
@@ -519,32 +529,54 @@ def generate_cpp_contents(config: ConfigType) -> None:
CORE.flush_tasks() CORE.flush_tasks()
def write_cpp_file() -> int: def write_cpp_file(native_idf: bool = False) -> int:
code_s = indent(CORE.cpp_main_section) code_s = indent(CORE.cpp_main_section)
writer.write_cpp(code_s) writer.write_cpp(code_s)
from esphome.build_gen import platformio if native_idf and CORE.is_esp32 and CORE.target_framework == "esp-idf":
from esphome.build_gen import espidf
platformio.write_project() espidf.write_project()
else:
from esphome.build_gen import platformio
platformio.write_project()
return 0 return 0
def compile_program(args: ArgsProtocol, config: ConfigType) -> int: def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
from esphome import platformio_api native_idf = getattr(args, "native_idf", False)
# NOTE: "Build path:" format is parsed by script/ci_memory_impact_extract.py # NOTE: "Build path:" format is parsed by script/ci_memory_impact_extract.py
# If you change this format, update the regex in that script as well # If you change this format, update the regex in that script as well
_LOGGER.info("Compiling app... Build path: %s", CORE.build_path) _LOGGER.info("Compiling app... Build path: %s", CORE.build_path)
rc = platformio_api.run_compile(config, CORE.verbose)
if rc != 0: if native_idf and CORE.is_esp32 and CORE.target_framework == "esp-idf":
return rc from esphome import espidf_api
rc = espidf_api.run_compile(config, CORE.verbose)
if rc != 0:
return rc
# Create factory.bin and ota.bin
espidf_api.create_factory_bin()
espidf_api.create_ota_bin()
else:
from esphome import platformio_api
rc = platformio_api.run_compile(config, CORE.verbose)
if rc != 0:
return rc
idedata = platformio_api.get_idedata(config)
if idedata is None:
return 1
# Check if firmware was rebuilt and emit build_info + create manifest # Check if firmware was rebuilt and emit build_info + create manifest
_check_and_emit_build_info() _check_and_emit_build_info()
idedata = platformio_api.get_idedata(config) return 0
return 0 if idedata is not None else 1
def _check_and_emit_build_info() -> None: def _check_and_emit_build_info() -> None:
@@ -801,7 +833,8 @@ def command_vscode(args: ArgsProtocol) -> int | None:
def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None: def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None:
exit_code = write_cpp(config) native_idf = getattr(args, "native_idf", False)
exit_code = write_cpp(config, native_idf=native_idf)
if exit_code != 0: if exit_code != 0:
return exit_code return exit_code
if args.only_generate: if args.only_generate:
@@ -856,7 +889,8 @@ def command_logs(args: ArgsProtocol, config: ConfigType) -> int | None:
def command_run(args: ArgsProtocol, config: ConfigType) -> int | None: def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
exit_code = write_cpp(config) native_idf = getattr(args, "native_idf", False)
exit_code = write_cpp(config, native_idf=native_idf)
if exit_code != 0: if exit_code != 0:
return exit_code return exit_code
exit_code = compile_program(args, config) exit_code = compile_program(args, config)
@@ -1310,6 +1344,11 @@ def parse_args(argv):
help="Only generate source code, do not compile.", help="Only generate source code, do not compile.",
action="store_true", action="store_true",
) )
parser_compile.add_argument(
"--native-idf",
help="Build with native ESP-IDF instead of PlatformIO (ESP32 esp-idf framework only).",
action="store_true",
)
parser_upload = subparsers.add_parser( parser_upload = subparsers.add_parser(
"upload", "upload",
@@ -1391,6 +1430,11 @@ def parse_args(argv):
help="Reset the device before starting serial logs.", help="Reset the device before starting serial logs.",
default=os.getenv("ESPHOME_SERIAL_LOGGING_RESET"), default=os.getenv("ESPHOME_SERIAL_LOGGING_RESET"),
) )
parser_run.add_argument(
"--native-idf",
help="Build with native ESP-IDF instead of PlatformIO (ESP32 esp-idf framework only).",
action="store_true",
)
parser_clean = subparsers.add_parser( parser_clean = subparsers.add_parser(
"clean-mqtt", "clean-mqtt",
File diff suppressed because it is too large Load Diff
+103 -1
View File
@@ -4,6 +4,8 @@ from __future__ import annotations
from collections import defaultdict from collections import defaultdict
from collections.abc import Callable from collections.abc import Callable
import heapq
from operator import itemgetter
import sys import sys
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@@ -12,6 +14,7 @@ from . import (
_COMPONENT_CORE, _COMPONENT_CORE,
_COMPONENT_PREFIX_ESPHOME, _COMPONENT_PREFIX_ESPHOME,
_COMPONENT_PREFIX_EXTERNAL, _COMPONENT_PREFIX_EXTERNAL,
_COMPONENT_PREFIX_LIB,
RAM_SECTIONS, RAM_SECTIONS,
MemoryAnalyzer, MemoryAnalyzer,
) )
@@ -29,6 +32,10 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
) )
# Lower threshold for RAM symbols (RAM is more constrained) # Lower threshold for RAM symbols (RAM is more constrained)
RAM_SYMBOL_SIZE_THRESHOLD: int = 24 RAM_SYMBOL_SIZE_THRESHOLD: int = 24
# Number of top symbols to show in the largest symbols report
TOP_SYMBOLS_LIMIT: int = 30
# Width for symbol name display in top symbols report
COL_TOP_SYMBOL_NAME: int = 55
# Column width constants # Column width constants
COL_COMPONENT: int = 29 COL_COMPONENT: int = 29
@@ -147,6 +154,83 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
section_label = f" [{section[1:]}]" # .data -> [data], .bss -> [bss] section_label = f" [{section[1:]}]" # .data -> [data], .bss -> [bss]
return f"{demangled} ({size:,} B){section_label}" return f"{demangled} ({size:,} B){section_label}"
def _add_top_symbols(self, lines: list[str]) -> None:
"""Add a section showing the top largest symbols in the binary."""
# Collect all symbols from all components: (symbol, demangled, size, section, component)
all_symbols = [
(symbol, demangled, size, section, component)
for component, symbols in self._component_symbols.items()
for symbol, demangled, size, section in symbols
]
# Get top N symbols by size using heapq for efficiency
top_symbols = heapq.nlargest(
self.TOP_SYMBOLS_LIMIT, all_symbols, key=itemgetter(2)
)
lines.append("")
lines.append(f"Top {self.TOP_SYMBOLS_LIMIT} Largest Symbols:")
# Calculate truncation limit from column width (leaving room for "...")
truncate_limit = self.COL_TOP_SYMBOL_NAME - 3
for i, (_, demangled, size, section, component) in enumerate(top_symbols):
# Format section label
section_label = f"[{section[1:]}]" if section else ""
# Truncate demangled name if too long
demangled_display = (
f"{demangled[:truncate_limit]}..."
if len(demangled) > self.COL_TOP_SYMBOL_NAME
else demangled
)
lines.append(
f"{i + 1:>2}. {size:>7,} B {section_label:<8} {demangled_display:<{self.COL_TOP_SYMBOL_NAME}} {component}"
)
def _add_cswtch_analysis(self, lines: list[str]) -> None:
"""Add CSWTCH (GCC switch table lookup) analysis section."""
self._add_section_header(lines, "CSWTCH Analysis (GCC Switch Table Lookups)")
total_size = sum(size for _, size, _, _ in self._cswtch_symbols)
lines.append(
f"Total: {len(self._cswtch_symbols)} switch table(s), {total_size:,} B"
)
lines.append("")
# Group by component
by_component: dict[str, list[tuple[str, int, str]]] = defaultdict(list)
for sym_name, size, source_file, component in self._cswtch_symbols:
by_component[component].append((sym_name, size, source_file))
# Sort components by total size descending
sorted_components = sorted(
by_component.items(),
key=lambda x: sum(s[1] for s in x[1]),
reverse=True,
)
for component, symbols in sorted_components:
comp_total = sum(s[1] for s in symbols)
lines.append(f"{component} ({comp_total:,} B, {len(symbols)} tables):")
# Group by source file within component
by_file: dict[str, list[tuple[str, int]]] = defaultdict(list)
for sym_name, size, source_file in symbols:
by_file[source_file].append((sym_name, size))
for source_file, file_symbols in sorted(
by_file.items(),
key=lambda x: sum(s[1] for s in x[1]),
reverse=True,
):
file_total = sum(s[1] for s in file_symbols)
lines.append(
f" {source_file} ({file_total:,} B, {len(file_symbols)} tables)"
)
for sym_name, size in sorted(
file_symbols, key=lambda x: x[1], reverse=True
):
lines.append(f" {size:>6,} B {sym_name}")
lines.append("")
def generate_report(self, detailed: bool = False) -> str: def generate_report(self, detailed: bool = False) -> str:
"""Generate a formatted memory report.""" """Generate a formatted memory report."""
components = sorted( components = sorted(
@@ -248,6 +332,9 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
"RAM", "RAM",
) )
# Top largest symbols in the binary
self._add_top_symbols(lines)
# Add ESPHome core detailed analysis if there are core symbols # Add ESPHome core detailed analysis if there are core symbols
if self._esphome_core_symbols: if self._esphome_core_symbols:
self._add_section_header(lines, f"{_COMPONENT_CORE} Detailed Analysis") self._add_section_header(lines, f"{_COMPONENT_CORE} Detailed Analysis")
@@ -321,6 +408,11 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
for name, mem in components for name, mem in components
if name.startswith(_COMPONENT_PREFIX_EXTERNAL) if name.startswith(_COMPONENT_PREFIX_EXTERNAL)
] ]
library_components = [
(name, mem)
for name, mem in components
if name.startswith(_COMPONENT_PREFIX_LIB)
]
top_esphome_components = sorted( top_esphome_components = sorted(
esphome_components, key=lambda x: x[1].flash_total, reverse=True esphome_components, key=lambda x: x[1].flash_total, reverse=True
@@ -331,6 +423,11 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
external_components, key=lambda x: x[1].flash_total, reverse=True external_components, key=lambda x: x[1].flash_total, reverse=True
) )
# Include all library components
top_library_components = sorted(
library_components, key=lambda x: x[1].flash_total, reverse=True
)
# Check if API component exists and ensure it's included # Check if API component exists and ensure it's included
api_component = None api_component = None
for name, mem in components: for name, mem in components:
@@ -349,10 +446,11 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
if name in system_components_to_include if name in system_components_to_include
] ]
# Combine all components to analyze: top ESPHome + all external + API if not already included + system components # Combine all components to analyze: top ESPHome + all external + libraries + API if not already included + system components
components_to_analyze = ( components_to_analyze = (
list(top_esphome_components) list(top_esphome_components)
+ list(top_external_components) + list(top_external_components)
+ list(top_library_components)
+ system_components + system_components
) )
if api_component and api_component not in components_to_analyze: if api_component and api_component not in components_to_analyze:
@@ -431,6 +529,10 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
lines.append(f" ... and {len(large_ram_syms) - 10} more") lines.append(f" ... and {len(large_ram_syms) - 10} more")
lines.append("") lines.append("")
# CSWTCH (GCC switch table) analysis
if self._cswtch_symbols:
self._add_cswtch_analysis(lines)
lines.append( lines.append(
"Note: This analysis covers symbols in the ELF file. Some runtime allocations may not be included." "Note: This analysis covers symbols in the ELF file. Some runtime allocations may not be included."
) )
+57 -14
View File
@@ -9,20 +9,61 @@ ESPHOME_COMPONENT_PATTERN = re.compile(r"esphome::([a-zA-Z0-9_]+)::")
# Maps standard section names to their various platform-specific variants # Maps standard section names to their various platform-specific variants
# Note: Order matters! More specific patterns (.bss) must come before general ones (.dram) # Note: Order matters! More specific patterns (.bss) must come before general ones (.dram)
# because ESP-IDF uses names like ".dram0.bss" which would match ".dram" otherwise # because ESP-IDF uses names like ".dram0.bss" which would match ".dram" otherwise
#
# Platform-specific sections:
# - ESP8266/ESP32: .iram*, .dram*
# - LibreTiny RTL87xx: .xip.code_* (flash), .ram.code_* (RAM)
# - LibreTiny BK7231: .itcm.code (fast RAM), .vectors (interrupt vectors)
# - LibreTiny LN882X: .flash_text, .flash_copy* (flash code)
# - Zephyr/nRF52: text, rodata, datas, bss (no leading dots)
SECTION_MAPPING = { SECTION_MAPPING = {
".text": frozenset([".text", ".iram"]), ".text": frozenset(
".rodata": frozenset([".rodata"]), [
".bss": frozenset([".bss"]), # Must be before .data to catch ".dram0.bss" ".text",
".data": frozenset([".data", ".dram"]), ".iram",
} # LibreTiny RTL87xx XIP (eXecute In Place) flash code
".xip.code",
# Section to ComponentMemory attribute mapping # LibreTiny RTL87xx RAM code
# Maps section names to the attribute name in ComponentMemory dataclass ".ram.code_text",
SECTION_TO_ATTR = { # LibreTiny BK7231 fast RAM code and vectors
".text": "text_size", ".itcm.code",
".rodata": "rodata_size", ".vectors",
".data": "data_size", # LibreTiny LN882X flash code
".bss": "bss_size", ".flash_text",
".flash_copy",
# Zephyr/nRF52 sections (no leading dots)
"text",
"rom_start",
]
),
".rodata": frozenset(
[
".rodata",
# LibreTiny RTL87xx read-only data in RAM
".ram.code_rodata",
# Zephyr/nRF52 sections (no leading dots)
"rodata",
]
),
# .bss patterns - must be before .data to catch ".dram0.bss"
".bss": frozenset(
[
".bss",
# LibreTiny LN882X BSS
".bss_ram",
# Zephyr/nRF52 sections (no leading dots)
"bss",
"noinit",
]
),
".data": frozenset(
[
".data",
".dram",
# Zephyr/nRF52 sections (no leading dots)
"datas",
]
),
} }
# Component identification rules # Component identification rules
@@ -463,7 +504,9 @@ SYMBOL_PATTERNS = {
"__FUNCTION__$", "__FUNCTION__$",
"DAYS_IN_MONTH", "DAYS_IN_MONTH",
"_DAYS_BEFORE_MONTH", "_DAYS_BEFORE_MONTH",
"CSWTCH$", # Note: CSWTCH$ symbols are GCC switch table lookup tables.
# They are attributed to their source object files via _analyze_cswtch_symbols()
# rather than being lumped into libc.
"dst$", "dst$",
"sulp", "sulp",
"_strtol_l", # String to long with locale "_strtol_l", # String to long with locale
+4 -4
View File
@@ -94,13 +94,13 @@ def parse_symbol_line(line: str) -> tuple[str, str, int, str] | None:
return None return None
# Find section, size, and name # Find section, size, and name
# Try each part as a potential section name
for i, part in enumerate(parts): for i, part in enumerate(parts):
if not part.startswith("."): # Skip parts that are clearly flags, addresses, or other metadata
continue # Sections start with '.' (standard ELF) or are known section names (Zephyr)
section = map_section_name(part) section = map_section_name(part)
if not section: if not section:
break continue
# Need at least size field after section # Need at least size field after section
if i + 1 >= len(parts): if i + 1 >= len(parts):
+82 -2
View File
@@ -3,6 +3,7 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
import os
from pathlib import Path from pathlib import Path
import subprocess import subprocess
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@@ -17,10 +18,82 @@ TOOLCHAIN_PREFIXES = [
"xtensa-lx106-elf-", # ESP8266 "xtensa-lx106-elf-", # ESP8266
"xtensa-esp32-elf-", # ESP32 "xtensa-esp32-elf-", # ESP32
"xtensa-esp-elf-", # ESP32 (newer IDF) "xtensa-esp-elf-", # ESP32 (newer IDF)
"arm-zephyr-eabi-", # nRF52/Zephyr SDK
"arm-none-eabi-", # Generic ARM (RP2040, etc.)
"", # System default (no prefix) "", # System default (no prefix)
] ]
def _find_in_platformio_packages(tool_name: str) -> str | None:
"""Search for a tool in PlatformIO package directories.
This handles cases like Zephyr SDK where tools are installed in nested
directories that aren't in PATH.
Args:
tool_name: Name of the tool (e.g., "readelf", "objdump")
Returns:
Full path to the tool or None if not found
"""
# Get PlatformIO packages directory
platformio_home = Path(os.path.expanduser("~/.platformio/packages"))
if not platformio_home.exists():
return None
# Search patterns for toolchains that might contain the tool
# Order matters - more specific patterns first
search_patterns = [
# Zephyr SDK deeply nested structure (4 levels)
# e.g., toolchain-gccarmnoneeabi/zephyr-sdk-0.17.4/arm-zephyr-eabi/bin/arm-zephyr-eabi-objdump
f"toolchain-*/*/*/bin/*-{tool_name}",
# Zephyr SDK nested structure (3 levels)
f"toolchain-*/*/bin/*-{tool_name}",
f"toolchain-*/bin/*-{tool_name}",
# Standard PlatformIO toolchain structure
f"toolchain-*/bin/*{tool_name}",
]
for pattern in search_patterns:
matches = list(platformio_home.glob(pattern))
if matches:
# Sort to get consistent results, prefer arm-zephyr-eabi over arm-none-eabi
matches.sort(key=lambda p: ("zephyr" not in str(p), str(p)))
tool_path = str(matches[0])
_LOGGER.debug("Found %s in PlatformIO packages: %s", tool_name, tool_path)
return tool_path
return None
def resolve_tool_path(
tool_name: str,
derived_path: str | None,
objdump_path: str | None = None,
) -> str | None:
"""Resolve a tool path, falling back to find_tool if derived path doesn't exist.
Args:
tool_name: Name of the tool (e.g., "objdump", "readelf")
derived_path: Path derived from idedata (may not exist for some platforms)
objdump_path: Path to objdump binary to derive other tool paths from
Returns:
Resolved path to the tool, or the original derived_path if it exists
"""
if derived_path and not Path(derived_path).exists():
found = find_tool(tool_name, objdump_path)
if found:
_LOGGER.debug(
"Derived %s path %s not found, using %s",
tool_name,
derived_path,
found,
)
return found
return derived_path
def find_tool( def find_tool(
tool_name: str, tool_name: str,
objdump_path: str | None = None, objdump_path: str | None = None,
@@ -28,7 +101,8 @@ def find_tool(
"""Find a toolchain tool by name. """Find a toolchain tool by name.
First tries to derive the tool path from objdump_path (if provided), First tries to derive the tool path from objdump_path (if provided),
then falls back to searching for platform-specific tools. then searches PlatformIO package directories (for cross-compile toolchains),
and finally falls back to searching for platform-specific tools in PATH.
Args: Args:
tool_name: Name of the tool (e.g., "objdump", "nm", "c++filt") tool_name: Name of the tool (e.g., "objdump", "nm", "c++filt")
@@ -47,7 +121,13 @@ def find_tool(
_LOGGER.debug("Found %s at: %s", tool_name, potential_path) _LOGGER.debug("Found %s at: %s", tool_name, potential_path)
return potential_path return potential_path
# Try platform-specific tools # Search in PlatformIO packages directory first (handles Zephyr SDK, etc.)
# This must come before PATH search because system tools (e.g., /usr/bin/objdump)
# are for the host architecture, not the target (ARM, Xtensa, etc.)
if found := _find_in_platformio_packages(tool_name):
return found
# Try platform-specific tools in PATH (fallback for when tools are installed globally)
for prefix in TOOLCHAIN_PREFIXES: for prefix in TOOLCHAIN_PREFIXES:
cmd = f"{prefix}{tool_name}" cmd = f"{prefix}{tool_name}"
try: try:
+139
View File
@@ -0,0 +1,139 @@
"""ESP-IDF direct build generator for ESPHome."""
import json
from pathlib import Path
from esphome.components.esp32 import get_esp32_variant
from esphome.core import CORE
from esphome.helpers import mkdir_p, write_file_if_changed
def get_available_components() -> list[str] | None:
"""Get list of available ESP-IDF components from project_description.json.
Returns only internal ESP-IDF components, excluding external/managed
components (from idf_component.yml).
"""
project_desc = Path(CORE.build_path) / "build" / "project_description.json"
if not project_desc.exists():
return None
try:
with open(project_desc, encoding="utf-8") as f:
data = json.load(f)
component_info = data.get("build_component_info", {})
result = []
for name, info in component_info.items():
# Exclude our own src component
if name == "src":
continue
# Exclude managed/external components
comp_dir = info.get("dir", "")
if "managed_components" in comp_dir:
continue
result.append(name)
return result
except (json.JSONDecodeError, OSError):
return None
def has_discovered_components() -> bool:
"""Check if we have discovered components from a previous configure."""
return get_available_components() is not None
def get_project_cmakelists() -> str:
"""Generate the top-level CMakeLists.txt for ESP-IDF project."""
# Get IDF target from ESP32 variant (e.g., ESP32S3 -> esp32s3)
variant = get_esp32_variant()
idf_target = variant.lower().replace("-", "")
return f"""\
# Auto-generated by ESPHome
cmake_minimum_required(VERSION 3.16)
set(IDF_TARGET {idf_target})
set(EXTRA_COMPONENT_DIRS ${{CMAKE_SOURCE_DIR}}/src)
include($ENV{{IDF_PATH}}/tools/cmake/project.cmake)
project({CORE.name})
"""
def get_component_cmakelists(minimal: bool = False) -> str:
"""Generate the main component CMakeLists.txt."""
idf_requires = [] if minimal else (get_available_components() or [])
requires_str = " ".join(idf_requires)
# Extract compile definitions from build flags (-DXXX -> XXX)
compile_defs = [flag[2:] for flag in CORE.build_flags if flag.startswith("-D")]
compile_defs_str = "\n ".join(compile_defs) if compile_defs else ""
# Extract compile options (-W flags, excluding linker flags)
compile_opts = [
flag
for flag in CORE.build_flags
if flag.startswith("-W") and not flag.startswith("-Wl,")
]
compile_opts_str = "\n ".join(compile_opts) if compile_opts else ""
# Extract linker options (-Wl, flags)
link_opts = [flag for flag in CORE.build_flags if flag.startswith("-Wl,")]
link_opts_str = "\n ".join(link_opts) if link_opts else ""
return f"""\
# Auto-generated by ESPHome
file(GLOB_RECURSE app_sources
"${{CMAKE_CURRENT_SOURCE_DIR}}/*.cpp"
"${{CMAKE_CURRENT_SOURCE_DIR}}/*.c"
"${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.cpp"
"${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.c"
)
idf_component_register(
SRCS ${{app_sources}}
INCLUDE_DIRS "." "esphome"
REQUIRES {requires_str}
)
# Apply C++ standard
target_compile_features(${{COMPONENT_LIB}} PUBLIC cxx_std_20)
# ESPHome compile definitions
target_compile_definitions(${{COMPONENT_LIB}} PUBLIC
{compile_defs_str}
)
# ESPHome compile options
target_compile_options(${{COMPONENT_LIB}} PUBLIC
{compile_opts_str}
)
# ESPHome linker options
target_link_options(${{COMPONENT_LIB}} PUBLIC
{link_opts_str}
)
"""
def write_project(minimal: bool = False) -> None:
"""Write ESP-IDF project files."""
mkdir_p(CORE.build_path)
mkdir_p(CORE.relative_src_path())
# Write top-level CMakeLists.txt
write_file_if_changed(
CORE.relative_build_path("CMakeLists.txt"),
get_project_cmakelists(),
)
# Write component CMakeLists.txt in src/
write_file_if_changed(
CORE.relative_src_path("CMakeLists.txt"),
get_component_cmakelists(minimal=minimal),
)
+2
View File
@@ -69,6 +69,7 @@ from esphome.cpp_types import ( # noqa: F401
JsonObjectConst, JsonObjectConst,
Parented, Parented,
PollingComponent, PollingComponent,
StringRef,
arduino_json_ns, arduino_json_ns,
bool_, bool_,
const_char_ptr, const_char_ptr,
@@ -86,6 +87,7 @@ from esphome.cpp_types import ( # noqa: F401
size_t, size_t,
std_ns, std_ns,
std_shared_ptr, std_shared_ptr,
std_span,
std_string, std_string,
std_string_ref, std_string_ref,
std_vector, std_vector,
@@ -45,8 +45,6 @@ void AbsoluteHumidityComponent::dump_config() {
this->temperature_sensor_->get_name().c_str(), this->humidity_sensor_->get_name().c_str()); this->temperature_sensor_->get_name().c_str(), this->humidity_sensor_->get_name().c_str());
} }
float AbsoluteHumidityComponent::get_setup_priority() const { return setup_priority::DATA; }
void AbsoluteHumidityComponent::loop() { void AbsoluteHumidityComponent::loop() {
if (!this->next_update_) { if (!this->next_update_) {
return; return;
@@ -24,7 +24,6 @@ class AbsoluteHumidityComponent : public sensor::Sensor, public Component {
void setup() override; void setup() override;
void dump_config() override; void dump_config() override;
float get_setup_priority() const override;
void loop() override; void loop() override;
protected: protected:
-5
View File
@@ -68,11 +68,6 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
/// This method is called during the ESPHome setup process to log the configuration. /// This method is called during the ESPHome setup process to log the configuration.
void dump_config() override; void dump_config() override;
/// Return the setup priority for this component.
/// Components with higher priority are initialized earlier during setup.
/// @return A float representing the setup priority.
float get_setup_priority() const override;
#ifdef USE_ZEPHYR #ifdef USE_ZEPHYR
/// Set the ADC channel to be used by the ADC sensor. /// Set the ADC channel to be used by the ADC sensor.
/// @param channel Pointer to an adc_dt_spec structure representing the ADC channel. /// @param channel Pointer to an adc_dt_spec structure representing the ADC channel.
@@ -79,7 +79,5 @@ void ADCSensor::set_sample_count(uint8_t sample_count) {
void ADCSensor::set_sampling_mode(SamplingMode sampling_mode) { this->sampling_mode_ = sampling_mode; } void ADCSensor::set_sampling_mode(SamplingMode sampling_mode) { this->sampling_mode_ = sampling_mode; }
float ADCSensor::get_setup_priority() const { return setup_priority::DATA; }
} // namespace adc } // namespace adc
} // namespace esphome } // namespace esphome
+11 -11
View File
@@ -42,11 +42,11 @@ void ADCSensor::setup() {
adc_oneshot_unit_init_cfg_t init_config = {}; // Zero initialize adc_oneshot_unit_init_cfg_t init_config = {}; // Zero initialize
init_config.unit_id = this->adc_unit_; init_config.unit_id = this->adc_unit_;
init_config.ulp_mode = ADC_ULP_MODE_DISABLE; init_config.ulp_mode = ADC_ULP_MODE_DISABLE;
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ #if USE_ESP32_VARIANT_ESP32C2 || USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || \
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2
init_config.clk_src = ADC_DIGI_CLK_SRC_DEFAULT; init_config.clk_src = ADC_DIGI_CLK_SRC_DEFAULT;
#endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || #endif // USE_ESP32_VARIANT_ESP32C2 || USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 ||
// USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 // USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2
esp_err_t err = adc_oneshot_new_unit(&init_config, &ADCSensor::shared_adc_handles[this->adc_unit_]); esp_err_t err = adc_oneshot_new_unit(&init_config, &ADCSensor::shared_adc_handles[this->adc_unit_]);
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGE(TAG, "Error initializing %s: %d", LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)), err); ESP_LOGE(TAG, "Error initializing %s: %d", LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)), err);
@@ -76,7 +76,7 @@ void ADCSensor::setup() {
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
// RISC-V variants and S3 use curve fitting calibration // RISC-V variants (except C2) and S3 use curve fitting calibration
adc_cali_curve_fitting_config_t cali_config = {}; // Zero initialize first adc_cali_curve_fitting_config_t cali_config = {}; // Zero initialize first
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
cali_config.chan = this->channel_; cali_config.chan = this->channel_;
@@ -94,14 +94,14 @@ void ADCSensor::setup() {
ESP_LOGW(TAG, "Curve fitting calibration failed with error %d, will use uncalibrated readings", err); ESP_LOGW(TAG, "Curve fitting calibration failed with error %d, will use uncalibrated readings", err);
this->setup_flags_.calibration_complete = false; this->setup_flags_.calibration_complete = false;
} }
#else // Other ESP32 variants use line fitting calibration #else // ESP32, ESP32-S2, and ESP32-C2 use line fitting calibration
adc_cali_line_fitting_config_t cali_config = { adc_cali_line_fitting_config_t cali_config = {
.unit_id = this->adc_unit_, .unit_id = this->adc_unit_,
.atten = this->attenuation_, .atten = this->attenuation_,
.bitwidth = ADC_BITWIDTH_DEFAULT, .bitwidth = ADC_BITWIDTH_DEFAULT,
#if !defined(USE_ESP32_VARIANT_ESP32S2) #if !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32C2)
.default_vref = 1100, // Default reference voltage in mV .default_vref = 1100, // Default reference voltage in mV
#endif // !defined(USE_ESP32_VARIANT_ESP32S2) #endif // !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32C2)
}; };
err = adc_cali_create_scheme_line_fitting(&cali_config, &handle); err = adc_cali_create_scheme_line_fitting(&cali_config, &handle);
if (err == ESP_OK) { if (err == ESP_OK) {
@@ -112,7 +112,7 @@ void ADCSensor::setup() {
ESP_LOGW(TAG, "Line fitting calibration failed with error %d, will use uncalibrated readings", err); ESP_LOGW(TAG, "Line fitting calibration failed with error %d, will use uncalibrated readings", err);
this->setup_flags_.calibration_complete = false; this->setup_flags_.calibration_complete = false;
} }
#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3 #endif // ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3
} }
this->setup_flags_.init_complete = true; this->setup_flags_.init_complete = true;
@@ -189,7 +189,7 @@ float ADCSensor::sample_fixed_attenuation_() {
adc_cali_delete_scheme_curve_fitting(this->calibration_handle_); adc_cali_delete_scheme_curve_fitting(this->calibration_handle_);
#else // Other ESP32 variants use line fitting calibration #else // Other ESP32 variants use line fitting calibration
adc_cali_delete_scheme_line_fitting(this->calibration_handle_); adc_cali_delete_scheme_line_fitting(this->calibration_handle_);
#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3 #endif // ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3
this->calibration_handle_ = nullptr; this->calibration_handle_ = nullptr;
} }
} }
@@ -247,7 +247,7 @@ float ADCSensor::sample_autorange_() {
.unit_id = this->adc_unit_, .unit_id = this->adc_unit_,
.atten = atten, .atten = atten,
.bitwidth = ADC_BITWIDTH_DEFAULT, .bitwidth = ADC_BITWIDTH_DEFAULT,
#if !defined(USE_ESP32_VARIANT_ESP32S2) #if !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32C2)
.default_vref = 1100, .default_vref = 1100,
#endif #endif
}; };
+18 -15
View File
@@ -2,7 +2,7 @@ import logging
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import sensor, voltage_sampler from esphome.components import sensor, voltage_sampler
from esphome.components.esp32 import get_esp32_variant from esphome.components.esp32 import get_esp32_variant, include_builtin_idf_component
from esphome.components.nrf52.const import AIN_TO_GPIO, EXTRA_ADC from esphome.components.nrf52.const import AIN_TO_GPIO, EXTRA_ADC
from esphome.components.zephyr import ( from esphome.components.zephyr import (
zephyr_add_overlay, zephyr_add_overlay,
@@ -118,6 +118,9 @@ async def to_code(config):
cg.add(var.set_sampling_mode(config[CONF_SAMPLING_MODE])) cg.add(var.set_sampling_mode(config[CONF_SAMPLING_MODE]))
if CORE.is_esp32: if CORE.is_esp32:
# Re-enable ESP-IDF's ADC driver (excluded by default to save compile time)
include_builtin_idf_component("esp_adc")
if attenuation := config.get(CONF_ATTENUATION): if attenuation := config.get(CONF_ATTENUATION):
if attenuation == "auto": if attenuation == "auto":
cg.add(var.set_autorange(cg.global_ns.true)) cg.add(var.set_autorange(cg.global_ns.true))
@@ -160,21 +163,21 @@ async def to_code(config):
zephyr_add_user("io-channels", f"<&adc {channel_id}>") zephyr_add_user("io-channels", f"<&adc {channel_id}>")
zephyr_add_overlay( zephyr_add_overlay(
f""" f"""
&adc {{ &adc {{
#address-cells = <1>; #address-cells = <1>;
#size-cells = <0>; #size-cells = <0>;
channel@{channel_id} {{ channel@{channel_id} {{
reg = <{channel_id}>; reg = <{channel_id}>;
zephyr,gain = "{gain}"; zephyr,gain = "{gain}";
zephyr,reference = "ADC_REF_INTERNAL"; zephyr,reference = "ADC_REF_INTERNAL";
zephyr,acquisition-time = <ADC_ACQ_TIME_DEFAULT>; zephyr,acquisition-time = <ADC_ACQ_TIME_DEFAULT>;
zephyr,input-positive = <NRF_SAADC_{pin_number}>; zephyr,input-positive = <NRF_SAADC_{pin_number}>;
zephyr,resolution = <14>; zephyr,resolution = <14>;
zephyr,oversampling = <8>; zephyr,oversampling = <8>;
}}; }};
}}; }};
""" """
) )
@@ -9,8 +9,6 @@ static const char *const TAG = "adc128s102.sensor";
ADC128S102Sensor::ADC128S102Sensor(uint8_t channel) : channel_(channel) {} ADC128S102Sensor::ADC128S102Sensor(uint8_t channel) : channel_(channel) {}
float ADC128S102Sensor::get_setup_priority() const { return setup_priority::DATA; }
void ADC128S102Sensor::dump_config() { void ADC128S102Sensor::dump_config() {
LOG_SENSOR("", "ADC128S102 Sensor", this); LOG_SENSOR("", "ADC128S102 Sensor", this);
ESP_LOGCONFIG(TAG, " Pin: %u", this->channel_); ESP_LOGCONFIG(TAG, " Pin: %u", this->channel_);
@@ -19,7 +19,6 @@ class ADC128S102Sensor : public PollingComponent,
void update() override; void update() override;
void dump_config() override; void dump_config() override;
float get_setup_priority() const override;
float sample() override; float sample() override;
protected: protected:
-2
View File
@@ -150,8 +150,6 @@ void AHT10Component::update() {
this->restart_read_(); this->restart_read_();
} }
float AHT10Component::get_setup_priority() const { return setup_priority::DATA; }
void AHT10Component::dump_config() { void AHT10Component::dump_config() {
ESP_LOGCONFIG(TAG, "AHT10:"); ESP_LOGCONFIG(TAG, "AHT10:");
LOG_I2C_DEVICE(this); LOG_I2C_DEVICE(this);
-1
View File
@@ -16,7 +16,6 @@ class AHT10Component : public PollingComponent, public i2c::I2CDevice {
void setup() override; void setup() override;
void update() override; void update() override;
void dump_config() override; void dump_config() override;
float get_setup_priority() const override;
void set_variant(AHT10Variant variant) { this->variant_ = variant; } void set_variant(AHT10Variant variant) { this->variant_ = variant; }
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
@@ -67,52 +67,29 @@ void AlarmControlPanel::add_on_ready_callback(std::function<void()> &&callback)
this->ready_callback_.add(std::move(callback)); this->ready_callback_.add(std::move(callback));
} }
void AlarmControlPanel::arm_away(optional<std::string> code) { void AlarmControlPanel::arm_with_code_(AlarmControlPanelCall &(AlarmControlPanelCall::*arm_method)(),
const char *code) {
auto call = this->make_call(); auto call = this->make_call();
call.arm_away(); (call.*arm_method)();
if (code.has_value()) if (code != nullptr)
call.set_code(code.value()); call.set_code(code);
call.perform(); call.perform();
} }
void AlarmControlPanel::arm_home(optional<std::string> code) { void AlarmControlPanel::arm_away(const char *code) { this->arm_with_code_(&AlarmControlPanelCall::arm_away, code); }
auto call = this->make_call();
call.arm_home(); void AlarmControlPanel::arm_home(const char *code) { this->arm_with_code_(&AlarmControlPanelCall::arm_home, code); }
if (code.has_value())
call.set_code(code.value()); void AlarmControlPanel::arm_night(const char *code) { this->arm_with_code_(&AlarmControlPanelCall::arm_night, code); }
call.perform();
void AlarmControlPanel::arm_vacation(const char *code) {
this->arm_with_code_(&AlarmControlPanelCall::arm_vacation, code);
} }
void AlarmControlPanel::arm_night(optional<std::string> code) { void AlarmControlPanel::arm_custom_bypass(const char *code) {
auto call = this->make_call(); this->arm_with_code_(&AlarmControlPanelCall::arm_custom_bypass, code);
call.arm_night();
if (code.has_value())
call.set_code(code.value());
call.perform();
} }
void AlarmControlPanel::arm_vacation(optional<std::string> code) { void AlarmControlPanel::disarm(const char *code) { this->arm_with_code_(&AlarmControlPanelCall::disarm, code); }
auto call = this->make_call();
call.arm_vacation();
if (code.has_value())
call.set_code(code.value());
call.perform();
}
void AlarmControlPanel::arm_custom_bypass(optional<std::string> code) {
auto call = this->make_call();
call.arm_custom_bypass();
if (code.has_value())
call.set_code(code.value());
call.perform();
}
void AlarmControlPanel::disarm(optional<std::string> code) {
auto call = this->make_call();
call.disarm();
if (code.has_value())
call.set_code(code.value());
call.perform();
}
} // namespace esphome::alarm_control_panel } // namespace esphome::alarm_control_panel
@@ -76,37 +76,53 @@ class AlarmControlPanel : public EntityBase {
* *
* @param code The code * @param code The code
*/ */
void arm_away(optional<std::string> code = nullopt); void arm_away(const char *code = nullptr);
void arm_away(const optional<std::string> &code) {
this->arm_away(code.has_value() ? code.value().c_str() : nullptr);
}
/** arm the alarm in home mode /** arm the alarm in home mode
* *
* @param code The code * @param code The code
*/ */
void arm_home(optional<std::string> code = nullopt); void arm_home(const char *code = nullptr);
void arm_home(const optional<std::string> &code) {
this->arm_home(code.has_value() ? code.value().c_str() : nullptr);
}
/** arm the alarm in night mode /** arm the alarm in night mode
* *
* @param code The code * @param code The code
*/ */
void arm_night(optional<std::string> code = nullopt); void arm_night(const char *code = nullptr);
void arm_night(const optional<std::string> &code) {
this->arm_night(code.has_value() ? code.value().c_str() : nullptr);
}
/** arm the alarm in vacation mode /** arm the alarm in vacation mode
* *
* @param code The code * @param code The code
*/ */
void arm_vacation(optional<std::string> code = nullopt); void arm_vacation(const char *code = nullptr);
void arm_vacation(const optional<std::string> &code) {
this->arm_vacation(code.has_value() ? code.value().c_str() : nullptr);
}
/** arm the alarm in custom bypass mode /** arm the alarm in custom bypass mode
* *
* @param code The code * @param code The code
*/ */
void arm_custom_bypass(optional<std::string> code = nullopt); void arm_custom_bypass(const char *code = nullptr);
void arm_custom_bypass(const optional<std::string> &code) {
this->arm_custom_bypass(code.has_value() ? code.value().c_str() : nullptr);
}
/** disarm the alarm /** disarm the alarm
* *
* @param code The code * @param code The code
*/ */
void disarm(optional<std::string> code = nullopt); void disarm(const char *code = nullptr);
void disarm(const optional<std::string> &code) { this->disarm(code.has_value() ? code.value().c_str() : nullptr); }
/** Get the state /** Get the state
* *
@@ -118,6 +134,8 @@ class AlarmControlPanel : public EntityBase {
protected: protected:
friend AlarmControlPanelCall; friend AlarmControlPanelCall;
// Helper to reduce code duplication for arm/disarm methods
void arm_with_code_(AlarmControlPanelCall &(AlarmControlPanelCall::*arm_method)(), const char *code);
// in order to store last panel state in flash // in order to store last panel state in flash
ESPPreferenceObject pref_; ESPPreferenceObject pref_;
// current state // current state
@@ -10,8 +10,10 @@ static const char *const TAG = "alarm_control_panel";
AlarmControlPanelCall::AlarmControlPanelCall(AlarmControlPanel *parent) : parent_(parent) {} AlarmControlPanelCall::AlarmControlPanelCall(AlarmControlPanel *parent) : parent_(parent) {}
AlarmControlPanelCall &AlarmControlPanelCall::set_code(const std::string &code) { AlarmControlPanelCall &AlarmControlPanelCall::set_code(const char *code) {
this->code_ = code; if (code != nullptr) {
this->code_ = std::string(code);
}
return *this; return *this;
} }
@@ -14,7 +14,8 @@ class AlarmControlPanelCall {
public: public:
AlarmControlPanelCall(AlarmControlPanel *parent); AlarmControlPanelCall(AlarmControlPanel *parent);
AlarmControlPanelCall &set_code(const std::string &code); AlarmControlPanelCall &set_code(const char *code);
AlarmControlPanelCall &set_code(const std::string &code) { return this->set_code(code.c_str()); }
AlarmControlPanelCall &arm_away(); AlarmControlPanelCall &arm_away();
AlarmControlPanelCall &arm_home(); AlarmControlPanelCall &arm_home();
AlarmControlPanelCall &arm_night(); AlarmControlPanelCall &arm_night();
@@ -1,32 +1,15 @@
#include "alarm_control_panel_state.h" #include "alarm_control_panel_state.h"
#include "esphome/core/progmem.h"
namespace esphome::alarm_control_panel { namespace esphome::alarm_control_panel {
// Alarm control panel state strings indexed by AlarmControlPanelState enum (0-9)
PROGMEM_STRING_TABLE(AlarmControlPanelStateStrings, "DISARMED", "ARMED_HOME", "ARMED_AWAY", "ARMED_NIGHT",
"ARMED_VACATION", "ARMED_CUSTOM_BYPASS", "PENDING", "ARMING", "DISARMING", "TRIGGERED", "UNKNOWN");
const LogString *alarm_control_panel_state_to_string(AlarmControlPanelState state) { const LogString *alarm_control_panel_state_to_string(AlarmControlPanelState state) {
switch (state) { return AlarmControlPanelStateStrings::get_log_str(static_cast<uint8_t>(state),
case ACP_STATE_DISARMED: AlarmControlPanelStateStrings::LAST_INDEX);
return LOG_STR("DISARMED");
case ACP_STATE_ARMED_HOME:
return LOG_STR("ARMED_HOME");
case ACP_STATE_ARMED_AWAY:
return LOG_STR("ARMED_AWAY");
case ACP_STATE_ARMED_NIGHT:
return LOG_STR("ARMED_NIGHT");
case ACP_STATE_ARMED_VACATION:
return LOG_STR("ARMED_VACATION");
case ACP_STATE_ARMED_CUSTOM_BYPASS:
return LOG_STR("ARMED_CUSTOM_BYPASS");
case ACP_STATE_PENDING:
return LOG_STR("PENDING");
case ACP_STATE_ARMING:
return LOG_STR("ARMING");
case ACP_STATE_DISARMING:
return LOG_STR("DISARMING");
case ACP_STATE_TRIGGERED:
return LOG_STR("TRIGGERED");
default:
return LOG_STR("UNKNOWN");
}
} }
} // namespace esphome::alarm_control_panel } // namespace esphome::alarm_control_panel
@@ -66,15 +66,7 @@ template<typename... Ts> class ArmAwayAction : public Action<Ts...> {
TEMPLATABLE_VALUE(std::string, code) TEMPLATABLE_VALUE(std::string, code)
void play(const Ts &...x) override { void play(const Ts &...x) override { this->alarm_control_panel_->arm_away(this->code_.optional_value(x...)); }
auto call = this->alarm_control_panel_->make_call();
auto code = this->code_.optional_value(x...);
if (code.has_value()) {
call.set_code(code.value());
}
call.arm_away();
call.perform();
}
protected: protected:
AlarmControlPanel *alarm_control_panel_; AlarmControlPanel *alarm_control_panel_;
@@ -86,15 +78,7 @@ template<typename... Ts> class ArmHomeAction : public Action<Ts...> {
TEMPLATABLE_VALUE(std::string, code) TEMPLATABLE_VALUE(std::string, code)
void play(const Ts &...x) override { void play(const Ts &...x) override { this->alarm_control_panel_->arm_home(this->code_.optional_value(x...)); }
auto call = this->alarm_control_panel_->make_call();
auto code = this->code_.optional_value(x...);
if (code.has_value()) {
call.set_code(code.value());
}
call.arm_home();
call.perform();
}
protected: protected:
AlarmControlPanel *alarm_control_panel_; AlarmControlPanel *alarm_control_panel_;
@@ -106,15 +90,7 @@ template<typename... Ts> class ArmNightAction : public Action<Ts...> {
TEMPLATABLE_VALUE(std::string, code) TEMPLATABLE_VALUE(std::string, code)
void play(const Ts &...x) override { void play(const Ts &...x) override { this->alarm_control_panel_->arm_night(this->code_.optional_value(x...)); }
auto call = this->alarm_control_panel_->make_call();
auto code = this->code_.optional_value(x...);
if (code.has_value()) {
call.set_code(code.value());
}
call.arm_night();
call.perform();
}
protected: protected:
AlarmControlPanel *alarm_control_panel_; AlarmControlPanel *alarm_control_panel_;
-2
View File
@@ -176,7 +176,5 @@ void AM2315C::dump_config() {
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
} }
float AM2315C::get_setup_priority() const { return setup_priority::DATA; }
} // namespace am2315c } // namespace am2315c
} // namespace esphome } // namespace esphome
-1
View File
@@ -33,7 +33,6 @@ class AM2315C : public PollingComponent, public i2c::I2CDevice {
void dump_config() override; void dump_config() override;
void update() override; void update() override;
void setup() override; void setup() override;
float get_setup_priority() const override;
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; } void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; }
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
-1
View File
@@ -51,7 +51,6 @@ void AM2320Component::dump_config() {
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
} }
float AM2320Component::get_setup_priority() const { return setup_priority::DATA; }
bool AM2320Component::read_bytes_(uint8_t a_register, uint8_t *data, uint8_t len, uint32_t conversion) { bool AM2320Component::read_bytes_(uint8_t a_register, uint8_t *data, uint8_t len, uint32_t conversion) {
if (!this->write_bytes(a_register, data, 2)) { if (!this->write_bytes(a_register, data, 2)) {
-1
View File
@@ -11,7 +11,6 @@ class AM2320Component : public PollingComponent, public i2c::I2CDevice {
public: public:
void setup() override; void setup() override;
void dump_config() override; void dump_config() override;
float get_setup_priority() const override;
void update() override; void update() override;
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }

Some files were not shown because too many files have changed in this diff Show More