Compare commits

..

1 Commits

Author SHA1 Message Date
J. Nick Koston
d67e9d0926 [web_server] Add legacy_id to SSE events for third-party integration compatibility 2026-01-23 08:18:24 -10:00
383 changed files with 11398 additions and 20223 deletions

View File

@@ -1,21 +1 @@
<<<<<<< HEAD
<<<<<<< HEAD
<<<<<<< HEAD
<<<<<<< HEAD
<<<<<<< HEAD
a172e2f65981e98354cc6b5ecf69bdb055dd13602226042ab2c7acd037a2bf41
=======
d565b0589e35e692b5f2fc0c14723a99595b4828a3a3ef96c442e86a23176c00
>>>>>>> mqtt_enum_flash
=======
a172e2f65981e98354cc6b5ecf69bdb055dd13602226042ab2c7acd037a2bf41
>>>>>>> upstream/dev
=======
08c21fa4c044fd80c8f3296371f30c4f5ff3418f1bc1efe63c5bad938301f122
>>>>>>> rp2040_web_server_only_no_cap
=======
cf3d341206b4184ec8b7fe85141aef4fe4696aa720c3f8a06d4e57930574bdab
>>>>>>> upstream/dev
=======
069fa9526c52f7c580a9ec17c7678d12f142221387e9b561c18f95394d4629a3
>>>>>>> upstream/dev
15dc295268b2dcf75942f42759b3ddec64eba89f75525698eb39c95a7f4b14ce

View File

@@ -22,7 +22,7 @@ runs:
python-version: ${{ inputs.python-version }}
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with:
path: venv
# yamllint disable-line rule:line-length

View File

@@ -1,38 +0,0 @@
// 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+/
]
};

View File

@@ -1,373 +0,0 @@
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
};

View File

@@ -1,187 +0,0 @@
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);
};

View File

@@ -1,41 +0,0 @@
// 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
};

View File

@@ -1,141 +0,0 @@
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
};

View File

@@ -36,5 +36,633 @@ jobs:
with:
github-token: ${{ steps.generate-token.outputs.token }}
script: |
const script = require('./.github/scripts/auto-label-pr/index.js');
await script({ github, context });
const fs = require('fs');
// Constants
const SMALL_PR_THRESHOLD = parseInt('${{ env.SMALL_PR_THRESHOLD }}');
const MAX_LABELS = parseInt('${{ env.MAX_LABELS }}');
const TOO_BIG_THRESHOLD = parseInt('${{ env.TOO_BIG_THRESHOLD }}');
const COMPONENT_LABEL_THRESHOLD = parseInt('${{ env.COMPONENT_LABEL_THRESHOLD }}');
const BOT_COMMENT_MARKER = '<!-- auto-label-pr-bot -->';
const CODEOWNERS_MARKER = '<!-- codeowners-request -->';
const TOO_BIG_MARKER = '<!-- too-big-request -->';
const MANAGED_LABELS = [
'new-component',
'new-platform',
'new-target-platform',
'merging-to-release',
'merging-to-beta',
'chained-pr',
'core',
'small-pr',
'dashboard',
'github-actions',
'by-code-owner',
'has-tests',
'needs-tests',
'needs-docs',
'needs-codeowners',
'too-big',
'labeller-recheck',
'bugfix',
'new-feature',
'breaking-change',
'developer-breaking-change',
'code-quality'
];
const DOCS_PR_PATTERNS = [
/https:\/\/github\.com\/esphome\/esphome-docs\/pull\/\d+/,
/esphome\/esphome-docs#\d+/
];
// Global state
const { owner, repo } = context.repo;
const pr_number = context.issue.number;
// Get current labels and PR data
const { data: currentLabelsData } = await github.rest.issues.listLabelsOnIssue({
owner,
repo,
issue_number: pr_number
});
const currentLabels = currentLabelsData.map(label => label.name);
const managedLabels = currentLabels.filter(label =>
label.startsWith('component: ') || MANAGED_LABELS.includes(label)
);
// Check for mega-PR early - if present, skip most automatic labeling
const isMegaPR = currentLabels.includes('mega-pr');
// Get all PR files with automatic pagination
const prFiles = await github.paginate(
github.rest.pulls.listFiles,
{
owner,
repo,
pull_number: pr_number
}
);
// Calculate data from PR files
const changedFiles = prFiles.map(file => file.filename);
const totalAdditions = prFiles.reduce((sum, file) => sum + (file.additions || 0), 0);
const totalDeletions = prFiles.reduce((sum, file) => sum + (file.deletions || 0), 0);
const totalChanges = totalAdditions + totalDeletions;
console.log('Current labels:', currentLabels.join(', '));
console.log('Changed files:', changedFiles.length);
console.log('Total changes:', totalChanges);
if (isMegaPR) {
console.log('Mega-PR detected - applying limited labeling logic');
}
// Fetch API data
async function fetchApiData() {
try {
const response = await fetch('https://data.esphome.io/components.json');
const componentsData = await response.json();
return {
targetPlatforms: componentsData.target_platforms || [],
platformComponents: componentsData.platform_components || []
};
} catch (error) {
console.log('Failed to fetch components data from API:', error.message);
return { targetPlatforms: [], platformComponents: [] };
}
}
// Strategy: Merge branch detection
async function detectMergeBranch() {
const labels = new Set();
const baseRef = context.payload.pull_request.base.ref;
if (baseRef === 'release') {
labels.add('merging-to-release');
} else if (baseRef === 'beta') {
labels.add('merging-to-beta');
} else if (baseRef !== 'dev') {
labels.add('chained-pr');
}
return labels;
}
// Strategy: Component and platform labeling
async function detectComponentPlatforms(apiData) {
const labels = new Set();
const componentRegex = /^esphome\/components\/([^\/]+)\//;
const targetPlatformRegex = new RegExp(`^esphome\/components\/(${apiData.targetPlatforms.join('|')})/`);
for (const file of changedFiles) {
const componentMatch = file.match(componentRegex);
if (componentMatch) {
labels.add(`component: ${componentMatch[1]}`);
}
const platformMatch = file.match(targetPlatformRegex);
if (platformMatch) {
labels.add(`platform: ${platformMatch[1]}`);
}
}
return labels;
}
// Strategy: New component detection
async function detectNewComponents() {
const labels = new Set();
const addedFiles = prFiles.filter(file => file.status === 'added').map(file => file.filename);
for (const file of addedFiles) {
const componentMatch = file.match(/^esphome\/components\/([^\/]+)\/__init__\.py$/);
if (componentMatch) {
try {
const content = fs.readFileSync(file, 'utf8');
if (content.includes('IS_TARGET_PLATFORM = True')) {
labels.add('new-target-platform');
}
} catch (error) {
console.log(`Failed to read content of ${file}:`, error.message);
}
labels.add('new-component');
}
}
return labels;
}
// Strategy: New platform detection
async function detectNewPlatforms(apiData) {
const labels = new Set();
const addedFiles = prFiles.filter(file => file.status === 'added').map(file => file.filename);
for (const file of addedFiles) {
const platformFileMatch = file.match(/^esphome\/components\/([^\/]+)\/([^\/]+)\.py$/);
if (platformFileMatch) {
const [, component, platform] = platformFileMatch;
if (apiData.platformComponents.includes(platform)) {
labels.add('new-platform');
}
}
const platformDirMatch = file.match(/^esphome\/components\/([^\/]+)\/([^\/]+)\/__init__\.py$/);
if (platformDirMatch) {
const [, component, platform] = platformDirMatch;
if (apiData.platformComponents.includes(platform)) {
labels.add('new-platform');
}
}
}
return labels;
}
// Strategy: Core files detection
async function detectCoreChanges() {
const labels = new Set();
const coreFiles = changedFiles.filter(file =>
file.startsWith('esphome/core/') ||
(file.startsWith('esphome/') && file.split('/').length === 2)
);
if (coreFiles.length > 0) {
labels.add('core');
}
return labels;
}
// Strategy: PR size detection
async function detectPRSize() {
const labels = new Set();
if (totalChanges <= SMALL_PR_THRESHOLD) {
labels.add('small-pr');
return labels;
}
const testAdditions = prFiles
.filter(file => file.filename.startsWith('tests/'))
.reduce((sum, file) => sum + (file.additions || 0), 0);
const testDeletions = prFiles
.filter(file => file.filename.startsWith('tests/'))
.reduce((sum, file) => sum + (file.deletions || 0), 0);
const nonTestChanges = (totalAdditions - testAdditions) - (totalDeletions - testDeletions);
// Don't add too-big if mega-pr label is already present
if (nonTestChanges > TOO_BIG_THRESHOLD && !isMegaPR) {
labels.add('too-big');
}
return labels;
}
// Strategy: Dashboard changes
async function detectDashboardChanges() {
const labels = new Set();
const dashboardFiles = changedFiles.filter(file =>
file.startsWith('esphome/dashboard/') ||
file.startsWith('esphome/components/dashboard_import/')
);
if (dashboardFiles.length > 0) {
labels.add('dashboard');
}
return labels;
}
// Strategy: GitHub Actions changes
async function detectGitHubActionsChanges() {
const labels = new Set();
const githubActionsFiles = changedFiles.filter(file =>
file.startsWith('.github/workflows/')
);
if (githubActionsFiles.length > 0) {
labels.add('github-actions');
}
return labels;
}
// Strategy: Code owner detection
async function detectCodeOwner() {
const labels = new Set();
try {
const { data: codeownersFile } = await github.rest.repos.getContent({
owner,
repo,
path: 'CODEOWNERS',
});
const codeownersContent = Buffer.from(codeownersFile.content, 'base64').toString('utf8');
const prAuthor = context.payload.pull_request.user.login;
const codeownersLines = codeownersContent.split('\n')
.map(line => line.trim())
.filter(line => line && !line.startsWith('#'));
const codeownersRegexes = codeownersLines.map(line => {
const parts = line.split(/\s+/);
const pattern = parts[0];
const owners = parts.slice(1);
let regex;
if (pattern.endsWith('*')) {
const dir = pattern.slice(0, -1);
regex = new RegExp(`^${dir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`);
} else if (pattern.includes('*')) {
// First escape all regex special chars except *, then replace * with .*
const regexPattern = pattern
.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
.replace(/\*/g, '.*');
regex = new RegExp(`^${regexPattern}$`);
} else {
regex = new RegExp(`^${pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`);
}
return { regex, owners };
});
for (const file of changedFiles) {
for (const { regex, owners } of codeownersRegexes) {
if (regex.test(file) && owners.some(owner => owner === `@${prAuthor}`)) {
labels.add('by-code-owner');
return labels;
}
}
}
} catch (error) {
console.log('Failed to read or parse CODEOWNERS file:', error.message);
}
return labels;
}
// Strategy: Test detection
async function detectTests() {
const labels = new Set();
const testFiles = changedFiles.filter(file => file.startsWith('tests/'));
if (testFiles.length > 0) {
labels.add('has-tests');
}
return labels;
}
// Strategy: PR Template Checkbox detection
async function detectPRTemplateCheckboxes() {
const labels = new Set();
const prBody = context.payload.pull_request.body || '';
console.log('Checking PR template checkboxes...');
// Check for checked checkboxes in the "Types of changes" section
const checkboxPatterns = [
{ pattern: /- \[x\] Bugfix \(non-breaking change which fixes an issue\)/i, label: 'bugfix' },
{ pattern: /- \[x\] New feature \(non-breaking change which adds functionality\)/i, label: 'new-feature' },
{ pattern: /- \[x\] Breaking change \(fix or feature that would cause existing functionality to not work as expected\)/i, label: 'breaking-change' },
{ pattern: /- \[x\] Developer breaking change \(an API change that could break external components\)/i, label: 'developer-breaking-change' },
{ pattern: /- \[x\] Code quality improvements to existing code or addition of tests/i, label: 'code-quality' }
];
for (const { pattern, label } of checkboxPatterns) {
if (pattern.test(prBody)) {
console.log(`Found checked checkbox for: ${label}`);
labels.add(label);
}
}
return labels;
}
// Strategy: Requirements detection
async function detectRequirements(allLabels) {
const labels = new Set();
// Check for missing tests
if ((allLabels.has('new-component') || allLabels.has('new-platform') || allLabels.has('new-feature')) && !allLabels.has('has-tests')) {
labels.add('needs-tests');
}
// Check for missing docs
if (allLabels.has('new-component') || allLabels.has('new-platform') || allLabels.has('new-feature')) {
const prBody = context.payload.pull_request.body || '';
const hasDocsLink = DOCS_PR_PATTERNS.some(pattern => pattern.test(prBody));
if (!hasDocsLink) {
labels.add('needs-docs');
}
}
// Check for missing CODEOWNERS
if (allLabels.has('new-component')) {
const codeownersModified = prFiles.some(file =>
file.filename === 'CODEOWNERS' &&
(file.status === 'modified' || file.status === 'added') &&
(file.additions || 0) > 0
);
if (!codeownersModified) {
labels.add('needs-codeowners');
}
}
return labels;
}
// Generate review messages
function generateReviewMessages(finalLabels, originalLabelCount) {
const messages = [];
const prAuthor = context.payload.pull_request.user.login;
// Too big message
if (finalLabels.includes('too-big')) {
const testAdditions = prFiles
.filter(file => file.filename.startsWith('tests/'))
.reduce((sum, file) => sum + (file.additions || 0), 0);
const testDeletions = prFiles
.filter(file => file.filename.startsWith('tests/'))
.reduce((sum, file) => sum + (file.deletions || 0), 0);
const nonTestChanges = (totalAdditions - testAdditions) - (totalDeletions - testDeletions);
const tooManyLabels = originalLabelCount > MAX_LABELS;
const tooManyChanges = nonTestChanges > TOO_BIG_THRESHOLD;
let message = `${TOO_BIG_MARKER}\n### 📦 Pull Request Size\n\n`;
if (tooManyLabels && tooManyChanges) {
message += `This PR is too large with ${nonTestChanges} line changes (excluding tests) and affects ${originalLabelCount} different components/areas.`;
} else if (tooManyLabels) {
message += `This PR affects ${originalLabelCount} different components/areas.`;
} else {
message += `This PR is too large with ${nonTestChanges} line changes (excluding tests).`;
}
message += ` Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.\n\n`;
message += `For guidance on breaking down large PRs, see: https://developers.esphome.io/contributing/submitting-your-work/#how-to-approach-large-submissions`;
messages.push(message);
}
// CODEOWNERS message
if (finalLabels.includes('needs-codeowners')) {
const message = `${CODEOWNERS_MARKER}\n### 👥 Code Ownership\n\n` +
`Hey there @${prAuthor},\n` +
`Thanks for submitting this pull request! Can you add yourself as a codeowner for this integration? ` +
`This way we can notify you if a bug report for this integration is reported.\n\n` +
`In \`__init__.py\` of the integration, please add:\n\n` +
`\`\`\`python\nCODEOWNERS = ["@${prAuthor}"]\n\`\`\`\n\n` +
`And run \`script/build_codeowners.py\``;
messages.push(message);
}
return messages;
}
// Handle reviews
async function handleReviews(finalLabels, originalLabelCount) {
const reviewMessages = generateReviewMessages(finalLabels, originalLabelCount);
const hasReviewableLabels = finalLabels.some(label =>
['too-big', 'needs-codeowners'].includes(label)
);
const { data: reviews } = await github.rest.pulls.listReviews({
owner,
repo,
pull_number: pr_number
});
const botReviews = reviews.filter(review =>
review.user.type === 'Bot' &&
review.state === 'CHANGES_REQUESTED' &&
review.body && review.body.includes(BOT_COMMENT_MARKER)
);
if (hasReviewableLabels) {
const reviewBody = `${BOT_COMMENT_MARKER}\n\n${reviewMessages.join('\n\n---\n\n')}`;
if (botReviews.length > 0) {
// Update existing review
await github.rest.pulls.updateReview({
owner,
repo,
pull_number: pr_number,
review_id: botReviews[0].id,
body: reviewBody
});
console.log('Updated existing bot review');
} else {
// Create new review
await github.rest.pulls.createReview({
owner,
repo,
pull_number: pr_number,
body: reviewBody,
event: 'REQUEST_CHANGES'
});
console.log('Created new bot review');
}
} else if (botReviews.length > 0) {
// Dismiss existing reviews
for (const review of botReviews) {
try {
await github.rest.pulls.dismissReview({
owner,
repo,
pull_number: pr_number,
review_id: review.id,
message: 'Review dismissed: All requirements have been met'
});
console.log(`Dismissed bot review ${review.id}`);
} catch (error) {
console.log(`Failed to dismiss review ${review.id}:`, error.message);
}
}
}
}
// Main execution
const apiData = await fetchApiData();
const baseRef = context.payload.pull_request.base.ref;
// Early exit for release and beta branches only
if (baseRef === 'release' || baseRef === 'beta') {
const branchLabels = await detectMergeBranch();
const finalLabels = Array.from(branchLabels);
console.log('Computed labels (merge branch only):', finalLabels.join(', '));
// Apply labels
if (finalLabels.length > 0) {
await github.rest.issues.addLabels({
owner,
repo,
issue_number: pr_number,
labels: finalLabels
});
}
// Remove old managed labels
const labelsToRemove = managedLabels.filter(label => !finalLabels.includes(label));
for (const label of labelsToRemove) {
try {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: pr_number,
name: label
});
} catch (error) {
console.log(`Failed to remove label ${label}:`, error.message);
}
}
return;
}
// Run all strategies
const [
branchLabels,
componentLabels,
newComponentLabels,
newPlatformLabels,
coreLabels,
sizeLabels,
dashboardLabels,
actionsLabels,
codeOwnerLabels,
testLabels,
checkboxLabels
] = await Promise.all([
detectMergeBranch(),
detectComponentPlatforms(apiData),
detectNewComponents(),
detectNewPlatforms(apiData),
detectCoreChanges(),
detectPRSize(),
detectDashboardChanges(),
detectGitHubActionsChanges(),
detectCodeOwner(),
detectTests(),
detectPRTemplateCheckboxes()
]);
// Combine all labels
const allLabels = new Set([
...branchLabels,
...componentLabels,
...newComponentLabels,
...newPlatformLabels,
...coreLabels,
...sizeLabels,
...dashboardLabels,
...actionsLabels,
...codeOwnerLabels,
...testLabels,
...checkboxLabels
]);
// Detect requirements based on all other labels
const requirementLabels = await detectRequirements(allLabels);
for (const label of requirementLabels) {
allLabels.add(label);
}
let finalLabels = Array.from(allLabels);
// For mega-PRs, exclude component labels if there are too many
if (isMegaPR) {
const componentLabels = finalLabels.filter(label => label.startsWith('component: '));
if (componentLabels.length > COMPONENT_LABEL_THRESHOLD) {
finalLabels = finalLabels.filter(label => !label.startsWith('component: '));
console.log(`Mega-PR detected - excluding ${componentLabels.length} component labels (threshold: ${COMPONENT_LABEL_THRESHOLD})`);
}
}
// Handle too many labels (only for non-mega PRs)
const tooManyLabels = finalLabels.length > MAX_LABELS;
const originalLabelCount = finalLabels.length;
if (tooManyLabels && !isMegaPR && !finalLabels.includes('too-big')) {
finalLabels = ['too-big'];
}
console.log('Computed labels:', finalLabels.join(', '));
// Handle reviews
await handleReviews(finalLabels, originalLabelCount);
// Apply labels
if (finalLabels.length > 0) {
console.log(`Adding labels: ${finalLabels.join(', ')}`);
await github.rest.issues.addLabels({
owner,
repo,
issue_number: pr_number,
labels: finalLabels
});
}
// Remove old managed labels
const labelsToRemove = managedLabels.filter(label => !finalLabels.includes(label));
for (const label of labelsToRemove) {
console.log(`Removing label: ${label}`);
try {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: pr_number,
name: label
});
} catch (error) {
console.log(`Failed to remove label ${label}:`, error.message);
}
}

View File

@@ -47,7 +47,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with:
path: venv
# yamllint disable-line rule:line-length
@@ -157,7 +157,7 @@ jobs:
token: ${{ secrets.CODECOV_TOKEN }}
- name: Save Python virtual environment cache
if: github.ref == 'refs/heads/dev'
uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/save@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with:
path: venv
key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
@@ -193,7 +193,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Restore components graph cache
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with:
path: .temp/components_graph.json
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
@@ -223,7 +223,7 @@ jobs:
echo "component-test-batches=$(echo "$output" | jq -c '.component_test_batches')" >> $GITHUB_OUTPUT
- name: Save components graph cache
if: github.ref == 'refs/heads/dev'
uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/save@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with:
path: .temp/components_graph.json
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
@@ -245,7 +245,7 @@ jobs:
python-version: "3.13"
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with:
path: venv
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
@@ -334,14 +334,14 @@ jobs:
- name: Cache platformio
if: github.ref == 'refs/heads/dev'
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with:
path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
- name: Cache platformio
if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with:
path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
@@ -413,14 +413,14 @@ jobs:
- name: Cache platformio
if: github.ref == 'refs/heads/dev'
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
- name: Cache platformio
if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
@@ -502,14 +502,14 @@ jobs:
- name: Cache platformio
if: github.ref == 'refs/heads/dev'
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
- name: Cache platformio
if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
@@ -735,7 +735,7 @@ jobs:
- name: Restore cached memory analysis
id: cache-memory-analysis
if: steps.check-script.outputs.skip != 'true'
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with:
path: memory-analysis-target.json
key: ${{ steps.cache-key.outputs.cache-key }}
@@ -759,7 +759,7 @@ jobs:
- name: Cache platformio
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with:
path: ~/.platformio
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
@@ -800,7 +800,7 @@ jobs:
- name: Save memory analysis to cache
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success'
uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/save@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with:
path: memory-analysis-target.json
key: ${{ steps.cache-key.outputs.cache-key }}
@@ -847,7 +847,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache platformio
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with:
path: ~/.platformio
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}

View File

@@ -58,7 +58,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
uses: github/codeql-action/init@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -86,6 +86,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
uses: github/codeql-action/analyze@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
with:
category: "/language:${{matrix.language}}"

View File

@@ -102,12 +102,12 @@ jobs:
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Log in to docker hub
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the GitHub container registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -182,13 +182,13 @@ jobs:
- name: Log in to docker hub
if: matrix.registry == 'dockerhub'
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the GitHub container registry
if: matrix.registry == 'ghcr'
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}

View File

@@ -11,7 +11,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.14.14
rev: v0.14.13
hooks:
# Run the linter.
- id: ruff

View File

@@ -88,8 +88,7 @@ esphome/components/bmp3xx/* @latonita
esphome/components/bmp3xx_base/* @latonita @martgras
esphome/components/bmp3xx_i2c/* @latonita
esphome/components/bmp3xx_spi/* @latonita
esphome/components/bmp581_base/* @danielkent-net @kahrendt
esphome/components/bmp581_i2c/* @danielkent-net @kahrendt
esphome/components/bmp581/* @kahrendt
esphome/components/bp1658cj/* @Cossid
esphome/components/bp5758d/* @Cossid
esphome/components/bthome_mithermometer/* @nagyrobi
@@ -104,7 +103,6 @@ esphome/components/cc1101/* @gabest11 @lygris
esphome/components/ccs811/* @habbie
esphome/components/cd74hc4067/* @asoehlke
esphome/components/ch422g/* @clydebarrow @jesterret
esphome/components/ch423/* @dwmw2
esphome/components/chsc6x/* @kkosik20
esphome/components/climate/* @esphome/core
esphome/components/climate_ir/* @glmnet
@@ -134,7 +132,6 @@ esphome/components/dfplayer/* @glmnet
esphome/components/dfrobot_sen0395/* @niklasweber
esphome/components/dht/* @OttoWinter
esphome/components/display_menu_base/* @numo68
esphome/components/dlms_meter/* @SimonFischer04
esphome/components/dps310/* @kbx81
esphome/components/ds1307/* @badbadc0ffee
esphome/components/ds2484/* @mrk-its
@@ -484,7 +481,6 @@ esphome/components/switch/* @esphome/core
esphome/components/switch/binary_sensor/* @ssieb
esphome/components/sx126x/* @swoboda1337
esphome/components/sx127x/* @swoboda1337
esphome/components/sy6970/* @linkedupbits
esphome/components/syslog/* @clydebarrow
esphome/components/t6615/* @tylermenezes
esphome/components/tc74/* @sethgirvan

View File

@@ -4,7 +4,6 @@ from __future__ import annotations
from collections import defaultdict
from collections.abc import Callable
import json
import sys
from typing import TYPE_CHECKING
@@ -30,8 +29,6 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
)
# Lower threshold for RAM symbols (RAM is more constrained)
RAM_SYMBOL_SIZE_THRESHOLD: int = 24
# Number of top symbols to show in the largest symbols report
TOP_SYMBOLS_LIMIT: int = 30
# Column width constants
COL_COMPONENT: int = 29
@@ -150,34 +147,6 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
section_label = f" [{section[1:]}]" # .data -> [data], .bss -> [bss]
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
all_symbols: list[
tuple[str, str, int, str, str]
] = [] # (symbol, demangled, size, section, component)
for component, symbols in self._component_symbols.items():
for symbol, demangled, size, section in symbols:
all_symbols.append((symbol, demangled, size, section, component))
# Sort by size descending
all_symbols.sort(key=lambda x: x[2], reverse=True)
lines.append("")
lines.append(f"Top {self.TOP_SYMBOLS_LIMIT} Largest Symbols:")
for i, (symbol, demangled, size, section, component) in enumerate(
all_symbols[: self.TOP_SYMBOLS_LIMIT]
):
# Format section label
section_label = f"[{section[1:]}]" if section else ""
# Truncate demangled name if too long
demangled_display = (
f"{demangled[:50]}..." if len(demangled) > 50 else demangled
)
lines.append(
f"{i + 1:>2}. {size:>7,} B {section_label:<8} {demangled_display:<55} {component}"
)
def generate_report(self, detailed: bool = False) -> str:
"""Generate a formatted memory report."""
components = sorted(
@@ -279,9 +248,6 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
"RAM",
)
# Top largest symbols in the binary
self._add_top_symbols(lines)
# Add ESPHome core detailed analysis if there are core symbols
if self._esphome_core_symbols:
self._add_section_header(lines, f"{_COMPONENT_CORE} Detailed Analysis")
@@ -472,28 +438,6 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
return "\n".join(lines)
def to_json(self) -> str:
"""Export analysis results as JSON."""
data = {
"components": {
name: {
"text": mem.text_size,
"rodata": mem.rodata_size,
"data": mem.data_size,
"bss": mem.bss_size,
"flash_total": mem.flash_total,
"ram_total": mem.ram_total,
"symbol_count": mem.symbol_count,
}
for name, mem in self.components.items()
},
"totals": {
"flash": sum(c.flash_total for c in self.components.values()),
"ram": sum(c.ram_total for c in self.components.values()),
},
}
return json.dumps(data, indent=2)
def dump_uncategorized_symbols(self, output_file: str | None = None) -> None:
"""Dump uncategorized symbols for analysis."""
# Sort by size descending

View File

@@ -2,7 +2,7 @@ import logging
import esphome.codegen as cg
from esphome.components import sensor, voltage_sampler
from esphome.components.esp32 import get_esp32_variant, include_builtin_idf_component
from esphome.components.esp32 import get_esp32_variant
from esphome.components.nrf52.const import AIN_TO_GPIO, EXTRA_ADC
from esphome.components.zephyr import (
zephyr_add_overlay,
@@ -118,9 +118,6 @@ async def to_code(config):
cg.add(var.set_sampling_mode(config[CONF_SAMPLING_MODE]))
if CORE.is_esp32:
# Re-enable ESP-IDF's ADC driver (excluded by default to save compile time)
include_builtin_idf_component("esp_adc")
if attenuation := config.get(CONF_ATTENUATION):
if attenuation == "auto":
cg.add(var.set_autorange(cg.global_ns.true))

View File

@@ -45,7 +45,6 @@ service APIConnection {
rpc time_command (TimeCommandRequest) returns (void) {}
rpc update_command (UpdateCommandRequest) returns (void) {}
rpc valve_command (ValveCommandRequest) returns (void) {}
rpc water_heater_command (WaterHeaterCommandRequest) returns (void) {}
rpc subscribe_bluetooth_le_advertisements(SubscribeBluetoothLEAdvertisementsRequest) returns (void) {}
rpc bluetooth_device_request(BluetoothDeviceRequest) returns (void) {}

View File

@@ -133,8 +133,8 @@ void APIConnection::start() {
return;
}
// Initialize client name with peername (IP address) until Hello message provides actual name
char peername[socket::SOCKADDR_STR_LEN];
this->helper_->set_client_name(this->helper_->get_peername_to(peername), strlen(peername));
const char *peername = this->helper_->get_client_peername();
this->helper_->set_client_name(peername, strlen(peername));
}
APIConnection::~APIConnection() {
@@ -179,8 +179,8 @@ void APIConnection::begin_iterator_(ActiveIterator type) {
void APIConnection::loop() {
if (this->flags_.next_close) {
// requested a disconnect - don't close socket here, let APIServer::loop() do it
// so getpeername() still works for the disconnect trigger
// requested a disconnect
this->helper_->close();
this->flags_.remove = true;
return;
}
@@ -293,8 +293,7 @@ bool APIConnection::send_disconnect_response(const DisconnectRequest &msg) {
return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE);
}
void APIConnection::on_disconnect_response(const DisconnectResponse &value) {
// Don't close socket here, let APIServer::loop() do it
// so getpeername() still works for the disconnect trigger
this->helper_->close();
this->flags_.remove = true;
}
@@ -1386,7 +1385,7 @@ uint16_t APIConnection::try_send_water_heater_info(EntityBase *entity, APIConnec
is_single);
}
void APIConnection::water_heater_command(const WaterHeaterCommandRequest &msg) {
void APIConnection::on_water_heater_command_request(const WaterHeaterCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(water_heater::WaterHeater, water_heater, water_heater)
if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_MODE)
call.set_mode(static_cast<water_heater::WaterHeaterMode>(msg.mode));
@@ -1525,11 +1524,8 @@ void APIConnection::complete_authentication_() {
this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::AUTHENTICATED);
this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("connected"));
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
{
char peername[socket::SOCKADDR_STR_LEN];
this->parent_->get_client_connected_trigger()->trigger(std::string(this->helper_->get_client_name()),
std::string(this->helper_->get_peername_to(peername)));
}
this->parent_->get_client_connected_trigger()->trigger(std::string(this->helper_->get_client_name()),
std::string(this->helper_->get_client_peername()));
#endif
#ifdef USE_HOMEASSISTANT_TIME
if (homeassistant::global_homeassistant_time != nullptr) {
@@ -1548,9 +1544,8 @@ bool APIConnection::send_hello_response(const HelloRequest &msg) {
this->helper_->set_client_name(msg.client_info.c_str(), msg.client_info.size());
this->client_api_version_major_ = msg.api_version_major;
this->client_api_version_minor_ = msg.api_version_minor;
char peername[socket::SOCKADDR_STR_LEN];
ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->helper_->get_client_name(),
this->helper_->get_peername_to(peername), this->client_api_version_major_, this->client_api_version_minor_);
this->helper_->get_client_peername(), this->client_api_version_major_, this->client_api_version_minor_);
HelloResponse resp;
resp.api_version_major = 1;
@@ -1867,8 +1862,7 @@ void APIConnection::on_no_setup_connection() {
this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("no connection setup"));
}
void APIConnection::on_fatal_error() {
// Don't close socket here - keep it open so getpeername() works for logging
// Socket will be closed when client is removed from the list in APIServer::loop()
this->helper_->close();
this->flags_.remove = true;
}
@@ -2224,14 +2218,12 @@ void APIConnection::process_state_subscriptions_() {
#endif // USE_API_HOMEASSISTANT_STATES
void APIConnection::log_client_(int level, const LogString *message) {
char peername[socket::SOCKADDR_STR_LEN];
esp_log_printf_(level, TAG, __LINE__, ESPHOME_LOG_FORMAT("%s (%s): %s"), this->helper_->get_client_name(),
this->helper_->get_peername_to(peername), LOG_STR_ARG(message));
this->helper_->get_client_peername(), LOG_STR_ARG(message));
}
void APIConnection::log_warning_(const LogString *message, APIError err) {
char peername[socket::SOCKADDR_STR_LEN];
ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->helper_->get_client_name(), this->helper_->get_peername_to(peername),
ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->helper_->get_client_name(), this->helper_->get_client_peername(),
LOG_STR_ARG(message), LOG_STR_ARG(api_error_to_logstr(err)), errno);
}

View File

@@ -170,7 +170,7 @@ class APIConnection final : public APIServerConnection {
#ifdef USE_WATER_HEATER
bool send_water_heater_state(water_heater::WaterHeater *water_heater);
void water_heater_command(const WaterHeaterCommandRequest &msg) override;
void on_water_heater_command_request(const WaterHeaterCommandRequest &msg) override;
#endif
#ifdef USE_IR_RF
@@ -281,10 +281,8 @@ class APIConnection final : public APIServerConnection {
bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override;
const char *get_name() const { return this->helper_->get_client_name(); }
/// Get peer name (IP address) into caller-provided buffer, returns buf for convenience
const char *get_peername_to(std::span<char, socket::SOCKADDR_STR_LEN> buf) const {
return this->helper_->get_peername_to(buf);
}
/// Get peer name (IP address) - cached at connection init time
const char *get_peername() const { return this->helper_->get_client_peername(); }
protected:
// Helper function to handle authentication completion

View File

@@ -16,12 +16,7 @@ static const char *const TAG = "api.frame_helper";
static constexpr size_t API_MAX_LOG_BYTES = 168;
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
#define HELPER_LOG(msg, ...) \
do { \
char peername_buf[socket::SOCKADDR_STR_LEN]; \
this->get_peername_to(peername_buf); \
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, peername_buf, ##__VA_ARGS__); \
} while (0)
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__)
#else
#define HELPER_LOG(msg, ...) ((void) 0)
#endif
@@ -245,20 +240,13 @@ APIError APIFrameHelper::try_send_tx_buf_() {
return APIError::OK; // All buffers sent successfully
}
const char *APIFrameHelper::get_peername_to(std::span<char, socket::SOCKADDR_STR_LEN> buf) const {
if (this->socket_) {
this->socket_->getpeername_to(buf);
} else {
buf[0] = '\0';
}
return buf.data();
}
APIError APIFrameHelper::init_common_() {
if (state_ != State::INITIALIZE || this->socket_ == nullptr) {
HELPER_LOG("Bad state for init %d", (int) state_);
return APIError::BAD_STATE;
}
// Cache peername now while socket is valid - needed for error logging after socket failure
this->socket_->getpeername_to(this->client_peername_);
int err = this->socket_->setblocking(false);
if (err != 0) {
state_ = State::FAILED;

View File

@@ -90,9 +90,8 @@ class APIFrameHelper {
// Get client name (null-terminated)
const char *get_client_name() const { return this->client_name_; }
// Get client peername/IP into caller-provided buffer (fetches on-demand from socket)
// Returns pointer to buf for convenience in printf-style calls
const char *get_peername_to(std::span<char, socket::SOCKADDR_STR_LEN> buf) const;
// Get client peername/IP (null-terminated, cached at init time for availability after socket failure)
const char *get_client_peername() const { return this->client_peername_; }
// Set client name from buffer with length (truncates if needed)
void set_client_name(const char *name, size_t len) {
size_t copy_len = std::min(len, sizeof(this->client_name_) - 1);
@@ -106,8 +105,6 @@ class APIFrameHelper {
bool can_write_without_blocking() { return this->state_ == State::DATA && this->tx_buf_count_ == 0; }
int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); }
APIError close() {
if (state_ == State::CLOSED)
return APIError::OK; // Already closed
state_ = State::CLOSED;
int err = this->socket_->close();
if (err == -1)
@@ -234,6 +231,8 @@ class APIFrameHelper {
// Client name buffer - stores name from Hello message or initial peername
char client_name_[CLIENT_INFO_NAME_MAX_LEN]{};
// Cached peername/IP address - captured at init time for availability after socket failure
char client_peername_[socket::SOCKADDR_STR_LEN]{};
// Group smaller types together
uint16_t rx_buf_len_ = 0;

View File

@@ -29,12 +29,7 @@ static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit")
static constexpr size_t API_MAX_LOG_BYTES = 168;
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
#define HELPER_LOG(msg, ...) \
do { \
char peername_buf[socket::SOCKADDR_STR_LEN]; \
this->get_peername_to(peername_buf); \
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, peername_buf, ##__VA_ARGS__); \
} while (0)
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__)
#else
#define HELPER_LOG(msg, ...) ((void) 0)
#endif

View File

@@ -21,12 +21,7 @@ static const char *const TAG = "api.plaintext";
static constexpr size_t API_MAX_LOG_BYTES = 168;
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
#define HELPER_LOG(msg, ...) \
do { \
char peername_buf[socket::SOCKADDR_STR_LEN]; \
this->get_peername_to(peername_buf); \
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, peername_buf, ##__VA_ARGS__); \
} while (0)
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__)
#else
#define HELPER_LOG(msg, ...) ((void) 0)
#endif

View File

@@ -746,11 +746,6 @@ void APIServerConnection::on_update_command_request(const UpdateCommandRequest &
#ifdef USE_VALVE
void APIServerConnection::on_valve_command_request(const ValveCommandRequest &msg) { this->valve_command(msg); }
#endif
#ifdef USE_WATER_HEATER
void APIServerConnection::on_water_heater_command_request(const WaterHeaterCommandRequest &msg) {
this->water_heater_command(msg);
}
#endif
#ifdef USE_BLUETOOTH_PROXY
void APIServerConnection::on_subscribe_bluetooth_le_advertisements_request(
const SubscribeBluetoothLEAdvertisementsRequest &msg) {

View File

@@ -303,9 +303,6 @@ class APIServerConnection : public APIServerConnectionBase {
#ifdef USE_VALVE
virtual void valve_command(const ValveCommandRequest &msg) = 0;
#endif
#ifdef USE_WATER_HEATER
virtual void water_heater_command(const WaterHeaterCommandRequest &msg) = 0;
#endif
#ifdef USE_BLUETOOTH_PROXY
virtual void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) = 0;
#endif
@@ -435,9 +432,6 @@ class APIServerConnection : public APIServerConnectionBase {
#ifdef USE_VALVE
void on_valve_command_request(const ValveCommandRequest &msg) override;
#endif
#ifdef USE_WATER_HEATER
void on_water_heater_command_request(const WaterHeaterCommandRequest &msg) override;
#endif
#ifdef USE_BLUETOOTH_PROXY
void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
#endif

View File

@@ -192,15 +192,11 @@ void APIServer::loop() {
ESP_LOGV(TAG, "Remove connection %s", client->get_name());
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
// Save client info before closing socket and removal for the trigger
char peername_buf[socket::SOCKADDR_STR_LEN];
// Save client info before removal for the trigger
std::string client_name(client->get_name());
std::string client_peername(client->get_peername_to(peername_buf));
std::string client_peername(client->get_peername());
#endif
// Close socket now (was deferred from on_fatal_error to allow getpeername)
client->helper_->close();
// Swap with the last element and pop (avoids expensive vector shifts)
if (client_index < this->clients_.size() - 1) {
std::swap(this->clients_[client_index], this->clients_.back());

View File

@@ -25,9 +25,7 @@ template<typename... X> class TemplatableStringValue : public TemplatableValue<s
private:
// Helper to convert value to string - handles the case where value is already a string
template<typename T> static std::string value_to_string(T &&val) {
return to_string(std::forward<T>(val)); // NOLINT
}
template<typename T> static std::string value_to_string(T &&val) { return to_string(std::forward<T>(val)); }
// Overloads for string types - needed because std::to_string doesn't support them
static std::string value_to_string(char *val) {

View File

@@ -264,9 +264,9 @@ template<typename... Ts> class APIRespondAction : public Action<Ts...> {
// Build and send JSON response
json::JsonBuilder builder;
this->json_builder_(x..., builder.root());
auto json_buf = builder.serialize();
std::string json_str = builder.serialize();
this->parent_->send_action_response(call_id, success, StringRef(error_message),
reinterpret_cast<const uint8_t *>(json_buf.data()), json_buf.size());
reinterpret_cast<const uint8_t *>(json_str.data()), json_str.size());
return;
}
#endif

View File

@@ -38,10 +38,8 @@ async def to_code(config):
# https://github.com/ESP32Async/ESPAsyncTCP
cg.add_library("ESP32Async/ESPAsyncTCP", "2.0.0")
elif CORE.is_rp2040:
# https://github.com/ayushsharma82/RPAsyncTCP
# RPAsyncTCP is a drop-in replacement for AsyncTCP_RP2040W with better
# ESPAsyncWebServer compatibility
cg.add_library("ayushsharma82/RPAsyncTCP", "1.3.2")
# https://github.com/khoih-prog/AsyncTCP_RP2040W
cg.add_library("khoih-prog/AsyncTCP_RP2040W", "1.2.0")
# Other platforms (host, etc) use socket-based implementation

View File

@@ -8,8 +8,8 @@
// Use ESPAsyncTCP library for ESP8266 (always Arduino)
#include <ESPAsyncTCP.h>
#elif defined(USE_RP2040)
// Use RPAsyncTCP library for RP2040
#include <RPAsyncTCP.h>
// Use AsyncTCP_RP2040W library for RP2040
#include <AsyncTCP_RP2040W.h>
#else
// Use socket-based implementation for other platforms
#include "async_tcp_socket.h"

View File

@@ -1,5 +1,5 @@
import esphome.codegen as cg
from esphome.components.esp32 import add_idf_component, include_builtin_idf_component
from esphome.components.esp32 import add_idf_component
import esphome.config_validation as cv
from esphome.const import CONF_BITS_PER_SAMPLE, CONF_NUM_CHANNELS, CONF_SAMPLE_RATE
import esphome.final_validate as fv
@@ -166,9 +166,6 @@ def final_validate_audio_schema(
async def to_code(config):
# Re-enable ESP-IDF's HTTP client (excluded by default to save compile time)
include_builtin_idf_component("esp_http_client")
add_idf_component(
name="esphome/esp-audio-libs",
ref="2.0.3",

View File

@@ -3,7 +3,6 @@
#include "bedjet_hub.h"
#include "bedjet_child.h"
#include "bedjet_const.h"
#include "esphome/components/esp32_ble/ble_uuid.h"
#include "esphome/core/application.h"
#include <cinttypes>

View File

@@ -14,7 +14,10 @@ void log_binary_sensor(const char *tag, const char *prefix, const char *type, Bi
}
ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str());
LOG_ENTITY_DEVICE_CLASS(tag, prefix, *obj);
if (!obj->get_device_class_ref().empty()) {
ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj->get_device_class_ref().c_str());
}
}
void BinarySensor::publish_state(bool new_state) {

View File

@@ -9,7 +9,7 @@ static const char *const TAG = "bl0940.number";
void CalibrationNumber::setup() {
float value = 0.0f;
if (this->restore_value_) {
this->pref_ = this->make_entity_preference<float>();
this->pref_ = global_preferences->make_preference<float>(this->get_preference_hash());
if (!this->pref_.load(&value)) {
value = 0.0f;
}

View File

@@ -89,9 +89,8 @@ async def to_code(config):
var.set_state_save_interval(config[CONF_STATE_SAVE_INTERVAL].total_milliseconds)
)
# Although this component does not use SPI/Wire directly, the BSEC library requires them
# Although this component does not use SPI, the BSEC library requires the SPI library
cg.add_library("SPI", None)
cg.add_library("Wire", None)
cg.add_define("USE_BSEC")
cg.add_library("boschsensortec/BSEC Software Library", "1.6.1480")

View File

@@ -10,11 +10,12 @@
* - All datasheet page references refer to Bosch Document Number BST-BMP581-DS004-04 (revision number 1.4)
*/
#include "bmp581_base.h"
#include "bmp581.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
namespace esphome::bmp581_base {
namespace esphome {
namespace bmp581 {
static const char *const TAG = "bmp581";
@@ -90,6 +91,7 @@ void BMP581Component::dump_config() {
break;
}
LOG_I2C_DEVICE(this);
LOG_UPDATE_INTERVAL(this);
ESP_LOGCONFIG(TAG, " Measurement conversion time: %ums", this->conversion_time_);
@@ -147,7 +149,7 @@ void BMP581Component::setup() {
uint8_t chip_id;
// read chip id from sensor
if (!this->bmp_read_byte(BMP581_CHIP_ID, &chip_id)) {
if (!this->read_byte(BMP581_CHIP_ID, &chip_id)) {
ESP_LOGE(TAG, "Read chip ID failed");
this->error_code_ = ERROR_COMMUNICATION_FAILED;
@@ -170,7 +172,7 @@ void BMP581Component::setup() {
// 3) Verify sensor status (check if NVM is okay) //
////////////////////////////////////////////////////
if (!this->bmp_read_byte(BMP581_STATUS, &this->status_.reg)) {
if (!this->read_byte(BMP581_STATUS, &this->status_.reg)) {
ESP_LOGE(TAG, "Failed to read status register");
this->error_code_ = ERROR_COMMUNICATION_FAILED;
@@ -357,7 +359,7 @@ bool BMP581Component::check_data_readiness_() {
uint8_t status;
if (!this->bmp_read_byte(BMP581_INT_STATUS, &status)) {
if (!this->read_byte(BMP581_INT_STATUS, &status)) {
ESP_LOGE(TAG, "Failed to read interrupt status register");
return false;
}
@@ -398,7 +400,7 @@ bool BMP581Component::prime_iir_filter_() {
// flush the IIR filter with forced measurements (we will only flush once)
this->dsp_config_.bit.iir_flush_forced_en = true;
if (!this->bmp_write_byte(BMP581_DSP, this->dsp_config_.reg)) {
if (!this->write_byte(BMP581_DSP, this->dsp_config_.reg)) {
ESP_LOGE(TAG, "Failed to write IIR source register");
return false;
@@ -428,7 +430,7 @@ bool BMP581Component::prime_iir_filter_() {
// disable IIR filter flushings on future forced measurements
this->dsp_config_.bit.iir_flush_forced_en = false;
if (!this->bmp_write_byte(BMP581_DSP, this->dsp_config_.reg)) {
if (!this->write_byte(BMP581_DSP, this->dsp_config_.reg)) {
ESP_LOGE(TAG, "Failed to write IIR source register");
return false;
@@ -452,7 +454,7 @@ bool BMP581Component::read_temperature_(float &temperature) {
}
uint8_t data[3];
if (!this->bmp_read_bytes(BMP581_MEASUREMENT_DATA, &data[0], 3)) {
if (!this->read_bytes(BMP581_MEASUREMENT_DATA, &data[0], 3)) {
ESP_LOGW(TAG, "Failed to read measurement");
this->status_set_warning();
@@ -481,7 +483,7 @@ bool BMP581Component::read_temperature_and_pressure_(float &temperature, float &
}
uint8_t data[6];
if (!this->bmp_read_bytes(BMP581_MEASUREMENT_DATA, &data[0], 6)) {
if (!this->read_bytes(BMP581_MEASUREMENT_DATA, &data[0], 6)) {
ESP_LOGW(TAG, "Failed to read measurement");
this->status_set_warning();
@@ -505,7 +507,7 @@ bool BMP581Component::reset_() {
// - returns the Power-On-Reboot interrupt status, which is asserted if successful
// writes reset command to BMP's command register
if (!this->bmp_write_byte(BMP581_COMMAND, RESET_COMMAND)) {
if (!this->write_byte(BMP581_COMMAND, RESET_COMMAND)) {
ESP_LOGE(TAG, "Failed to write reset command");
return false;
@@ -516,7 +518,7 @@ bool BMP581Component::reset_() {
delay(3);
// read interrupt status register
if (!this->bmp_read_byte(BMP581_INT_STATUS, &this->int_status_.reg)) {
if (!this->read_byte(BMP581_INT_STATUS, &this->int_status_.reg)) {
ESP_LOGE(TAG, "Failed to read interrupt status register");
return false;
@@ -560,7 +562,7 @@ bool BMP581Component::write_iir_settings_(IIRFilter temperature_iir, IIRFilter p
// BMP581_DSP register and BMP581_DSP_IIR registers are successive
// - allows us to write the IIR configuration with one command to both registers
uint8_t register_data[2] = {this->dsp_config_.reg, this->iir_config_.reg};
return this->bmp_write_bytes(BMP581_DSP, register_data, sizeof(register_data));
return this->write_bytes(BMP581_DSP, register_data, sizeof(register_data));
}
bool BMP581Component::write_interrupt_source_settings_(bool data_ready_enable) {
@@ -570,7 +572,7 @@ bool BMP581Component::write_interrupt_source_settings_(bool data_ready_enable) {
this->int_source_.bit.drdy_data_reg_en = data_ready_enable;
// write interrupt source register
return this->bmp_write_byte(BMP581_INT_SOURCE, this->int_source_.reg);
return this->write_byte(BMP581_INT_SOURCE, this->int_source_.reg);
}
bool BMP581Component::write_oversampling_settings_(Oversampling temperature_oversampling,
@@ -581,7 +583,7 @@ bool BMP581Component::write_oversampling_settings_(Oversampling temperature_over
this->osr_config_.bit.osr_t = temperature_oversampling;
this->osr_config_.bit.osr_p = pressure_oversampling;
return this->bmp_write_byte(BMP581_OSR, this->osr_config_.reg);
return this->write_byte(BMP581_OSR, this->osr_config_.reg);
}
bool BMP581Component::write_power_mode_(OperationMode mode) {
@@ -591,7 +593,8 @@ bool BMP581Component::write_power_mode_(OperationMode mode) {
this->odr_config_.bit.pwr_mode = mode;
// write odr register
return this->bmp_write_byte(BMP581_ODR, this->odr_config_.reg);
return this->write_byte(BMP581_ODR, this->odr_config_.reg);
}
} // namespace esphome::bmp581_base
} // namespace bmp581
} // namespace esphome

View File

@@ -3,9 +3,11 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/sensor/sensor.h"
namespace esphome::bmp581_base {
namespace esphome {
namespace bmp581 {
static const uint8_t BMP581_ASIC_ID = 0x50; // BMP581's ASIC chip ID (page 51 of datasheet)
static const uint8_t RESET_COMMAND = 0xB6; // Soft reset command
@@ -57,7 +59,7 @@ enum IIRFilter {
IIR_FILTER_128 = 0x7
};
class BMP581Component : public PollingComponent {
class BMP581Component : public PollingComponent, public i2c::I2CDevice {
public:
void dump_config() override;
@@ -82,11 +84,6 @@ class BMP581Component : public PollingComponent {
void set_conversion_time(uint8_t conversion_time) { this->conversion_time_ = conversion_time; }
protected:
virtual bool bmp_read_byte(uint8_t a_register, uint8_t *data) = 0;
virtual bool bmp_write_byte(uint8_t a_register, uint8_t data) = 0;
virtual bool bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0;
virtual bool bmp_write_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0;
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *pressure_sensor_{nullptr};
@@ -219,4 +216,5 @@ class BMP581Component : public PollingComponent {
} odr_config_ = {.reg = 0};
};
} // namespace esphome::bmp581_base
} // namespace bmp581
} // namespace esphome

View File

@@ -1,5 +1,164 @@
import esphome.config_validation as cv
import math
CONFIG_SCHEMA = cv.invalid(
"The bmp581 sensor component has been renamed to bmp581_i2c."
import esphome.codegen as cg
from esphome.components import i2c, sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
CONF_IIR_FILTER,
CONF_OVERSAMPLING,
CONF_PRESSURE,
CONF_TEMPERATURE,
DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_PASCAL,
)
CODEOWNERS = ["@kahrendt"]
DEPENDENCIES = ["i2c"]
bmp581_ns = cg.esphome_ns.namespace("bmp581")
Oversampling = bmp581_ns.enum("Oversampling")
OVERSAMPLING_OPTIONS = {
"NONE": Oversampling.OVERSAMPLING_NONE,
"2X": Oversampling.OVERSAMPLING_X2,
"4X": Oversampling.OVERSAMPLING_X4,
"8X": Oversampling.OVERSAMPLING_X8,
"16X": Oversampling.OVERSAMPLING_X16,
"32X": Oversampling.OVERSAMPLING_X32,
"64X": Oversampling.OVERSAMPLING_X64,
"128X": Oversampling.OVERSAMPLING_X128,
}
IIRFilter = bmp581_ns.enum("IIRFilter")
IIR_FILTER_OPTIONS = {
"OFF": IIRFilter.IIR_FILTER_OFF,
"2X": IIRFilter.IIR_FILTER_2,
"4X": IIRFilter.IIR_FILTER_4,
"8X": IIRFilter.IIR_FILTER_8,
"16X": IIRFilter.IIR_FILTER_16,
"32X": IIRFilter.IIR_FILTER_32,
"64X": IIRFilter.IIR_FILTER_64,
"128X": IIRFilter.IIR_FILTER_128,
}
BMP581Component = bmp581_ns.class_(
"BMP581Component", cg.PollingComponent, i2c.I2CDevice
)
def compute_measurement_conversion_time(config):
# - adds up sensor conversion time based on temperature and pressure oversampling rates given in datasheet
# - returns a rounded up time in ms
# Page 12 of datasheet
PRESSURE_OVERSAMPLING_CONVERSION_TIMES = {
"NONE": 1.0,
"2X": 1.7,
"4X": 2.9,
"8X": 5.4,
"16X": 10.4,
"32X": 20.4,
"64X": 40.4,
"128X": 80.4,
}
# Page 12 of datasheet
TEMPERATURE_OVERSAMPLING_CONVERSION_TIMES = {
"NONE": 1.0,
"2X": 1.1,
"4X": 1.5,
"8X": 2.1,
"16X": 3.3,
"32X": 5.8,
"64X": 10.8,
"128X": 20.8,
}
pressure_conversion_time = (
0.0 # No conversion time necessary without a pressure sensor
)
if pressure_config := config.get(CONF_PRESSURE):
pressure_conversion_time = PRESSURE_OVERSAMPLING_CONVERSION_TIMES[
pressure_config.get(CONF_OVERSAMPLING)
]
temperature_conversion_time = (
1.0 # BMP581 always samples the temperature even if only reading pressure
)
if temperature_config := config.get(CONF_TEMPERATURE):
temperature_conversion_time = TEMPERATURE_OVERSAMPLING_CONVERSION_TIMES[
temperature_config.get(CONF_OVERSAMPLING)
]
# Datasheet indicates a 5% possible error in each conversion time listed
return math.ceil(1.05 * (pressure_conversion_time + temperature_conversion_time))
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(BMP581Component),
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
cv.Optional(CONF_OVERSAMPLING, default="NONE"): cv.enum(
OVERSAMPLING_OPTIONS, upper=True
),
cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum(
IIR_FILTER_OPTIONS, upper=True
),
}
),
cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
unit_of_measurement=UNIT_PASCAL,
accuracy_decimals=0,
device_class=DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
OVERSAMPLING_OPTIONS, upper=True
),
cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum(
IIR_FILTER_OPTIONS, upper=True
),
}
),
}
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x46))
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if temperature_config := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature_config)
cg.add(var.set_temperature_sensor(sens))
cg.add(
var.set_temperature_oversampling_config(
temperature_config[CONF_OVERSAMPLING]
)
)
cg.add(
var.set_temperature_iir_filter_config(temperature_config[CONF_IIR_FILTER])
)
if pressure_config := config.get(CONF_PRESSURE):
sens = await sensor.new_sensor(pressure_config)
cg.add(var.set_pressure_sensor(sens))
cg.add(var.set_pressure_oversampling_config(pressure_config[CONF_OVERSAMPLING]))
cg.add(var.set_pressure_iir_filter_config(pressure_config[CONF_IIR_FILTER]))
cg.add(var.set_conversion_time(compute_measurement_conversion_time(config)))

View File

@@ -1,157 +0,0 @@
import math
import esphome.codegen as cg
from esphome.components import sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
CONF_IIR_FILTER,
CONF_OVERSAMPLING,
CONF_PRESSURE,
CONF_TEMPERATURE,
DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_PASCAL,
)
CODEOWNERS = ["@kahrendt", "@danielkent-net"]
bmp581_ns = cg.esphome_ns.namespace("bmp581_base")
Oversampling = bmp581_ns.enum("Oversampling")
OVERSAMPLING_OPTIONS = {
"NONE": Oversampling.OVERSAMPLING_NONE,
"2X": Oversampling.OVERSAMPLING_X2,
"4X": Oversampling.OVERSAMPLING_X4,
"8X": Oversampling.OVERSAMPLING_X8,
"16X": Oversampling.OVERSAMPLING_X16,
"32X": Oversampling.OVERSAMPLING_X32,
"64X": Oversampling.OVERSAMPLING_X64,
"128X": Oversampling.OVERSAMPLING_X128,
}
IIRFilter = bmp581_ns.enum("IIRFilter")
IIR_FILTER_OPTIONS = {
"OFF": IIRFilter.IIR_FILTER_OFF,
"2X": IIRFilter.IIR_FILTER_2,
"4X": IIRFilter.IIR_FILTER_4,
"8X": IIRFilter.IIR_FILTER_8,
"16X": IIRFilter.IIR_FILTER_16,
"32X": IIRFilter.IIR_FILTER_32,
"64X": IIRFilter.IIR_FILTER_64,
"128X": IIRFilter.IIR_FILTER_128,
}
BMP581Component = bmp581_ns.class_("BMP581Component", cg.PollingComponent)
def compute_measurement_conversion_time(config):
# - adds up sensor conversion time based on temperature and pressure oversampling rates given in datasheet
# - returns a rounded up time in ms
# Page 12 of datasheet
PRESSURE_OVERSAMPLING_CONVERSION_TIMES = {
"NONE": 1.0,
"2X": 1.7,
"4X": 2.9,
"8X": 5.4,
"16X": 10.4,
"32X": 20.4,
"64X": 40.4,
"128X": 80.4,
}
# Page 12 of datasheet
TEMPERATURE_OVERSAMPLING_CONVERSION_TIMES = {
"NONE": 1.0,
"2X": 1.1,
"4X": 1.5,
"8X": 2.1,
"16X": 3.3,
"32X": 5.8,
"64X": 10.8,
"128X": 20.8,
}
pressure_conversion_time = (
0.0 # No conversion time necessary without a pressure sensor
)
if pressure_config := config.get(CONF_PRESSURE):
pressure_conversion_time = PRESSURE_OVERSAMPLING_CONVERSION_TIMES[
pressure_config.get(CONF_OVERSAMPLING)
]
temperature_conversion_time = (
1.0 # BMP581 always samples the temperature even if only reading pressure
)
if temperature_config := config.get(CONF_TEMPERATURE):
temperature_conversion_time = TEMPERATURE_OVERSAMPLING_CONVERSION_TIMES[
temperature_config.get(CONF_OVERSAMPLING)
]
# Datasheet indicates a 5% possible error in each conversion time listed
return math.ceil(1.05 * (pressure_conversion_time + temperature_conversion_time))
CONFIG_SCHEMA_BASE = cv.Schema(
{
cv.GenerateID(): cv.declare_id(BMP581Component),
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
cv.Optional(CONF_OVERSAMPLING, default="NONE"): cv.enum(
OVERSAMPLING_OPTIONS, upper=True
),
cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum(
IIR_FILTER_OPTIONS, upper=True
),
}
),
cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
unit_of_measurement=UNIT_PASCAL,
accuracy_decimals=0,
device_class=DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
OVERSAMPLING_OPTIONS, upper=True
),
cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum(
IIR_FILTER_OPTIONS, upper=True
),
}
),
}
).extend(cv.polling_component_schema("60s"))
async def to_code_base(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
if temperature_config := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature_config)
cg.add(var.set_temperature_sensor(sens))
cg.add(
var.set_temperature_oversampling_config(
temperature_config[CONF_OVERSAMPLING]
)
)
cg.add(
var.set_temperature_iir_filter_config(temperature_config[CONF_IIR_FILTER])
)
if pressure_config := config.get(CONF_PRESSURE):
sens = await sensor.new_sensor(pressure_config)
cg.add(var.set_pressure_sensor(sens))
cg.add(var.set_pressure_oversampling_config(pressure_config[CONF_OVERSAMPLING]))
cg.add(var.set_pressure_iir_filter_config(pressure_config[CONF_IIR_FILTER]))
cg.add(var.set_conversion_time(compute_measurement_conversion_time(config)))
return var

View File

@@ -1,12 +0,0 @@
#include "bmp581_i2c.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome::bmp581_i2c {
void BMP581I2CComponent::dump_config() {
LOG_I2C_DEVICE(this);
BMP581Component::dump_config();
}
} // namespace esphome::bmp581_i2c

View File

@@ -1,24 +0,0 @@
#pragma once
#include "esphome/components/bmp581_base/bmp581_base.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome::bmp581_i2c {
static const char *const TAG = "bmp581_i2c.sensor";
/// This class implements support for the BMP581 Temperature+Pressure i2c sensor.
class BMP581I2CComponent : public esphome::bmp581_base::BMP581Component, public i2c::I2CDevice {
public:
bool bmp_read_byte(uint8_t a_register, uint8_t *data) override { return read_byte(a_register, data); }
bool bmp_write_byte(uint8_t a_register, uint8_t data) override { return write_byte(a_register, data); }
bool bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) override {
return read_bytes(a_register, data, len);
}
bool bmp_write_bytes(uint8_t a_register, uint8_t *data, size_t len) override {
return write_bytes(a_register, data, len);
}
void dump_config() override;
};
} // namespace esphome::bmp581_i2c

View File

@@ -1,23 +0,0 @@
import esphome.codegen as cg
from esphome.components import i2c
import esphome.config_validation as cv
from ..bmp581_base import CONFIG_SCHEMA_BASE, to_code_base
AUTO_LOAD = ["bmp581_base"]
CODEOWNERS = ["@kahrendt", "@danielkent-net"]
DEPENDENCIES = ["i2c"]
bmp581_ns = cg.esphome_ns.namespace("bmp581_i2c")
BMP581I2CComponent = bmp581_ns.class_(
"BMP581I2CComponent", cg.PollingComponent, i2c.I2CDevice
)
CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend(
i2c.i2c_device_schema(default_address=0x46)
).extend({cv.GenerateID(): cv.declare_id(BMP581I2CComponent)})
async def to_code(config):
var = await to_code_base(config)
await i2c.register_i2c_device(var, config)

View File

@@ -12,7 +12,10 @@ void log_button(const char *tag, const char *prefix, const char *type, Button *o
}
ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str());
LOG_ENTITY_ICON(tag, prefix, *obj);
if (!obj->get_icon_ref().empty()) {
ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj->get_icon_ref().c_str());
}
}
void Button::press() {

View File

@@ -96,16 +96,10 @@ void CaptivePortal::start() {
}
void CaptivePortal::handleRequest(AsyncWebServerRequest *req) {
#ifdef USE_ESP32
char url_buf[AsyncWebServerRequest::URL_BUF_SIZE];
StringRef url = req->url_to(url_buf);
#else
const auto &url = req->url();
#endif
if (url == ESPHOME_F("/config.json")) {
if (req->url() == ESPHOME_F("/config.json")) {
this->handle_config(req);
return;
} else if (url == ESPHOME_F("/wifisave")) {
} else if (req->url() == ESPHOME_F("/wifisave")) {
this->handle_wifisave(req);
return;
}

View File

@@ -1,103 +0,0 @@
from esphome import pins
import esphome.codegen as cg
from esphome.components import i2c
from esphome.components.i2c import I2CBus
import esphome.config_validation as cv
from esphome.const import (
CONF_I2C_ID,
CONF_ID,
CONF_INPUT,
CONF_INVERTED,
CONF_MODE,
CONF_NUMBER,
CONF_OPEN_DRAIN,
CONF_OUTPUT,
)
from esphome.core import CORE
CODEOWNERS = ["@dwmw2"]
DEPENDENCIES = ["i2c"]
MULTI_CONF = True
ch423_ns = cg.esphome_ns.namespace("ch423")
CH423Component = ch423_ns.class_("CH423Component", cg.Component, i2c.I2CDevice)
CH423GPIOPin = ch423_ns.class_(
"CH423GPIOPin", cg.GPIOPin, cg.Parented.template(CH423Component)
)
CONF_CH423 = "ch423"
# Note that no address is configurable - each register in the CH423 has a dedicated i2c address
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_ID): cv.declare_id(CH423Component),
cv.GenerateID(CONF_I2C_ID): cv.use_id(I2CBus),
}
).extend(cv.COMPONENT_SCHEMA)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
# Can't use register_i2c_device because there is no CONF_ADDRESS
parent = await cg.get_variable(config[CONF_I2C_ID])
cg.add(var.set_i2c_bus(parent))
# This is used as a final validation step so that modes have been fully transformed.
def pin_mode_check(pin_config, _):
if pin_config[CONF_MODE][CONF_INPUT] and pin_config[CONF_NUMBER] >= 8:
raise cv.Invalid("CH423 only supports input on pins 0-7")
if pin_config[CONF_MODE][CONF_OPEN_DRAIN] and pin_config[CONF_NUMBER] < 8:
raise cv.Invalid("CH423 only supports open drain output on pins 8-23")
ch423_id = pin_config[CONF_CH423]
pin_num = pin_config[CONF_NUMBER]
is_output = pin_config[CONF_MODE][CONF_OUTPUT]
is_open_drain = pin_config[CONF_MODE][CONF_OPEN_DRAIN]
# Track pin modes per CH423 instance in CORE.data
ch423_modes = CORE.data.setdefault(CONF_CH423, {})
if ch423_id not in ch423_modes:
ch423_modes[ch423_id] = {"gpio_output": None, "gpo_open_drain": None}
if pin_num < 8:
# GPIO pins (0-7): all must have same direction
if ch423_modes[ch423_id]["gpio_output"] is None:
ch423_modes[ch423_id]["gpio_output"] = is_output
elif ch423_modes[ch423_id]["gpio_output"] != is_output:
raise cv.Invalid(
"CH423 GPIO pins (0-7) must all be configured as input or all as output"
)
# GPO pins (8-23): all must have same open-drain setting
elif ch423_modes[ch423_id]["gpo_open_drain"] is None:
ch423_modes[ch423_id]["gpo_open_drain"] = is_open_drain
elif ch423_modes[ch423_id]["gpo_open_drain"] != is_open_drain:
raise cv.Invalid(
"CH423 GPO pins (8-23) must all be configured as push-pull or all as open-drain"
)
CH423_PIN_SCHEMA = pins.gpio_base_schema(
CH423GPIOPin,
cv.int_range(min=0, max=23),
modes=[CONF_INPUT, CONF_OUTPUT, CONF_OPEN_DRAIN],
).extend(
{
cv.Required(CONF_CH423): cv.use_id(CH423Component),
}
)
@pins.PIN_SCHEMA_REGISTRY.register(CONF_CH423, CH423_PIN_SCHEMA, pin_mode_check)
async def ch423_pin_to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
parent = await cg.get_variable(config[CONF_CH423])
cg.add(var.set_parent(parent))
num = config[CONF_NUMBER]
cg.add(var.set_pin(num))
cg.add(var.set_inverted(config[CONF_INVERTED]))
cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE])))
return var

View File

@@ -1,148 +0,0 @@
#include "ch423.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
namespace esphome::ch423 {
static constexpr uint8_t CH423_REG_SYS = 0x24; // Set system parameters (0x48 >> 1)
static constexpr uint8_t CH423_SYS_IO_OE = 0x01; // IO output enable
static constexpr uint8_t CH423_SYS_OD_EN = 0x04; // Open drain enable for OC pins
static constexpr uint8_t CH423_REG_IO = 0x30; // Write/read IO7-IO0 (0x60 >> 1)
static constexpr uint8_t CH423_REG_IO_RD = 0x26; // Read IO7-IO0 (0x4D >> 1, rounded down)
static constexpr uint8_t CH423_REG_OCL = 0x22; // Write OC7-OC0 (0x44 >> 1)
static constexpr uint8_t CH423_REG_OCH = 0x23; // Write OC15-OC8 (0x46 >> 1)
static const char *const TAG = "ch423";
void CH423Component::setup() {
// set outputs before mode
this->write_outputs_();
// Set system parameters and check for errors
bool success = this->write_reg_(CH423_REG_SYS, this->sys_params_);
// Only read inputs if pins are configured for input (IO_OE not set)
if (success && !(this->sys_params_ & CH423_SYS_IO_OE)) {
success = this->read_inputs_();
}
if (!success) {
ESP_LOGE(TAG, "CH423 not detected");
this->mark_failed();
return;
}
ESP_LOGCONFIG(TAG, "Initialization complete. Warning: %d, Error: %d", this->status_has_warning(),
this->status_has_error());
}
void CH423Component::loop() {
// Clear all the previously read flags.
this->pin_read_flags_ = 0x00;
}
void CH423Component::dump_config() {
ESP_LOGCONFIG(TAG, "CH423:");
if (this->is_failed()) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
}
}
void CH423Component::pin_mode(uint8_t pin, gpio::Flags flags) {
if (pin < 8) {
if (flags & gpio::FLAG_OUTPUT) {
this->sys_params_ |= CH423_SYS_IO_OE;
}
} else if (pin >= 8 && pin < 24) {
if (flags & gpio::FLAG_OPEN_DRAIN) {
this->sys_params_ |= CH423_SYS_OD_EN;
}
}
}
bool CH423Component::digital_read(uint8_t pin) {
if (this->pin_read_flags_ == 0 || this->pin_read_flags_ & (1 << pin)) {
// Read values on first access or in case it's being read again in the same loop
this->read_inputs_();
}
this->pin_read_flags_ |= (1 << pin);
return (this->input_bits_ & (1 << pin)) != 0;
}
void CH423Component::digital_write(uint8_t pin, bool value) {
if (value) {
this->output_bits_ |= (1 << pin);
} else {
this->output_bits_ &= ~(1 << pin);
}
this->write_outputs_();
}
bool CH423Component::read_inputs_() {
if (this->is_failed()) {
return false;
}
// reading inputs requires IO_OE to be 0
if (this->sys_params_ & CH423_SYS_IO_OE) {
return false;
}
uint8_t result = this->read_reg_(CH423_REG_IO_RD);
this->input_bits_ = result;
this->status_clear_warning();
return true;
}
// Write a register. Can't use the standard write_byte() method because there is no single pre-configured i2c address.
bool CH423Component::write_reg_(uint8_t reg, uint8_t value) {
auto err = this->bus_->write_readv(reg, &value, 1, nullptr, 0);
if (err != i2c::ERROR_OK) {
char buf[64];
ESPHOME_snprintf_P(buf, sizeof(buf), ESPHOME_PSTR("write failed for register 0x%X, error %d"), reg, err);
this->status_set_warning(buf);
return false;
}
this->status_clear_warning();
return true;
}
uint8_t CH423Component::read_reg_(uint8_t reg) {
uint8_t value;
auto err = this->bus_->write_readv(reg, nullptr, 0, &value, 1);
if (err != i2c::ERROR_OK) {
char buf[64];
ESPHOME_snprintf_P(buf, sizeof(buf), ESPHOME_PSTR("read failed for register 0x%X, error %d"), reg, err);
this->status_set_warning(buf);
return 0;
}
this->status_clear_warning();
return value;
}
bool CH423Component::write_outputs_() {
bool success = true;
// Write IO7-IO0
success &= this->write_reg_(CH423_REG_IO, static_cast<uint8_t>(this->output_bits_));
// Write OC7-OC0
success &= this->write_reg_(CH423_REG_OCL, static_cast<uint8_t>(this->output_bits_ >> 8));
// Write OC15-OC8
success &= this->write_reg_(CH423_REG_OCH, static_cast<uint8_t>(this->output_bits_ >> 16));
return success;
}
float CH423Component::get_setup_priority() const { return setup_priority::IO; }
// Run our loop() method very early in the loop, so that we cache read values
// before other components call our digital_read() method.
float CH423Component::get_loop_priority() const { return 9.0f; } // Just after WIFI
void CH423GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); }
bool CH423GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) ^ this->inverted_; }
void CH423GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value ^ this->inverted_); }
size_t CH423GPIOPin::dump_summary(char *buffer, size_t len) const {
return snprintf(buffer, len, "EXIO%u via CH423", this->pin_);
}
void CH423GPIOPin::set_flags(gpio::Flags flags) {
flags_ = flags;
this->parent_->pin_mode(this->pin_, flags);
}
} // namespace esphome::ch423

View File

@@ -1,67 +0,0 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome::ch423 {
class CH423Component : public Component, public i2c::I2CDevice {
public:
CH423Component() = default;
/// Check i2c availability and setup masks
void setup() override;
/// Poll for input changes periodically
void loop() override;
/// Helper function to read the value of a pin.
bool digital_read(uint8_t pin);
/// Helper function to write the value of a pin.
void digital_write(uint8_t pin, bool value);
/// Helper function to set the pin mode of a pin.
void pin_mode(uint8_t pin, gpio::Flags flags);
float get_setup_priority() const override;
float get_loop_priority() const override;
void dump_config() override;
protected:
bool write_reg_(uint8_t reg, uint8_t value);
uint8_t read_reg_(uint8_t reg);
bool read_inputs_();
bool write_outputs_();
/// The mask to write as output state - 1 means HIGH, 0 means LOW
uint32_t output_bits_{0x00};
/// Flags to check if read previously during this loop
uint8_t pin_read_flags_{0x00};
/// Copy of last read values
uint8_t input_bits_{0x00};
/// System parameters
uint8_t sys_params_{0x00};
};
/// Helper class to expose a CH423 pin as a GPIO pin.
class CH423GPIOPin : public GPIOPin {
public:
void setup() override{};
void pin_mode(gpio::Flags flags) override;
bool digital_read() override;
void digital_write(bool value) override;
size_t dump_summary(char *buffer, size_t len) const override;
void set_parent(CH423Component *parent) { parent_ = parent; }
void set_pin(uint8_t pin) { pin_ = pin; }
void set_inverted(bool inverted) { inverted_ = inverted; }
void set_flags(gpio::Flags flags);
gpio::Flags get_flags() const override { return this->flags_; }
protected:
CH423Component *parent_{};
uint8_t pin_{};
bool inverted_{};
gpio::Flags flags_{};
};
} // namespace esphome::ch423

View File

@@ -75,16 +75,16 @@ void ClimateCall::perform() {
ESP_LOGD(TAG, " Swing: %s", LOG_STR_ARG(swing_mode_s));
}
if (this->target_temperature_.has_value()) {
ESP_LOGD(TAG, " Target Temperature: %s%d.%02d", DECIMAL_2(*this->target_temperature_));
ESP_LOGD(TAG, " Target Temperature: %.2f", *this->target_temperature_);
}
if (this->target_temperature_low_.has_value()) {
ESP_LOGD(TAG, " Target Temperature Low: %s%d.%02d", DECIMAL_2(*this->target_temperature_low_));
ESP_LOGD(TAG, " Target Temperature Low: %.2f", *this->target_temperature_low_);
}
if (this->target_temperature_high_.has_value()) {
ESP_LOGD(TAG, " Target Temperature High: %s%d.%02d", DECIMAL_2(*this->target_temperature_high_));
ESP_LOGD(TAG, " Target Temperature High: %.2f", *this->target_temperature_high_);
}
if (this->target_humidity_.has_value()) {
ESP_LOGD(TAG, " Target Humidity: %d%%", (int) *this->target_humidity_);
ESP_LOGD(TAG, " Target Humidity: %.0f", *this->target_humidity_);
}
this->parent_->control(*this);
}
@@ -161,8 +161,7 @@ void ClimateCall::validate_() {
float low = *this->target_temperature_low_;
float high = *this->target_temperature_high_;
if (low > high) {
ESP_LOGW(TAG, " Target temperature low %s%d.%02d must be less than high %s%d.%02d", DECIMAL_2(low),
DECIMAL_2(high));
ESP_LOGW(TAG, " Target temperature low %.2f must be less than target temperature high %.2f", low, high);
this->target_temperature_low_.reset();
this->target_temperature_high_.reset();
}
@@ -361,7 +360,8 @@ void Climate::add_on_control_callback(std::function<void(ClimateCall &)> &&callb
static const uint32_t RESTORE_STATE_VERSION = 0x848EA6ADUL;
optional<ClimateDeviceRestoreState> Climate::restore_state_() {
this->rtc_ = this->make_entity_preference<ClimateDeviceRestoreState>(RESTORE_STATE_VERSION);
this->rtc_ = global_preferences->make_preference<ClimateDeviceRestoreState>(this->get_preference_hash() ^
RESTORE_STATE_VERSION);
ClimateDeviceRestoreState recovered{};
if (!this->rtc_.load(&recovered))
return {};
@@ -459,20 +459,20 @@ void Climate::publish_state() {
ESP_LOGD(TAG, " Swing Mode: %s", LOG_STR_ARG(climate_swing_mode_to_string(this->swing_mode)));
}
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) {
ESP_LOGD(TAG, " Current Temperature: %s%d.%02d°C", DECIMAL_2(this->current_temperature));
ESP_LOGD(TAG, " Current Temperature: %.2f°C", this->current_temperature);
}
if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
ESP_LOGD(TAG, " Target Temperature: Low: %s%d.%02d°C High: %s%d.%02d°C", DECIMAL_2(this->target_temperature_low),
DECIMAL_2(this->target_temperature_high));
ESP_LOGD(TAG, " Target Temperature: Low: %.2f°C High: %.2f°C", this->target_temperature_low,
this->target_temperature_high);
} else {
ESP_LOGD(TAG, " Target Temperature: %s%d.%02d°C", DECIMAL_2(this->target_temperature));
ESP_LOGD(TAG, " Target Temperature: %.2f°C", this->target_temperature);
}
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) {
ESP_LOGD(TAG, " Current Humidity: %d%%", (int) this->current_humidity);
ESP_LOGD(TAG, " Current Humidity: %.0f%%", this->current_humidity);
}
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
ESP_LOGD(TAG, " Target Humidity: %d%%", (int) this->target_humidity);
ESP_LOGD(TAG, " Target Humidity: %.0f%%", this->target_humidity);
}
// Send state to frontend
@@ -721,21 +721,21 @@ void Climate::dump_traits_(const char *tag) {
ESP_LOGCONFIG(tag, "ClimateTraits:");
ESP_LOGCONFIG(tag,
" Visual settings:\n"
" - Min temperature: %s%d.%d\n"
" - Max temperature: %s%d.%d\n"
" - Min temperature: %.1f\n"
" - Max temperature: %.1f\n"
" - Temperature step:\n"
" Target: %s%d.%d",
DECIMAL_1(traits.get_visual_min_temperature()), DECIMAL_1(traits.get_visual_max_temperature()),
DECIMAL_1(traits.get_visual_target_temperature_step()));
" Target: %.1f",
traits.get_visual_min_temperature(), traits.get_visual_max_temperature(),
traits.get_visual_target_temperature_step());
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) {
ESP_LOGCONFIG(tag, " Current: %s%d.%d", DECIMAL_1(traits.get_visual_current_temperature_step()));
ESP_LOGCONFIG(tag, " Current: %.1f", traits.get_visual_current_temperature_step());
}
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY |
climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) {
ESP_LOGCONFIG(tag,
" - Min humidity: %d\n"
" - Max humidity: %d",
(int) traits.get_visual_min_humidity(), (int) traits.get_visual_max_humidity());
" - Min humidity: %.0f\n"
" - Max humidity: %.0f",
traits.get_visual_min_humidity(), traits.get_visual_max_humidity());
}
if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {

View File

@@ -1,11 +1,11 @@
#include "cover.h"
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include <strings.h>
#include "esphome/core/log.h"
namespace esphome::cover {
static const char *const TAG = "cover";
@@ -22,24 +22,30 @@ const LogString *cover_command_to_str(float pos) {
return LOG_STR("UNKNOWN");
}
}
// Cover operation strings indexed by CoverOperation enum (0-2): IDLE, OPENING, CLOSING, plus UNKNOWN
PROGMEM_STRING_TABLE(CoverOperationStrings, "IDLE", "OPENING", "CLOSING", "UNKNOWN");
const LogString *cover_operation_to_str(CoverOperation op) {
return CoverOperationStrings::get_log_str(static_cast<uint8_t>(op), CoverOperationStrings::LAST_INDEX);
switch (op) {
case COVER_OPERATION_IDLE:
return LOG_STR("IDLE");
case COVER_OPERATION_OPENING:
return LOG_STR("OPENING");
case COVER_OPERATION_CLOSING:
return LOG_STR("CLOSING");
default:
return LOG_STR("UNKNOWN");
}
}
Cover::Cover() : position{COVER_OPEN} {}
CoverCall::CoverCall(Cover *parent) : parent_(parent) {}
CoverCall &CoverCall::set_command(const char *command) {
if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("OPEN")) == 0) {
if (strcasecmp(command, "OPEN") == 0) {
this->set_command_open();
} else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("CLOSE")) == 0) {
} else if (strcasecmp(command, "CLOSE") == 0) {
this->set_command_close();
} else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("STOP")) == 0) {
} else if (strcasecmp(command, "STOP") == 0) {
this->set_command_stop();
} else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("TOGGLE")) == 0) {
} else if (strcasecmp(command, "TOGGLE") == 0) {
this->set_command_toggle();
} else {
ESP_LOGW(TAG, "'%s' - Unrecognized command %s", this->parent_->get_name().c_str(), command);
@@ -79,13 +85,13 @@ void CoverCall::perform() {
}
if (this->position_.has_value()) {
if (traits.get_supports_position()) {
ESP_LOGD(TAG, " Position: %d%%", (int) (*this->position_ * 100.0f));
ESP_LOGD(TAG, " Position: %.0f%%", *this->position_ * 100.0f);
} else {
ESP_LOGD(TAG, " Command: %s", LOG_STR_ARG(cover_command_to_str(*this->position_)));
}
}
if (this->tilt_.has_value()) {
ESP_LOGD(TAG, " Tilt: %d%%", (int) (*this->tilt_ * 100.0f));
ESP_LOGD(TAG, " Tilt: %.0f%%", *this->tilt_ * 100.0f);
}
if (this->toggle_.has_value()) {
ESP_LOGD(TAG, " Command: TOGGLE");
@@ -105,7 +111,7 @@ void CoverCall::validate_() {
ESP_LOGW(TAG, "'%s': position unsupported", name);
this->position_.reset();
} else if (pos < 0.0f || pos > 1.0f) {
ESP_LOGW(TAG, "'%s': position %s%d.%02d out of range", name, DECIMAL_2(pos));
ESP_LOGW(TAG, "'%s': position %.2f out of range", name, pos);
this->position_ = clamp(pos, 0.0f, 1.0f);
}
}
@@ -115,7 +121,7 @@ void CoverCall::validate_() {
ESP_LOGW(TAG, "'%s': tilt unsupported", name);
this->tilt_.reset();
} else if (tilt < 0.0f || tilt > 1.0f) {
ESP_LOGW(TAG, "'%s': tilt %s%d.%02d out of range", name, DECIMAL_2(tilt));
ESP_LOGW(TAG, "'%s': tilt %.2f out of range", name, tilt);
this->tilt_ = clamp(tilt, 0.0f, 1.0f);
}
}
@@ -150,7 +156,7 @@ void Cover::publish_state(bool save) {
ESP_LOGD(TAG, "'%s' >>", this->name_.c_str());
auto traits = this->get_traits();
if (traits.get_supports_position()) {
ESP_LOGD(TAG, " Position: %d%%", (int) (this->position * 100.0f));
ESP_LOGD(TAG, " Position: %.0f%%", this->position * 100.0f);
} else {
if (this->position == COVER_OPEN) {
ESP_LOGD(TAG, " State: OPEN");
@@ -161,7 +167,7 @@ void Cover::publish_state(bool save) {
}
}
if (traits.get_supports_tilt()) {
ESP_LOGD(TAG, " Tilt: %d%%", (int) (this->tilt * 100.0f));
ESP_LOGD(TAG, " Tilt: %.0f%%", this->tilt * 100.0f);
}
ESP_LOGD(TAG, " Current Operation: %s", LOG_STR_ARG(cover_operation_to_str(this->current_operation)));
@@ -181,7 +187,7 @@ void Cover::publish_state(bool save) {
}
}
optional<CoverRestoreState> Cover::restore_state_() {
this->rtc_ = this->make_entity_preference<CoverRestoreState>();
this->rtc_ = global_preferences->make_preference<CoverRestoreState>(this->get_preference_hash());
CoverRestoreState recovered{};
if (!this->rtc_.load(&recovered))
return {};

View File

@@ -20,7 +20,9 @@ const extern float COVER_CLOSED;
if (traits_.get_is_assumed_state()) { \
ESP_LOGCONFIG(TAG, "%s Assumed State: YES", prefix); \
} \
LOG_ENTITY_DEVICE_CLASS(TAG, prefix, *(obj)); \
if (!(obj)->get_device_class_ref().empty()) { \
ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class_ref().c_str()); \
} \
}
class Cover;

View File

@@ -15,7 +15,9 @@ namespace esphome::datetime {
#define LOG_DATETIME_DATE(prefix, type, obj) \
if ((obj) != nullptr) { \
ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \
LOG_ENTITY_ICON(TAG, prefix, *(obj)); \
if (!(obj)->get_icon_ref().empty()) { \
ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \
} \
}
class DateCall;

View File

@@ -15,7 +15,9 @@ namespace esphome::datetime {
#define LOG_DATETIME_DATETIME(prefix, type, obj) \
if ((obj) != nullptr) { \
ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \
LOG_ENTITY_ICON(TAG, prefix, *(obj)); \
if (!(obj)->get_icon_ref().empty()) { \
ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \
} \
}
class DateTimeCall;

View File

@@ -15,7 +15,9 @@ namespace esphome::datetime {
#define LOG_DATETIME_TIME(prefix, type, obj) \
if ((obj) != nullptr) { \
ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \
LOG_ENTITY_ICON(TAG, prefix, *(obj)); \
if (!(obj)->get_icon_ref().empty()) { \
ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \
} \
}
class TimeCall;

View File

@@ -15,7 +15,7 @@ from esphome.const import (
CONF_UPDATE_INTERVAL,
SCHEDULER_DONT_RUN,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core import CoroPriority, coroutine_with_priority
IS_PLATFORM_COMPONENT = True
@@ -63,13 +63,11 @@ def validate_auto_clear(value):
return cv.boolean(value)
def basic_display_schema(default_update_interval: str = "1s") -> cv.Schema:
"""Create a basic display schema with configurable default update interval."""
return cv.Schema(
{
cv.Exclusive(CONF_LAMBDA, CONF_LAMBDA): cv.lambda_,
}
).extend(cv.polling_component_schema(default_update_interval))
BASIC_DISPLAY_SCHEMA = cv.Schema(
{
cv.Exclusive(CONF_LAMBDA, CONF_LAMBDA): cv.lambda_,
}
).extend(cv.polling_component_schema("1s"))
def _validate_test_card(config):
@@ -83,41 +81,34 @@ def _validate_test_card(config):
return config
def full_display_schema(default_update_interval: str = "1s") -> cv.Schema:
"""Create a full display schema with configurable default update interval."""
schema = basic_display_schema(default_update_interval).extend(
{
cv.Optional(CONF_ROTATION): validate_rotation,
cv.Exclusive(CONF_PAGES, CONF_LAMBDA): cv.All(
cv.ensure_list(
{
cv.GenerateID(): cv.declare_id(DisplayPage),
cv.Required(CONF_LAMBDA): cv.lambda_,
}
),
cv.Length(min=1),
),
cv.Optional(CONF_ON_PAGE_CHANGE): automation.validate_automation(
FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend(
{
cv.Optional(CONF_ROTATION): validate_rotation,
cv.Exclusive(CONF_PAGES, CONF_LAMBDA): cv.All(
cv.ensure_list(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
DisplayOnPageChangeTrigger
),
cv.Optional(CONF_FROM): cv.use_id(DisplayPage),
cv.Optional(CONF_TO): cv.use_id(DisplayPage),
cv.GenerateID(): cv.declare_id(DisplayPage),
cv.Required(CONF_LAMBDA): cv.lambda_,
}
),
cv.Optional(
CONF_AUTO_CLEAR_ENABLED, default=CONF_UNSPECIFIED
): validate_auto_clear,
cv.Optional(CONF_SHOW_TEST_CARD): cv.boolean,
}
)
schema.add_extra(_validate_test_card)
return schema
BASIC_DISPLAY_SCHEMA = basic_display_schema("1s")
FULL_DISPLAY_SCHEMA = full_display_schema("1s")
cv.Length(min=1),
),
cv.Optional(CONF_ON_PAGE_CHANGE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
DisplayOnPageChangeTrigger
),
cv.Optional(CONF_FROM): cv.use_id(DisplayPage),
cv.Optional(CONF_TO): cv.use_id(DisplayPage),
}
),
cv.Optional(
CONF_AUTO_CLEAR_ENABLED, default=CONF_UNSPECIFIED
): validate_auto_clear,
cv.Optional(CONF_SHOW_TEST_CARD): cv.boolean,
}
)
FULL_DISPLAY_SCHEMA.add_extra(_validate_test_card)
async def setup_display_core_(var, config):
@@ -231,8 +222,3 @@ async def display_is_displaying_page_to_code(config, condition_id, template_arg,
async def to_code(config):
cg.add_global(display_ns.using)
cg.add_define("USE_DISPLAY")
if CORE.is_esp32:
# Re-enable ESP-IDF's LCD driver (excluded by default to save compile time)
from esphome.components.esp32 import include_builtin_idf_component
include_builtin_idf_component("esp_lcd")

View File

@@ -1,57 +0,0 @@
import esphome.codegen as cg
from esphome.components import uart
import esphome.config_validation as cv
from esphome.const import CONF_ID, PLATFORM_ESP32, PLATFORM_ESP8266
CODEOWNERS = ["@SimonFischer04"]
DEPENDENCIES = ["uart"]
CONF_DLMS_METER_ID = "dlms_meter_id"
CONF_DECRYPTION_KEY = "decryption_key"
CONF_PROVIDER = "provider"
PROVIDERS = {"generic": 0, "netznoe": 1}
dlms_meter_component_ns = cg.esphome_ns.namespace("dlms_meter")
DlmsMeterComponent = dlms_meter_component_ns.class_(
"DlmsMeterComponent", cg.Component, uart.UARTDevice
)
def validate_key(value):
value = cv.string_strict(value)
if len(value) != 32:
raise cv.Invalid("Decryption key must be 32 hex characters (16 bytes)")
try:
return [int(value[i : i + 2], 16) for i in range(0, 32, 2)]
except ValueError as exc:
raise cv.Invalid("Decryption key must be hex values from 00 to FF") from exc
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(DlmsMeterComponent),
cv.Required(CONF_DECRYPTION_KEY): validate_key,
cv.Optional(CONF_PROVIDER, default="generic"): cv.enum(
PROVIDERS, lower=True
),
}
)
.extend(uart.UART_DEVICE_SCHEMA)
.extend(cv.COMPONENT_SCHEMA),
cv.only_on([PLATFORM_ESP8266, PLATFORM_ESP32]),
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"dlms_meter", baud_rate=2400, require_rx=True
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
key = ", ".join(str(b) for b in config[CONF_DECRYPTION_KEY])
cg.add(var.set_decryption_key(cg.RawExpression(f"{{{key}}}")))
cg.add(var.set_provider(PROVIDERS[config[CONF_PROVIDER]]))

View File

@@ -1,71 +0,0 @@
#pragma once
#include <cstdint>
namespace esphome::dlms_meter {
/*
+-------------------------------+
| Ciphering Service |
+-------------------------------+
| System Title Length |
+-------------------------------+
| |
| |
| |
| System |
| Title |
| |
| |
| |
+-------------------------------+
| Length | (1 or 3 Bytes)
+-------------------------------+
| Security Control Byte |
+-------------------------------+
| |
| Frame |
| Counter |
| |
+-------------------------------+
| |
~ ~
Encrypted Payload
~ ~
| |
+-------------------------------+
Ciphering Service: 0xDB (General-Glo-Ciphering)
System Title Length: 0x08
System Title: Unique ID of meter
Length: 1 Byte=Length <= 127, 3 Bytes=Length > 127 (0x82 & 2 Bytes length)
Security Control Byte:
- Bit 3…0: Security_Suite_Id
- Bit 4: "A" subfield: indicates that authentication is applied
- Bit 5: "E" subfield: indicates that encryption is applied
- Bit 6: Key_Set subfield: 0 = Unicast, 1 = Broadcast
- Bit 7: Indicates the use of compression.
*/
static constexpr uint8_t DLMS_HEADER_LENGTH = 16;
static constexpr uint8_t DLMS_HEADER_EXT_OFFSET = 2; // Extra offset for extended length header
static constexpr uint8_t DLMS_CIPHER_OFFSET = 0;
static constexpr uint8_t DLMS_SYST_OFFSET = 1;
static constexpr uint8_t DLMS_LENGTH_OFFSET = 10;
static constexpr uint8_t TWO_BYTE_LENGTH = 0x82;
static constexpr uint8_t DLMS_LENGTH_CORRECTION = 5; // Header bytes included in length field
static constexpr uint8_t DLMS_SECBYTE_OFFSET = 11;
static constexpr uint8_t DLMS_FRAMECOUNTER_OFFSET = 12;
static constexpr uint8_t DLMS_FRAMECOUNTER_LENGTH = 4;
static constexpr uint8_t DLMS_PAYLOAD_OFFSET = 16;
static constexpr uint8_t GLO_CIPHERING = 0xDB;
static constexpr uint8_t DATA_NOTIFICATION = 0x0F;
static constexpr uint8_t TIMESTAMP_DATETIME = 0x0C;
static constexpr uint16_t MAX_MESSAGE_LENGTH = 512; // Maximum size of message (when having 2 bytes length in header).
// Provider specific quirks
static constexpr uint8_t NETZ_NOE_MAGIC_BYTE = 0x81; // Magic length byte used by Netz NOE
static constexpr uint8_t NETZ_NOE_EXPECTED_MESSAGE_LENGTH = 0xF8;
static constexpr uint8_t NETZ_NOE_EXPECTED_SECURITY_CONTROL_BYTE = 0x20;
} // namespace esphome::dlms_meter

View File

@@ -1,468 +0,0 @@
#include "dlms_meter.h"
#include <cmath>
#if defined(USE_ESP8266_FRAMEWORK_ARDUINO)
#include <bearssl/bearssl.h>
#elif defined(USE_ESP32)
#include "mbedtls/esp_config.h"
#include "mbedtls/gcm.h"
#endif
namespace esphome::dlms_meter {
static constexpr const char *TAG = "dlms_meter";
void DlmsMeterComponent::dump_config() {
const char *provider_name = this->provider_ == PROVIDER_NETZNOE ? "Netz NOE" : "Generic";
ESP_LOGCONFIG(TAG,
"DLMS Meter:\n"
" Provider: %s\n"
" Read Timeout: %u ms",
provider_name, this->read_timeout_);
#define DLMS_METER_LOG_SENSOR(s) LOG_SENSOR(" ", #s, this->s##_sensor_);
DLMS_METER_SENSOR_LIST(DLMS_METER_LOG_SENSOR, )
#define DLMS_METER_LOG_TEXT_SENSOR(s) LOG_TEXT_SENSOR(" ", #s, this->s##_text_sensor_);
DLMS_METER_TEXT_SENSOR_LIST(DLMS_METER_LOG_TEXT_SENSOR, )
}
void DlmsMeterComponent::loop() {
// Read while data is available, netznoe uses two frames so allow 2x max frame length
while (this->available()) {
if (this->receive_buffer_.size() >= MBUS_MAX_FRAME_LENGTH * 2) {
ESP_LOGW(TAG, "Receive buffer full, dropping remaining bytes");
break;
}
uint8_t c;
this->read_byte(&c);
this->receive_buffer_.push_back(c);
this->last_read_ = millis();
}
if (!this->receive_buffer_.empty() && millis() - this->last_read_ > this->read_timeout_) {
this->mbus_payload_.clear();
if (!this->parse_mbus_(this->mbus_payload_))
return;
uint16_t message_length;
uint8_t systitle_length;
uint16_t header_offset;
if (!this->parse_dlms_(this->mbus_payload_, message_length, systitle_length, header_offset))
return;
if (message_length < DECODER_START_OFFSET || message_length > MAX_MESSAGE_LENGTH) {
ESP_LOGE(TAG, "DLMS: Message length invalid: %u", message_length);
this->receive_buffer_.clear();
return;
}
// Decrypt in place and then decode the OBIS codes
if (!this->decrypt_(this->mbus_payload_, message_length, systitle_length, header_offset))
return;
this->decode_obis_(&this->mbus_payload_[header_offset + DLMS_PAYLOAD_OFFSET], message_length);
}
}
bool DlmsMeterComponent::parse_mbus_(std::vector<uint8_t> &mbus_payload) {
ESP_LOGV(TAG, "Parsing M-Bus frames");
uint16_t frame_offset = 0; // Offset is used if the M-Bus message is split into multiple frames
while (frame_offset < this->receive_buffer_.size()) {
// Ensure enough bytes remain for the minimal intro header before accessing indices
if (this->receive_buffer_.size() - frame_offset < MBUS_HEADER_INTRO_LENGTH) {
ESP_LOGE(TAG, "MBUS: Not enough data for frame header (need %d, have %d)", MBUS_HEADER_INTRO_LENGTH,
(this->receive_buffer_.size() - frame_offset));
this->receive_buffer_.clear();
return false;
}
// Check start bytes
if (this->receive_buffer_[frame_offset + MBUS_START1_OFFSET] != START_BYTE_LONG_FRAME ||
this->receive_buffer_[frame_offset + MBUS_START2_OFFSET] != START_BYTE_LONG_FRAME) {
ESP_LOGE(TAG, "MBUS: Start bytes do not match");
this->receive_buffer_.clear();
return false;
}
// Both length bytes must be identical
if (this->receive_buffer_[frame_offset + MBUS_LENGTH1_OFFSET] !=
this->receive_buffer_[frame_offset + MBUS_LENGTH2_OFFSET]) {
ESP_LOGE(TAG, "MBUS: Length bytes do not match");
this->receive_buffer_.clear();
return false;
}
uint8_t frame_length = this->receive_buffer_[frame_offset + MBUS_LENGTH1_OFFSET]; // Get length of this frame
// Check if received data is enough for the given frame length
if (this->receive_buffer_.size() - frame_offset <
frame_length + 3) { // length field inside packet does not account for second start- + checksum- + stop- byte
ESP_LOGE(TAG, "MBUS: Frame too big for received data");
this->receive_buffer_.clear();
return false;
}
// Ensure we have full frame (header + payload + checksum + stop byte) before accessing stop byte
size_t required_total =
frame_length + MBUS_HEADER_INTRO_LENGTH + MBUS_FOOTER_LENGTH; // payload + header + 2 footer bytes
if (this->receive_buffer_.size() - frame_offset < required_total) {
ESP_LOGE(TAG, "MBUS: Incomplete frame (need %d, have %d)", (unsigned int) required_total,
this->receive_buffer_.size() - frame_offset);
this->receive_buffer_.clear();
return false;
}
if (this->receive_buffer_[frame_offset + frame_length + MBUS_HEADER_INTRO_LENGTH + MBUS_FOOTER_LENGTH - 1] !=
STOP_BYTE) {
ESP_LOGE(TAG, "MBUS: Invalid stop byte");
this->receive_buffer_.clear();
return false;
}
// Verify checksum: sum of all bytes starting at MBUS_HEADER_INTRO_LENGTH, take last byte
uint8_t checksum = 0; // use uint8_t so only the 8 least significant bits are stored
for (uint16_t i = 0; i < frame_length; i++) {
checksum += this->receive_buffer_[frame_offset + MBUS_HEADER_INTRO_LENGTH + i];
}
if (checksum != this->receive_buffer_[frame_offset + frame_length + MBUS_HEADER_INTRO_LENGTH]) {
ESP_LOGE(TAG, "MBUS: Invalid checksum: %x != %x", checksum,
this->receive_buffer_[frame_offset + frame_length + MBUS_HEADER_INTRO_LENGTH]);
this->receive_buffer_.clear();
return false;
}
mbus_payload.insert(mbus_payload.end(), &this->receive_buffer_[frame_offset + MBUS_FULL_HEADER_LENGTH],
&this->receive_buffer_[frame_offset + MBUS_HEADER_INTRO_LENGTH + frame_length]);
frame_offset += MBUS_HEADER_INTRO_LENGTH + frame_length + MBUS_FOOTER_LENGTH;
}
return true;
}
bool DlmsMeterComponent::parse_dlms_(const std::vector<uint8_t> &mbus_payload, uint16_t &message_length,
uint8_t &systitle_length, uint16_t &header_offset) {
ESP_LOGV(TAG, "Parsing DLMS header");
if (mbus_payload.size() < DLMS_HEADER_LENGTH + DLMS_HEADER_EXT_OFFSET) {
ESP_LOGE(TAG, "DLMS: Payload too short");
this->receive_buffer_.clear();
return false;
}
if (mbus_payload[DLMS_CIPHER_OFFSET] != GLO_CIPHERING) { // Only general-glo-ciphering is supported (0xDB)
ESP_LOGE(TAG, "DLMS: Unsupported cipher");
this->receive_buffer_.clear();
return false;
}
systitle_length = mbus_payload[DLMS_SYST_OFFSET];
if (systitle_length != 0x08) { // Only system titles with length of 8 are supported
ESP_LOGE(TAG, "DLMS: Unsupported system title length");
this->receive_buffer_.clear();
return false;
}
message_length = mbus_payload[DLMS_LENGTH_OFFSET];
header_offset = 0;
if (this->provider_ == PROVIDER_NETZNOE) {
// for some reason EVN seems to set the standard "length" field to 0x81 and then the actual length is in the next
// byte. Check some bytes to see if received data still matches expectation
if (message_length == NETZ_NOE_MAGIC_BYTE &&
mbus_payload[DLMS_LENGTH_OFFSET + 1] == NETZ_NOE_EXPECTED_MESSAGE_LENGTH &&
mbus_payload[DLMS_LENGTH_OFFSET + 2] == NETZ_NOE_EXPECTED_SECURITY_CONTROL_BYTE) {
message_length = mbus_payload[DLMS_LENGTH_OFFSET + 1];
header_offset = 1;
} else {
ESP_LOGE(TAG, "Wrong Length - Security Control Byte sequence detected for provider EVN");
}
} else {
if (message_length == TWO_BYTE_LENGTH) {
message_length = encode_uint16(mbus_payload[DLMS_LENGTH_OFFSET + 1], mbus_payload[DLMS_LENGTH_OFFSET + 2]);
header_offset = DLMS_HEADER_EXT_OFFSET;
}
}
if (message_length < DLMS_LENGTH_CORRECTION) {
ESP_LOGE(TAG, "DLMS: Message length too short: %u", message_length);
this->receive_buffer_.clear();
return false;
}
message_length -= DLMS_LENGTH_CORRECTION; // Correct message length due to part of header being included in length
if (mbus_payload.size() - DLMS_HEADER_LENGTH - header_offset != message_length) {
ESP_LOGV(TAG, "DLMS: Length mismatch - payload=%d, header=%d, offset=%d, message=%d", mbus_payload.size(),
DLMS_HEADER_LENGTH, header_offset, message_length);
ESP_LOGE(TAG, "DLMS: Message has invalid length");
this->receive_buffer_.clear();
return false;
}
if (mbus_payload[header_offset + DLMS_SECBYTE_OFFSET] != 0x21 &&
mbus_payload[header_offset + DLMS_SECBYTE_OFFSET] !=
0x20) { // Only certain security suite is supported (0x21 || 0x20)
ESP_LOGE(TAG, "DLMS: Unsupported security control byte");
this->receive_buffer_.clear();
return false;
}
return true;
}
bool DlmsMeterComponent::decrypt_(std::vector<uint8_t> &mbus_payload, uint16_t message_length, uint8_t systitle_length,
uint16_t header_offset) {
ESP_LOGV(TAG, "Decrypting payload");
uint8_t iv[12]; // Reserve space for the IV, always 12 bytes
// Copy system title to IV (System title is before length; no header offset needed!)
// Add 1 to the offset in order to skip the system title length byte
memcpy(&iv[0], &mbus_payload[DLMS_SYST_OFFSET + 1], systitle_length);
memcpy(&iv[8], &mbus_payload[header_offset + DLMS_FRAMECOUNTER_OFFSET],
DLMS_FRAMECOUNTER_LENGTH); // Copy frame counter to IV
uint8_t *payload_ptr = &mbus_payload[header_offset + DLMS_PAYLOAD_OFFSET];
#if defined(USE_ESP8266_FRAMEWORK_ARDUINO)
br_gcm_context gcm_ctx;
br_aes_ct_ctr_keys bc;
br_aes_ct_ctr_init(&bc, this->decryption_key_.data(), this->decryption_key_.size());
br_gcm_init(&gcm_ctx, &bc.vtable, br_ghash_ctmul32);
br_gcm_reset(&gcm_ctx, iv, sizeof(iv));
br_gcm_flip(&gcm_ctx);
br_gcm_run(&gcm_ctx, 0, payload_ptr, message_length);
#elif defined(USE_ESP32)
size_t outlen = 0;
mbedtls_gcm_context gcm_ctx;
mbedtls_gcm_init(&gcm_ctx);
mbedtls_gcm_setkey(&gcm_ctx, MBEDTLS_CIPHER_ID_AES, this->decryption_key_.data(), this->decryption_key_.size() * 8);
mbedtls_gcm_starts(&gcm_ctx, MBEDTLS_GCM_DECRYPT, iv, sizeof(iv));
auto ret = mbedtls_gcm_update(&gcm_ctx, payload_ptr, message_length, payload_ptr, message_length, &outlen);
mbedtls_gcm_free(&gcm_ctx);
if (ret != 0) {
ESP_LOGE(TAG, "Decryption failed with error: %d", ret);
this->receive_buffer_.clear();
return false;
}
#else
#error "Invalid Platform"
#endif
if (payload_ptr[0] != DATA_NOTIFICATION || payload_ptr[5] != TIMESTAMP_DATETIME) {
ESP_LOGE(TAG, "OBIS: Packet was decrypted but data is invalid");
this->receive_buffer_.clear();
return false;
}
ESP_LOGV(TAG, "Decrypted payload: %d bytes", message_length);
return true;
}
void DlmsMeterComponent::decode_obis_(uint8_t *plaintext, uint16_t message_length) {
ESP_LOGV(TAG, "Decoding payload");
MeterData data{};
uint16_t current_position = DECODER_START_OFFSET;
bool power_factor_found = false;
while (current_position + OBIS_CODE_OFFSET <= message_length) {
if (plaintext[current_position + OBIS_TYPE_OFFSET] != DataType::OCTET_STRING) {
ESP_LOGE(TAG, "OBIS: Unsupported OBIS header type: %x", plaintext[current_position + OBIS_TYPE_OFFSET]);
this->receive_buffer_.clear();
return;
}
uint8_t obis_code_length = plaintext[current_position + OBIS_LENGTH_OFFSET];
if (obis_code_length != OBIS_CODE_LENGTH_STANDARD && obis_code_length != OBIS_CODE_LENGTH_EXTENDED) {
ESP_LOGE(TAG, "OBIS: Unsupported OBIS header length: %x", obis_code_length);
this->receive_buffer_.clear();
return;
}
if (current_position + OBIS_CODE_OFFSET + obis_code_length > message_length) {
ESP_LOGE(TAG, "OBIS: Buffer too short for OBIS code");
this->receive_buffer_.clear();
return;
}
uint8_t *obis_code = &plaintext[current_position + OBIS_CODE_OFFSET];
uint8_t obis_medium = obis_code[OBIS_A];
uint16_t obis_cd = encode_uint16(obis_code[OBIS_C], obis_code[OBIS_D]);
bool timestamp_found = false;
bool meter_number_found = false;
if (this->provider_ == PROVIDER_NETZNOE) {
// Do not advance Position when reading the Timestamp at DECODER_START_OFFSET
if ((obis_code_length == OBIS_CODE_LENGTH_EXTENDED) && (current_position == DECODER_START_OFFSET)) {
timestamp_found = true;
} else if (power_factor_found) {
meter_number_found = true;
power_factor_found = false;
} else {
current_position += obis_code_length + OBIS_CODE_OFFSET; // Advance past code and position
}
} else {
current_position += obis_code_length + OBIS_CODE_OFFSET; // Advance past code, position and type
}
if (!timestamp_found && !meter_number_found && obis_medium != Medium::ELECTRICITY &&
obis_medium != Medium::ABSTRACT) {
ESP_LOGE(TAG, "OBIS: Unsupported OBIS medium: %x", obis_medium);
this->receive_buffer_.clear();
return;
}
if (current_position >= message_length) {
ESP_LOGE(TAG, "OBIS: Buffer too short for data type");
this->receive_buffer_.clear();
return;
}
float value = 0.0f;
uint8_t value_size = 0;
uint8_t data_type = plaintext[current_position];
current_position++;
switch (data_type) {
case DataType::DOUBLE_LONG_UNSIGNED: {
value_size = 4;
if (current_position + value_size > message_length) {
ESP_LOGE(TAG, "OBIS: Buffer too short for DOUBLE_LONG_UNSIGNED");
this->receive_buffer_.clear();
return;
}
value = encode_uint32(plaintext[current_position + 0], plaintext[current_position + 1],
plaintext[current_position + 2], plaintext[current_position + 3]);
current_position += value_size;
break;
}
case DataType::LONG_UNSIGNED: {
value_size = 2;
if (current_position + value_size > message_length) {
ESP_LOGE(TAG, "OBIS: Buffer too short for LONG_UNSIGNED");
this->receive_buffer_.clear();
return;
}
value = encode_uint16(plaintext[current_position + 0], plaintext[current_position + 1]);
current_position += value_size;
break;
}
case DataType::OCTET_STRING: {
uint8_t data_length = plaintext[current_position];
current_position++; // Advance past string length
if (current_position + data_length > message_length) {
ESP_LOGE(TAG, "OBIS: Buffer too short for OCTET_STRING");
this->receive_buffer_.clear();
return;
}
// Handle timestamp (normal OBIS code or NETZNOE special case)
if (obis_cd == OBIS_TIMESTAMP || timestamp_found) {
if (data_length < 8) {
ESP_LOGE(TAG, "OBIS: Timestamp data too short: %u", data_length);
this->receive_buffer_.clear();
return;
}
uint16_t year = encode_uint16(plaintext[current_position + 0], plaintext[current_position + 1]);
uint8_t month = plaintext[current_position + 2];
uint8_t day = plaintext[current_position + 3];
uint8_t hour = plaintext[current_position + 5];
uint8_t minute = plaintext[current_position + 6];
uint8_t second = plaintext[current_position + 7];
if (year > 9999 || month > 12 || day > 31 || hour > 23 || minute > 59 || second > 59) {
ESP_LOGE(TAG, "Invalid timestamp values: %04u-%02u-%02uT%02u:%02u:%02uZ", year, month, day, hour, minute,
second);
this->receive_buffer_.clear();
return;
}
snprintf(data.timestamp, sizeof(data.timestamp), "%04u-%02u-%02uT%02u:%02u:%02uZ", year, month, day, hour,
minute, second);
} else if (meter_number_found) {
snprintf(data.meternumber, sizeof(data.meternumber), "%.*s", data_length, &plaintext[current_position]);
}
current_position += data_length;
break;
}
default:
ESP_LOGE(TAG, "OBIS: Unsupported OBIS data type: %x", data_type);
this->receive_buffer_.clear();
return;
}
// Skip break after data
if (this->provider_ == PROVIDER_NETZNOE) {
// Don't skip the break on the first timestamp, as there's none
if (!timestamp_found) {
current_position += 2;
}
} else {
current_position += 2;
}
// Check for additional data (scaler-unit structure)
if (current_position < message_length && plaintext[current_position] == DataType::INTEGER) {
// Apply scaler: real_value = raw_value × 10^scaler
if (current_position + 1 < message_length) {
int8_t scaler = static_cast<int8_t>(plaintext[current_position + 1]);
if (scaler != 0) {
value *= powf(10.0f, scaler);
}
}
// on EVN Meters there is no additional break
if (this->provider_ == PROVIDER_NETZNOE) {
current_position += 4;
} else {
current_position += 6;
}
}
// Handle numeric values (LONG_UNSIGNED and DOUBLE_LONG_UNSIGNED)
if (value_size > 0) {
switch (obis_cd) {
case OBIS_VOLTAGE_L1:
data.voltage_l1 = value;
break;
case OBIS_VOLTAGE_L2:
data.voltage_l2 = value;
break;
case OBIS_VOLTAGE_L3:
data.voltage_l3 = value;
break;
case OBIS_CURRENT_L1:
data.current_l1 = value;
break;
case OBIS_CURRENT_L2:
data.current_l2 = value;
break;
case OBIS_CURRENT_L3:
data.current_l3 = value;
break;
case OBIS_ACTIVE_POWER_PLUS:
data.active_power_plus = value;
break;
case OBIS_ACTIVE_POWER_MINUS:
data.active_power_minus = value;
break;
case OBIS_ACTIVE_ENERGY_PLUS:
data.active_energy_plus = value;
break;
case OBIS_ACTIVE_ENERGY_MINUS:
data.active_energy_minus = value;
break;
case OBIS_REACTIVE_ENERGY_PLUS:
data.reactive_energy_plus = value;
break;
case OBIS_REACTIVE_ENERGY_MINUS:
data.reactive_energy_minus = value;
break;
case OBIS_POWER_FACTOR:
data.power_factor = value;
power_factor_found = true;
break;
default:
ESP_LOGW(TAG, "Unsupported OBIS code 0x%04X", obis_cd);
}
}
}
this->receive_buffer_.clear();
ESP_LOGI(TAG, "Received valid data");
this->publish_sensors(data);
this->status_clear_warning();
}
} // namespace esphome::dlms_meter

View File

@@ -1,96 +0,0 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/log.h"
#ifdef USE_SENSOR
#include "esphome/components/sensor/sensor.h"
#endif
#ifdef USE_TEXT_SENSOR
#include "esphome/components/text_sensor/text_sensor.h"
#endif
#include "esphome/components/uart/uart.h"
#include "mbus.h"
#include "dlms.h"
#include "obis.h"
#include <array>
#include <vector>
namespace esphome::dlms_meter {
#ifndef DLMS_METER_SENSOR_LIST
#define DLMS_METER_SENSOR_LIST(F, SEP)
#endif
#ifndef DLMS_METER_TEXT_SENSOR_LIST
#define DLMS_METER_TEXT_SENSOR_LIST(F, SEP)
#endif
struct MeterData {
float voltage_l1 = 0.0f; // Voltage L1
float voltage_l2 = 0.0f; // Voltage L2
float voltage_l3 = 0.0f; // Voltage L3
float current_l1 = 0.0f; // Current L1
float current_l2 = 0.0f; // Current L2
float current_l3 = 0.0f; // Current L3
float active_power_plus = 0.0f; // Active power taken from grid
float active_power_minus = 0.0f; // Active power put into grid
float active_energy_plus = 0.0f; // Active energy taken from grid
float active_energy_minus = 0.0f; // Active energy put into grid
float reactive_energy_plus = 0.0f; // Reactive energy taken from grid
float reactive_energy_minus = 0.0f; // Reactive energy put into grid
char timestamp[27]{}; // Text sensor for the timestamp value
// Netz NOE
float power_factor = 0.0f; // Power Factor
char meternumber[13]{}; // Text sensor for the meterNumber value
};
// Provider constants
enum Providers : uint32_t { PROVIDER_GENERIC = 0x00, PROVIDER_NETZNOE = 0x01 };
class DlmsMeterComponent : public Component, public uart::UARTDevice {
public:
DlmsMeterComponent() = default;
void dump_config() override;
void loop() override;
void set_decryption_key(const std::array<uint8_t, 16> &key) { this->decryption_key_ = key; }
void set_provider(uint32_t provider) { this->provider_ = provider; }
void publish_sensors(MeterData &data) {
#define DLMS_METER_PUBLISH_SENSOR(s) \
if (this->s##_sensor_ != nullptr) \
s##_sensor_->publish_state(data.s);
DLMS_METER_SENSOR_LIST(DLMS_METER_PUBLISH_SENSOR, )
#define DLMS_METER_PUBLISH_TEXT_SENSOR(s) \
if (this->s##_text_sensor_ != nullptr) \
s##_text_sensor_->publish_state(data.s);
DLMS_METER_TEXT_SENSOR_LIST(DLMS_METER_PUBLISH_TEXT_SENSOR, )
}
DLMS_METER_SENSOR_LIST(SUB_SENSOR, )
DLMS_METER_TEXT_SENSOR_LIST(SUB_TEXT_SENSOR, )
protected:
bool parse_mbus_(std::vector<uint8_t> &mbus_payload);
bool parse_dlms_(const std::vector<uint8_t> &mbus_payload, uint16_t &message_length, uint8_t &systitle_length,
uint16_t &header_offset);
bool decrypt_(std::vector<uint8_t> &mbus_payload, uint16_t message_length, uint8_t systitle_length,
uint16_t header_offset);
void decode_obis_(uint8_t *plaintext, uint16_t message_length);
std::vector<uint8_t> receive_buffer_; // Stores the packet currently being received
std::vector<uint8_t> mbus_payload_; // Parsed M-Bus payload, reused to avoid heap churn
uint32_t last_read_ = 0; // Timestamp when data was last read
uint32_t read_timeout_ = 1000; // Time to wait after last byte before considering data complete
uint32_t provider_ = PROVIDER_GENERIC; // Provider of the meter / your grid operator
std::array<uint8_t, 16> decryption_key_;
};
} // namespace esphome::dlms_meter

View File

@@ -1,69 +0,0 @@
#pragma once
#include <cstdint>
namespace esphome::dlms_meter {
/*
+----------------------------------------------------+ -
| Start Character [0x68] | \
+----------------------------------------------------+ |
| Data Length (L) | |
+----------------------------------------------------+ |
| Data Length Repeat (L) | |
+----------------------------------------------------+ > M-Bus Data link layer
| Start Character Repeat [0x68] | |
+----------------------------------------------------+ |
| Control/Function Field (C) | |
+----------------------------------------------------+ |
| Address Field (A) | /
+----------------------------------------------------+ -
| Control Information Field (CI) | \
+----------------------------------------------------+ |
| Source Transport Service Access Point (STSAP) | > DLMS/COSEM M-Bus transport layer
+----------------------------------------------------+ |
| Destination Transport Service Access Point (DTSAP) | /
+----------------------------------------------------+ -
| | \
~ ~ |
Data > DLMS/COSEM Application Layer
~ ~ |
| | /
+----------------------------------------------------+ -
| Checksum | \
+----------------------------------------------------+ > M-Bus Data link layer
| Stop Character [0x16] | /
+----------------------------------------------------+ -
Data_Length = L - C - A - CI
Each line (except Data) is one Byte
Possible Values found in publicly available docs:
- C: 0x53/0x73 (SND_UD)
- A: FF (Broadcast)
- CI: 0x00-0x1F/0x60/0x61/0x7C/0x7D
- STSAP: 0x01 (Management Logical Device ID 1 of the meter)
- DTSAP: 0x67 (Consumer Information Push Client ID 103)
*/
// MBUS start bytes for different telegram formats:
// - Single Character: 0xE5 (length=1)
// - Short Frame: 0x10 (length=5)
// - Control Frame: 0x68 (length=9)
// - Long Frame: 0x68 (length=9+data_length)
// This component currently only uses Long Frame.
static constexpr uint8_t START_BYTE_SINGLE_CHARACTER = 0xE5;
static constexpr uint8_t START_BYTE_SHORT_FRAME = 0x10;
static constexpr uint8_t START_BYTE_CONTROL_FRAME = 0x68;
static constexpr uint8_t START_BYTE_LONG_FRAME = 0x68;
static constexpr uint8_t MBUS_HEADER_INTRO_LENGTH = 4; // Header length for the intro (0x68, length, length, 0x68)
static constexpr uint8_t MBUS_FULL_HEADER_LENGTH = 9; // Total header length
static constexpr uint8_t MBUS_FOOTER_LENGTH = 2; // Footer after frame
static constexpr uint8_t MBUS_MAX_FRAME_LENGTH = 250; // Maximum size of frame
static constexpr uint8_t MBUS_START1_OFFSET = 0; // Offset of first start byte
static constexpr uint8_t MBUS_LENGTH1_OFFSET = 1; // Offset of first length byte
static constexpr uint8_t MBUS_LENGTH2_OFFSET = 2; // Offset of (duplicated) second length byte
static constexpr uint8_t MBUS_START2_OFFSET = 3; // Offset of (duplicated) second start byte
static constexpr uint8_t STOP_BYTE = 0x16;
} // namespace esphome::dlms_meter

View File

@@ -1,94 +0,0 @@
#pragma once
#include <cstdint>
namespace esphome::dlms_meter {
// Data types as per specification
enum DataType {
NULL_DATA = 0x00,
BOOLEAN = 0x03,
BIT_STRING = 0x04,
DOUBLE_LONG = 0x05,
DOUBLE_LONG_UNSIGNED = 0x06,
OCTET_STRING = 0x09,
VISIBLE_STRING = 0x0A,
UTF8_STRING = 0x0C,
BINARY_CODED_DECIMAL = 0x0D,
INTEGER = 0x0F,
LONG = 0x10,
UNSIGNED = 0x11,
LONG_UNSIGNED = 0x12,
LONG64 = 0x14,
LONG64_UNSIGNED = 0x15,
ENUM = 0x16,
FLOAT32 = 0x17,
FLOAT64 = 0x18,
DATE_TIME = 0x19,
DATE = 0x1A,
TIME = 0x1B,
ARRAY = 0x01,
STRUCTURE = 0x02,
COMPACT_ARRAY = 0x13
};
enum Medium {
ABSTRACT = 0x00,
ELECTRICITY = 0x01,
HEAT_COST_ALLOCATOR = 0x04,
COOLING = 0x05,
HEAT = 0x06,
GAS = 0x07,
COLD_WATER = 0x08,
HOT_WATER = 0x09,
OIL = 0x10,
COMPRESSED_AIR = 0x11,
NITROGEN = 0x12
};
// Data structure
static constexpr uint8_t DECODER_START_OFFSET = 20; // Skip header, timestamp and break block
static constexpr uint8_t OBIS_TYPE_OFFSET = 0;
static constexpr uint8_t OBIS_LENGTH_OFFSET = 1;
static constexpr uint8_t OBIS_CODE_OFFSET = 2;
static constexpr uint8_t OBIS_CODE_LENGTH_STANDARD = 0x06; // 6-byte OBIS code (A.B.C.D.E.F)
static constexpr uint8_t OBIS_CODE_LENGTH_EXTENDED = 0x0C; // 12-byte extended OBIS code
static constexpr uint8_t OBIS_A = 0;
static constexpr uint8_t OBIS_B = 1;
static constexpr uint8_t OBIS_C = 2;
static constexpr uint8_t OBIS_D = 3;
static constexpr uint8_t OBIS_E = 4;
static constexpr uint8_t OBIS_F = 5;
// Metadata
static constexpr uint16_t OBIS_TIMESTAMP = 0x0100;
static constexpr uint16_t OBIS_SERIAL_NUMBER = 0x6001;
static constexpr uint16_t OBIS_DEVICE_NAME = 0x2A00;
// Voltage
static constexpr uint16_t OBIS_VOLTAGE_L1 = 0x2007;
static constexpr uint16_t OBIS_VOLTAGE_L2 = 0x3407;
static constexpr uint16_t OBIS_VOLTAGE_L3 = 0x4807;
// Current
static constexpr uint16_t OBIS_CURRENT_L1 = 0x1F07;
static constexpr uint16_t OBIS_CURRENT_L2 = 0x3307;
static constexpr uint16_t OBIS_CURRENT_L3 = 0x4707;
// Power
static constexpr uint16_t OBIS_ACTIVE_POWER_PLUS = 0x0107;
static constexpr uint16_t OBIS_ACTIVE_POWER_MINUS = 0x0207;
// Active energy
static constexpr uint16_t OBIS_ACTIVE_ENERGY_PLUS = 0x0108;
static constexpr uint16_t OBIS_ACTIVE_ENERGY_MINUS = 0x0208;
// Reactive energy
static constexpr uint16_t OBIS_REACTIVE_ENERGY_PLUS = 0x0308;
static constexpr uint16_t OBIS_REACTIVE_ENERGY_MINUS = 0x0408;
// Netz NOE specific
static constexpr uint16_t OBIS_POWER_FACTOR = 0x0D07;
} // namespace esphome::dlms_meter

View File

@@ -1,124 +0,0 @@
import esphome.codegen as cg
from esphome.components import sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_POWER,
DEVICE_CLASS_POWER_FACTOR,
DEVICE_CLASS_VOLTAGE,
STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL_INCREASING,
UNIT_AMPERE,
UNIT_VOLT,
UNIT_WATT,
UNIT_WATT_HOURS,
)
from .. import CONF_DLMS_METER_ID, DlmsMeterComponent
AUTO_LOAD = ["dlms_meter"]
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_DLMS_METER_ID): cv.use_id(DlmsMeterComponent),
cv.Optional("voltage_l1"): sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT,
accuracy_decimals=1,
device_class=DEVICE_CLASS_VOLTAGE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("voltage_l2"): sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT,
accuracy_decimals=1,
device_class=DEVICE_CLASS_VOLTAGE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("voltage_l3"): sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT,
accuracy_decimals=1,
device_class=DEVICE_CLASS_VOLTAGE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("current_l1"): sensor.sensor_schema(
unit_of_measurement=UNIT_AMPERE,
accuracy_decimals=2,
device_class=DEVICE_CLASS_CURRENT,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("current_l2"): sensor.sensor_schema(
unit_of_measurement=UNIT_AMPERE,
accuracy_decimals=2,
device_class=DEVICE_CLASS_CURRENT,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("current_l3"): sensor.sensor_schema(
unit_of_measurement=UNIT_AMPERE,
accuracy_decimals=2,
device_class=DEVICE_CLASS_CURRENT,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("active_power_plus"): sensor.sensor_schema(
unit_of_measurement=UNIT_WATT,
accuracy_decimals=0,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("active_power_minus"): sensor.sensor_schema(
unit_of_measurement=UNIT_WATT,
accuracy_decimals=0,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("active_energy_plus"): sensor.sensor_schema(
unit_of_measurement=UNIT_WATT_HOURS,
accuracy_decimals=0,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("active_energy_minus"): sensor.sensor_schema(
unit_of_measurement=UNIT_WATT_HOURS,
accuracy_decimals=0,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("reactive_energy_plus"): sensor.sensor_schema(
unit_of_measurement=UNIT_WATT_HOURS,
accuracy_decimals=0,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("reactive_energy_minus"): sensor.sensor_schema(
unit_of_measurement=UNIT_WATT_HOURS,
accuracy_decimals=0,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
# Netz NOE
cv.Optional("power_factor"): sensor.sensor_schema(
accuracy_decimals=3,
device_class=DEVICE_CLASS_POWER_FACTOR,
state_class=STATE_CLASS_MEASUREMENT,
),
}
).extend(cv.COMPONENT_SCHEMA)
async def to_code(config):
hub = await cg.get_variable(config[CONF_DLMS_METER_ID])
sensors = []
for key, conf in config.items():
if not isinstance(conf, dict):
continue
id = conf[CONF_ID]
if id and id.type == sensor.Sensor:
sens = await sensor.new_sensor(conf)
cg.add(getattr(hub, f"set_{key}_sensor")(sens))
sensors.append(f"F({key})")
if sensors:
cg.add_define(
"DLMS_METER_SENSOR_LIST(F, sep)", cg.RawExpression(" sep ".join(sensors))
)

View File

@@ -1,37 +0,0 @@
import esphome.codegen as cg
from esphome.components import text_sensor
import esphome.config_validation as cv
from esphome.const import CONF_ID
from .. import CONF_DLMS_METER_ID, DlmsMeterComponent
AUTO_LOAD = ["dlms_meter"]
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_DLMS_METER_ID): cv.use_id(DlmsMeterComponent),
cv.Optional("timestamp"): text_sensor.text_sensor_schema(),
# Netz NOE
cv.Optional("meternumber"): text_sensor.text_sensor_schema(),
}
).extend(cv.COMPONENT_SCHEMA)
async def to_code(config):
hub = await cg.get_variable(config[CONF_DLMS_METER_ID])
text_sensors = []
for key, conf in config.items():
if not isinstance(conf, dict):
continue
id = conf[CONF_ID]
if id and id.type == text_sensor.TextSensor:
sens = await text_sensor.new_text_sensor(conf)
cg.add(getattr(hub, f"set_{key}_text_sensor")(sens))
text_sensors.append(f"F({key})")
if text_sensors:
cg.add_define(
"DLMS_METER_TEXT_SENSOR_LIST(F, sep)",
cg.RawExpression(" sep ".join(text_sensors)),
)

View File

@@ -41,7 +41,7 @@ void DutyTimeSensor::setup() {
uint32_t seconds = 0;
if (this->restore_) {
this->pref_ = this->make_entity_preference<uint32_t>();
this->pref_ = global_preferences->make_preference<uint32_t>(this->get_preference_hash());
this->pref_.load(&seconds);
}

View File

@@ -31,7 +31,6 @@ from esphome.const import (
CONF_TRANSFORM,
CONF_UPDATE_INTERVAL,
CONF_WIDTH,
SCHEDULER_DONT_RUN,
)
from esphome.cpp_generator import RawExpression
from esphome.final_validate import full_config
@@ -73,10 +72,12 @@ TRANSFORM_OPTIONS = {CONF_MIRROR_X, CONF_MIRROR_Y, CONF_SWAP_XY}
def model_schema(config):
model = MODELS[config[CONF_MODEL]]
class_name = epaper_spi_ns.class_(model.class_name, EPaperBase)
minimum_update_interval = update_interval(
model.get_default(CONF_MINIMUM_UPDATE_INTERVAL, "1s")
)
cv_dimensions = cv.Optional if model.get_default(CONF_WIDTH) else cv.Required
return (
display.full_display_schema("60s")
.extend(
display.FULL_DISPLAY_SCHEMA.extend(
spi.spi_device_schema(
cs_pin_required=False,
default_mode="MODE0",
@@ -93,6 +94,9 @@ def model_schema(config):
{
cv.Optional(CONF_ROTATION, default=0): validate_rotation,
cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True),
cv.Optional(CONF_UPDATE_INTERVAL, default=cv.UNDEFINED): cv.All(
update_interval, cv.Range(min=minimum_update_interval)
),
cv.Optional(CONF_TRANSFORM): cv.Schema(
{
cv.Required(CONF_MIRROR_X): cv.boolean,
@@ -146,22 +150,15 @@ def _final_validate(config):
global_config = full_config.get()
from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN
# If no drawing methods are configured, and LVGL is not enabled, show a test card
if (
CONF_LAMBDA not in config
and CONF_PAGES not in config
and LVGL_DOMAIN not in global_config
):
config[CONF_SHOW_TEST_CARD] = True
interval = config[CONF_UPDATE_INTERVAL]
if interval != SCHEDULER_DONT_RUN:
model = MODELS[config[CONF_MODEL]]
minimum = update_interval(model.get_default(CONF_MINIMUM_UPDATE_INTERVAL, "1s"))
if interval < minimum:
raise cv.Invalid(
f"update_interval must be at least {minimum} for {model.name}, got {interval}"
)
if CONF_LAMBDA not in config and CONF_PAGES not in config:
if LVGL_DOMAIN in global_config:
if CONF_UPDATE_INTERVAL not in config:
config[CONF_UPDATE_INTERVAL] = update_interval("never")
else:
# If no drawing methods are configured, and LVGL is not enabled, show a test card
config[CONF_SHOW_TEST_CARD] = True
elif CONF_UPDATE_INTERVAL not in config:
config[CONF_UPDATE_INTERVAL] = update_interval("1min")
return config

View File

@@ -2,8 +2,7 @@ import esphome.codegen as cg
from esphome.components import i2c
from esphome.components.audio_dac import AudioDac
import esphome.config_validation as cv
from esphome.const import CONF_AUDIO_DAC, CONF_BITS_PER_SAMPLE, CONF_ID
import esphome.final_validate as fv
from esphome.const import CONF_ID
CODEOWNERS = ["@kbx81"]
DEPENDENCIES = ["i2c"]
@@ -22,29 +21,6 @@ CONFIG_SCHEMA = (
)
def _final_validate(config):
full_config = fv.full_config.get()
# Check all speaker configurations for ones that reference this es8156
speaker_configs = full_config.get("speaker", [])
for speaker_config in speaker_configs:
audio_dac_id = speaker_config.get(CONF_AUDIO_DAC)
if (
audio_dac_id is not None
and audio_dac_id == config[CONF_ID]
and (bits_per_sample := speaker_config.get(CONF_BITS_PER_SAMPLE))
is not None
and bits_per_sample > 24
):
raise cv.Invalid(
f"ES8156 does not support more than 24 bits per sample. "
f"The speaker referencing this audio_dac has bits_per_sample set to {bits_per_sample}."
)
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)

View File

@@ -17,61 +17,24 @@ static const char *const TAG = "es8156";
}
void ES8156::setup() {
// REG02 MODE CONFIG 1: Enable software mode for I2C control of volume/mute
// Bit 2: SOFT_MODE_SEL=1 (software mode enabled)
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG02_SCLK_MODE, 0x04));
// Analog system configuration (active-low power down bits, active-high enables)
// REG20 ANALOG SYSTEM: Configure analog signal path
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG20_ANALOG_SYS1, 0x2A));
// REG21 ANALOG SYSTEM: VSEL=0x1C (bias level ~120%), normal VREF ramp speed
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG21_ANALOG_SYS2, 0x3C));
// REG22 ANALOG SYSTEM: Line out mode (HPSW=0), OUT_MUTE=0 (not muted)
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG22_ANALOG_SYS3, 0x00));
// REG24 ANALOG SYSTEM: Low power mode for VREFBUF, HPCOM, DACVRP; DAC normal power
// Bits 2:0 = 0x07: LPVREFBUF=1, LPHPCOM=1, LPDACVRP=1, LPDAC=0
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG24_ANALOG_LP, 0x07));
// REG23 ANALOG SYSTEM: Lowest bias (IBIAS_SW=0), VMIDLVL=VDDA/2, normal impedance
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG23_ANALOG_SYS4, 0x00));
// Timing and interface configuration
// REG0A/0B TIME CONTROL: Fast state machine transitions
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG0A_TIME_CONTROL1, 0x01));
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG0B_TIME_CONTROL2, 0x01));
// REG11 SDP INTERFACE CONFIG: Default I2S format (24-bit, I2S mode)
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG11_DAC_SDP, 0x00));
// REG19 EQ CONTROL 1: EQ disabled (EQ_ON=0), EQ_BAND_NUM=2
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG19_EQ_CONTROL1, 0x20));
// REG0D P2S CONTROL: Parallel-to-serial converter settings
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG0D_P2S_CONTROL, 0x14));
// REG09 MISC CONTROL 2: Default settings
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG09_MISC_CONTROL2, 0x00));
// REG18 MISC CONTROL 3: Stereo channel routing, no inversion
// Bits 5:4 CHN_CROSS: 0=L→L/R→R, 1=L to both, 2=R to both, 3=swap L/R
// Bits 3:2: LCH_INV/RCH_INV channel inversion
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG18_MISC_CONTROL3, 0x00));
// REG08 CLOCK OFF: Enable all internal clocks (0x3F = all clock gates open)
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG08_CLOCK_ON_OFF, 0x3F));
// REG00 RESET CONTROL: Reset sequence
// First: RST_DIG=1 (assert digital reset)
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG00_RESET, 0x02));
// Then: CSM_ON=1 (enable chip state machine), RST_DIG=1
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG00_RESET, 0x03));
// REG25 ANALOG SYSTEM: Power up analog blocks
// VMIDSEL=2 (normal VMID operation), PDN_ANA=0, ENREFR=0, ENHPCOM=0
// PDN_DACVREFGEN=0, PDN_VREFBUF=0, PDN_DAC=0 (all enabled)
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG25_ANALOG_SYS5, 0x20));
}

View File

@@ -5,7 +5,6 @@ import logging
import os
from pathlib import Path
import re
import shutil
from esphome import yaml_util
import esphome.codegen as cg
@@ -51,14 +50,11 @@ from esphome.writer import clean_cmake_cache
from .boards import BOARDS, STANDARD_BOARDS
from .const import ( # noqa
KEY_ARDUINO_LIBRARIES,
KEY_BOARD,
KEY_COMPONENTS,
KEY_ESP32,
KEY_EXCLUDE_COMPONENTS,
KEY_EXTRA_BUILD_FILES,
KEY_FLASH_SIZE,
KEY_FULL_CERT_BUNDLE,
KEY_PATH,
KEY_REF,
KEY_REPO,
@@ -89,7 +85,6 @@ IS_TARGET_PLATFORM = True
CONF_ASSERTION_LEVEL = "assertion_level"
CONF_COMPILER_OPTIMIZATION = "compiler_optimization"
CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES = "enable_idf_experimental_features"
CONF_INCLUDE_BUILTIN_IDF_COMPONENTS = "include_builtin_idf_components"
CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert"
CONF_ENABLE_OTA_ROLLBACK = "enable_ota_rollback"
CONF_EXECUTE_FROM_PSRAM = "execute_from_psram"
@@ -118,204 +113,6 @@ COMPILER_OPTIMIZATIONS = {
"SIZE": "CONFIG_COMPILER_OPTIMIZATION_SIZE",
}
# ESP-IDF components excluded by default to reduce compile time.
# Components can be re-enabled by calling include_builtin_idf_component() in to_code().
#
# Cannot be excluded (dependencies of required components):
# - "console": espressif/mdns unconditionally depends on it
# - "sdmmc": driver -> esp_driver_sdmmc -> sdmmc dependency chain
DEFAULT_EXCLUDED_IDF_COMPONENTS = (
"cmock", # Unit testing mock framework - ESPHome doesn't use IDF's testing
"driver", # Legacy driver shim - only needed by esp32_touch, esp32_can for legacy headers
"esp_adc", # ADC driver - only needed by adc component
"esp_driver_dac", # DAC driver - only needed by esp32_dac component
"esp_driver_i2s", # I2S driver - only needed by i2s_audio component
"esp_driver_mcpwm", # MCPWM driver - ESPHome doesn't use motor control PWM
"esp_driver_rmt", # RMT driver - only needed by remote_transmitter/receiver, neopixelbus
"esp_driver_touch_sens", # Touch sensor driver - only needed by esp32_touch
"esp_driver_twai", # TWAI/CAN driver - only needed by esp32_can component
"esp_eth", # Ethernet driver - only needed by ethernet component
"esp_hid", # HID host/device support - ESPHome doesn't implement HID functionality
"esp_http_client", # HTTP client - only needed by http_request component
"esp_https_ota", # ESP-IDF HTTPS OTA - ESPHome has its own OTA implementation
"esp_https_server", # HTTPS server - ESPHome has its own web server
"esp_lcd", # LCD controller drivers - only needed by display component
"esp_local_ctrl", # Local control over HTTPS/BLE - ESPHome has native API
"espcoredump", # Core dump support - ESPHome has its own debug component
"fatfs", # FAT filesystem - ESPHome doesn't use filesystem storage
"mqtt", # ESP-IDF MQTT library - ESPHome has its own MQTT implementation
"openthread", # Thread protocol - only needed by openthread component
"perfmon", # Xtensa performance monitor - ESPHome has its own debug component
"protocomm", # Protocol communication for provisioning - unused by ESPHome
"spiffs", # SPIFFS filesystem - ESPHome doesn't use filesystem storage (IDF only)
"ulp", # ULP coprocessor - not currently used by any ESPHome component
"unity", # Unit testing framework - ESPHome doesn't use IDF's testing
"wear_levelling", # Flash wear levelling for fatfs - unused since fatfs unused
"wifi_provisioning", # WiFi provisioning - ESPHome uses its own improv implementation
)
# Additional IDF managed components to exclude for Arduino framework builds
# These are pulled in by the Arduino framework's idf_component.yml but not used by ESPHome
# Note: Component names include the namespace prefix (e.g., "espressif__cbor") because
# that's how managed components are registered in the IDF build system
# List includes direct dependencies from arduino-esp32/idf_component.yml
# plus transitive dependencies from RainMaker/Insights (except espressif/mdns which we need)
ARDUINO_EXCLUDED_IDF_COMPONENTS = (
"chmorgan__esp-libhelix-mp3", # MP3 decoder - not used
"espressif__cbor", # CBOR library - only used by RainMaker/Insights
"espressif__esp-dsp", # DSP library - not used
"espressif__esp-modbus", # Modbus - ESPHome has its own
"espressif__esp-sr", # Speech recognition - not used
"espressif__esp-zboss-lib", # Zigbee ZBOSS library - not used
"espressif__esp-zigbee-lib", # Zigbee library - not used
"espressif__esp_diag_data_store", # Diagnostics - not used
"espressif__esp_diagnostics", # Diagnostics - not used
"espressif__esp_hosted", # ESP hosted - only for ESP32-P4
"espressif__esp_insights", # ESP Insights - not used
"espressif__esp_modem", # Modem library - not used
"espressif__esp_rainmaker", # RainMaker - not used
"espressif__esp_rcp_update", # RCP update - RainMaker transitive dep
"espressif__esp_schedule", # Schedule - RainMaker transitive dep
"espressif__esp_secure_cert_mgr", # Secure cert - RainMaker transitive dep
"espressif__esp_wifi_remote", # WiFi remote - only for ESP32-P4
"espressif__json_generator", # JSON generator - RainMaker transitive dep
"espressif__json_parser", # JSON parser - RainMaker transitive dep
"espressif__lan867x", # Ethernet PHY - ESPHome uses ESP-IDF ethernet directly
"espressif__libsodium", # Crypto - ESPHome uses its own noise-c library
"espressif__network_provisioning", # Network provisioning - not used
"espressif__qrcode", # QR code - not used
"espressif__rmaker_common", # RainMaker common - not used
"joltwallet__littlefs", # LittleFS - ESPHome doesn't use filesystem
)
# Mapping of Arduino libraries to IDF managed components they require
# When an Arduino library is enabled via cg.add_library(), these components
# are automatically un-stubbed from ARDUINO_EXCLUDED_IDF_COMPONENTS.
#
# Note: Some libraries (Matter, LittleFS, ESP_SR, WiFiProv, ArduinoOTA) already have
# conditional maybe_add_component() calls in arduino-esp32/CMakeLists.txt that handle
# their managed component dependencies. Our mapping is primarily needed for libraries
# that don't have such conditionals (Ethernet, PPP, Zigbee, RainMaker, Insights, etc.)
# and to ensure the stubs are removed from our idf_component.yml overrides.
ARDUINO_LIBRARY_IDF_COMPONENTS: dict[str, tuple[str, ...]] = {
"BLE": ("esp_driver_gptimer",),
"BluetoothSerial": ("esp_driver_gptimer",),
"ESP_HostedOTA": ("espressif__esp_hosted", "espressif__esp_wifi_remote"),
"ESP_SR": ("espressif__esp-sr",),
"Ethernet": ("espressif__lan867x",),
"FFat": ("fatfs",),
"Insights": (
"espressif__cbor",
"espressif__esp_insights",
"espressif__esp_diagnostics",
"espressif__esp_diag_data_store",
"espressif__rmaker_common", # Transitive dep from esp_insights
),
"LittleFS": ("joltwallet__littlefs",),
"Matter": ("espressif__esp_matter",),
"PPP": ("espressif__esp_modem",),
"RainMaker": (
# Direct deps from idf_component.yml
"espressif__cbor",
"espressif__esp_rainmaker",
"espressif__esp_insights",
"espressif__esp_diagnostics",
"espressif__esp_diag_data_store",
"espressif__rmaker_common",
"espressif__qrcode",
# Transitive deps from esp_rainmaker
"espressif__esp_rcp_update",
"espressif__esp_schedule",
"espressif__esp_secure_cert_mgr",
"espressif__json_generator",
"espressif__json_parser",
"espressif__network_provisioning",
),
"SD": ("fatfs",),
"SD_MMC": ("fatfs",),
"SPIFFS": ("spiffs",),
"WiFiProv": ("espressif__network_provisioning", "espressif__qrcode"),
"Zigbee": ("espressif__esp-zigbee-lib", "espressif__esp-zboss-lib"),
}
# Arduino library to Arduino library dependencies
# When enabling one library, also enable its dependencies
# Kconfig "select" statements don't work with CONFIG_ARDUINO_SELECTIVE_COMPILATION
ARDUINO_LIBRARY_DEPENDENCIES: dict[str, tuple[str, ...]] = {
"Ethernet": ("Network",),
"WiFi": ("Network",),
}
def _idf_component_stub_name(component: str) -> str:
"""Get stub directory name from IDF component name.
Component names are typically namespace__name (e.g., espressif__cbor).
Returns just the name part (e.g., cbor). If no namespace is present,
returns the original component name.
"""
_prefix, sep, suffix = component.partition("__")
return suffix if sep else component
def _idf_component_dep_name(component: str) -> str:
"""Convert IDF component name to dependency format.
Converts espressif__cbor to espressif/cbor.
"""
return component.replace("__", "/")
# Arduino libraries to disable by default when using Arduino framework
# ESPHome uses ESP-IDF APIs directly; we only need the Arduino core
# (HardwareSerial, Print, Stream, GPIO functions which are always compiled)
# Components use cg.add_library() which auto-enables any they need
# This list must match ARDUINO_ALL_LIBRARIES from arduino-esp32/CMakeLists.txt
ARDUINO_DISABLED_LIBRARIES: frozenset[str] = frozenset(
{
"ArduinoOTA",
"AsyncUDP",
"BLE",
"BluetoothSerial",
"DNSServer",
"EEPROM",
"ESP_HostedOTA",
"ESP_I2S",
"ESP_NOW",
"ESP_SR",
"ESPmDNS",
"Ethernet",
"FFat",
"FS",
"Hash",
"HTTPClient",
"HTTPUpdate",
"Insights",
"LittleFS",
"Matter",
"NetBIOS",
"Network",
"NetworkClientSecure",
"OpenThread",
"PPP",
"Preferences",
"RainMaker",
"SD",
"SD_MMC",
"SimpleBLE",
"SPI",
"SPIFFS",
"Ticker",
"Update",
"USB",
"WebServer",
"WiFi",
"WiFiProv",
"Wire",
"Zigbee",
}
)
# ESP32 (original) chip revision options
# Setting minimum revision to 3.0 or higher:
# - Reduces flash size by excluding workaround code for older chip bugs
@@ -405,15 +202,6 @@ def set_core_data(config):
)
CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] = {}
CORE.data[KEY_ESP32][KEY_COMPONENTS] = {}
# Initialize with default exclusions - components can call include_builtin_idf_component()
# to re-enable any they need
excluded = set(DEFAULT_EXCLUDED_IDF_COMPONENTS)
# Add Arduino-specific managed component exclusions when using Arduino framework
if conf[CONF_TYPE] == FRAMEWORK_ARDUINO:
excluded.update(ARDUINO_EXCLUDED_IDF_COMPONENTS)
CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS] = excluded
# Initialize Arduino library tracking - cg.add_library() auto-enables libraries
CORE.data[KEY_ESP32][KEY_ARDUINO_LIBRARIES] = set()
CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse(
config[CONF_FRAMEWORK][CONF_VERSION]
)
@@ -539,48 +327,6 @@ def add_idf_component(
}
def exclude_builtin_idf_component(name: str) -> None:
"""Exclude an ESP-IDF component from the build.
This reduces compile time by skipping components that are not needed.
The component will be passed to ESP-IDF's EXCLUDE_COMPONENTS cmake variable.
Note: Components that are dependencies of other required components
cannot be excluded - ESP-IDF will still build them.
"""
CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS].add(name)
def include_builtin_idf_component(name: str) -> None:
"""Remove an ESP-IDF component from the exclusion list.
Call this from components that need an ESP-IDF component that is
excluded by default in DEFAULT_EXCLUDED_IDF_COMPONENTS. This ensures the
component will be built when needed.
"""
CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS].discard(name)
def _enable_arduino_library(name: str) -> None:
"""Enable an Arduino library that is disabled by default.
This is called automatically by CORE.add_library() when a component adds
an Arduino library via cg.add_library(). Components should not call this
directly - just use cg.add_library("LibName", None).
Args:
name: The library name (e.g., "Wire", "SPI", "WiFi")
"""
enabled_libs: set[str] = CORE.data[KEY_ESP32][KEY_ARDUINO_LIBRARIES]
enabled_libs.add(name)
# Also enable any required Arduino library dependencies
for dep_lib in ARDUINO_LIBRARY_DEPENDENCIES.get(name, ()):
enabled_libs.add(dep_lib)
# Also enable any required IDF components
for idf_component in ARDUINO_LIBRARY_IDF_COMPONENTS.get(name, ()):
include_builtin_idf_component(idf_component)
def add_extra_script(stage: str, filename: str, path: Path):
"""Add an extra script to the project."""
key = f"{stage}:{filename}"
@@ -924,27 +670,11 @@ CONF_FREERTOS_IN_IRAM = "freertos_in_iram"
CONF_RINGBUF_IN_IRAM = "ringbuf_in_iram"
CONF_HEAP_IN_IRAM = "heap_in_iram"
CONF_LOOP_TASK_STACK_SIZE = "loop_task_stack_size"
CONF_USE_FULL_CERTIFICATE_BUNDLE = "use_full_certificate_bundle"
CONF_DISABLE_DEBUG_STUBS = "disable_debug_stubs"
CONF_DISABLE_OCD_AWARE = "disable_ocd_aware"
CONF_DISABLE_USB_SERIAL_JTAG_SECONDARY = "disable_usb_serial_jtag_secondary"
CONF_DISABLE_DEV_NULL_VFS = "disable_dev_null_vfs"
CONF_DISABLE_MBEDTLS_PEER_CERT = "disable_mbedtls_peer_cert"
CONF_DISABLE_MBEDTLS_PKCS7 = "disable_mbedtls_pkcs7"
CONF_DISABLE_REGI2C_IN_IRAM = "disable_regi2c_in_iram"
CONF_DISABLE_FATFS = "disable_fatfs"
# VFS requirement tracking
# Components that need VFS features can call require_vfs_*() functions
# Components that need VFS features can call require_vfs_select() or require_vfs_dir()
KEY_VFS_SELECT_REQUIRED = "vfs_select_required"
KEY_VFS_DIR_REQUIRED = "vfs_dir_required"
KEY_VFS_TERMIOS_REQUIRED = "vfs_termios_required"
# Feature requirement tracking - components can call require_* functions to re-enable
# These are stored in CORE.data[KEY_ESP32] dict
KEY_USB_SERIAL_JTAG_SECONDARY_REQUIRED = "usb_serial_jtag_secondary_required"
KEY_MBEDTLS_PEER_CERT_REQUIRED = "mbedtls_peer_cert_required"
KEY_MBEDTLS_PKCS7_REQUIRED = "mbedtls_pkcs7_required"
KEY_FATFS_REQUIRED = "fatfs_required"
def require_vfs_select() -> None:
@@ -965,64 +695,6 @@ def require_vfs_dir() -> None:
CORE.data[KEY_VFS_DIR_REQUIRED] = True
def require_vfs_termios() -> None:
"""Mark that VFS termios support is required by a component.
Call this from components that use terminal I/O functions (usb_serial_jtag_vfs_*, etc.).
This prevents CONFIG_VFS_SUPPORT_TERMIOS from being disabled.
"""
CORE.data[KEY_VFS_TERMIOS_REQUIRED] = True
def require_full_certificate_bundle() -> None:
"""Request the full certificate bundle instead of the common-CAs-only bundle.
By default, ESPHome uses CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_CMN which
includes only CAs with >1% market share (~51 KB smaller than full bundle).
This covers ~99% of websites including Let's Encrypt, DigiCert, Google, Amazon.
Call this from components that need to connect to services using uncommon CAs.
"""
CORE.data[KEY_ESP32][KEY_FULL_CERT_BUNDLE] = True
def require_usb_serial_jtag_secondary() -> None:
"""Mark that USB Serial/JTAG secondary console is required by a component.
Call this from components (e.g., logger) that need USB Serial/JTAG console output.
This prevents CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG from being disabled.
"""
CORE.data[KEY_ESP32][KEY_USB_SERIAL_JTAG_SECONDARY_REQUIRED] = True
def require_mbedtls_peer_cert() -> None:
"""Mark that mbedTLS peer certificate retention is required by a component.
Call this from components that need access to the peer certificate after
the TLS handshake is complete. This prevents CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE
from being disabled.
"""
CORE.data[KEY_ESP32][KEY_MBEDTLS_PEER_CERT_REQUIRED] = True
def require_mbedtls_pkcs7() -> None:
"""Mark that mbedTLS PKCS#7 support is required by a component.
Call this from components that need PKCS#7 certificate validation.
This prevents CONFIG_MBEDTLS_PKCS7_C from being disabled.
"""
CORE.data[KEY_ESP32][KEY_MBEDTLS_PKCS7_REQUIRED] = True
def require_fatfs() -> None:
"""Mark that FATFS support is required by a component.
Call this from components that use FATFS (e.g., SD card, storage components).
This prevents FATFS from being disabled when disable_fatfs is set.
"""
CORE.data[KEY_ESP32][KEY_FATFS_REQUIRED] = True
def _parse_idf_component(value: str) -> ConfigType:
"""Parse IDF component shorthand syntax like 'owner/component^version'"""
# Match operator followed by version-like string (digit or *)
@@ -1104,22 +776,6 @@ FRAMEWORK_SCHEMA = cv.Schema(
min=8192, max=32768
),
cv.Optional(CONF_ENABLE_OTA_ROLLBACK, default=True): cv.boolean,
cv.Optional(
CONF_USE_FULL_CERTIFICATE_BUNDLE, default=False
): cv.boolean,
cv.Optional(
CONF_INCLUDE_BUILTIN_IDF_COMPONENTS, default=[]
): cv.ensure_list(cv.string_strict),
cv.Optional(CONF_DISABLE_DEBUG_STUBS, default=True): cv.boolean,
cv.Optional(CONF_DISABLE_OCD_AWARE, default=True): cv.boolean,
cv.Optional(
CONF_DISABLE_USB_SERIAL_JTAG_SECONDARY, default=True
): cv.boolean,
cv.Optional(CONF_DISABLE_DEV_NULL_VFS, default=True): cv.boolean,
cv.Optional(CONF_DISABLE_MBEDTLS_PEER_CERT, default=True): cv.boolean,
cv.Optional(CONF_DISABLE_MBEDTLS_PKCS7, default=True): cv.boolean,
cv.Optional(CONF_DISABLE_REGI2C_IN_IRAM, default=True): cv.boolean,
cv.Optional(CONF_DISABLE_FATFS, default=True): cv.boolean,
}
),
cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list(
@@ -1309,40 +965,6 @@ def _configure_lwip_max_sockets(conf: dict) -> None:
add_idf_sdkconfig_option("CONFIG_LWIP_MAX_SOCKETS", max_sockets)
@coroutine_with_priority(CoroPriority.FINAL)
async def _write_exclude_components() -> None:
"""Write EXCLUDE_COMPONENTS cmake arg after all components have registered exclusions."""
if KEY_ESP32 not in CORE.data:
return
excluded = CORE.data[KEY_ESP32].get(KEY_EXCLUDE_COMPONENTS)
if excluded:
exclude_list = ";".join(sorted(excluded))
cg.add_platformio_option(
"board_build.cmake_extra_args", f"-DEXCLUDE_COMPONENTS={exclude_list}"
)
@coroutine_with_priority(CoroPriority.FINAL)
async def _write_arduino_libraries_sdkconfig() -> None:
"""Write Arduino selective compilation sdkconfig after all components have added libraries.
This must run at FINAL priority so that all components have had a chance to call
cg.add_library() which auto-enables Arduino libraries via _enable_arduino_library().
"""
if KEY_ESP32 not in CORE.data:
return
# Enable Arduino selective compilation to disable unused Arduino libraries
# ESPHome uses ESP-IDF APIs directly; we only need the Arduino core
# (HardwareSerial, Print, Stream, GPIO functions which are always compiled)
# cg.add_library() auto-enables needed libraries; users can also add
# libraries via esphome: libraries: config which calls cg.add_library()
add_idf_sdkconfig_option("CONFIG_ARDUINO_SELECTIVE_COMPILATION", True)
enabled_libs = CORE.data[KEY_ESP32].get(KEY_ARDUINO_LIBRARIES, set())
for lib in ARDUINO_DISABLED_LIBRARIES:
# Enable if explicitly requested, disable otherwise
add_idf_sdkconfig_option(f"CONFIG_ARDUINO_SELECTIVE_{lib}", lib in enabled_libs)
@coroutine_with_priority(CoroPriority.FINAL)
async def _add_yaml_idf_components(components: list[ConfigType]):
"""Add IDF components from YAML config with final priority to override code-added components."""
@@ -1426,19 +1048,6 @@ async def to_code(config):
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF")
if use_platformio:
cg.add_platformio_option("framework", "espidf")
# Wrap std::__throw_* functions to abort immediately, eliminating ~3KB of
# exception class overhead. See throw_stubs.cpp for implementation.
# ESP-IDF already compiles with -fno-exceptions, so this code was dead anyway.
for mangled in [
"_ZSt20__throw_length_errorPKc",
"_ZSt19__throw_logic_errorPKc",
"_ZSt20__throw_out_of_rangePKc",
"_ZSt24__throw_out_of_range_fmtPKcz",
"_ZSt17__throw_bad_allocv",
"_ZSt25__throw_bad_function_callv",
]:
cg.add_build_flag(f"-Wl,--wrap={mangled}")
else:
cg.add_build_flag("-DUSE_ARDUINO")
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO")
@@ -1471,18 +1080,6 @@ async def to_code(config):
cg.add_build_flag("-Wno-nonnull-compare")
# Use CMN (common CAs) bundle by default to save ~51KB flash
# CMN covers CAs with >1% market share (~99% of websites)
# Components needing uncommon CAs can call require_full_certificate_bundle()
use_full_bundle = conf[CONF_ADVANCED].get(
CONF_USE_FULL_CERTIFICATE_BUNDLE, False
) or CORE.data[KEY_ESP32].get(KEY_FULL_CERT_BUNDLE, False)
add_idf_sdkconfig_option(
"CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL", use_full_bundle
)
if not use_full_bundle:
add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_CMN", True)
add_idf_sdkconfig_option(f"CONFIG_IDF_TARGET_{variant}", True)
add_idf_sdkconfig_option(
f"CONFIG_ESPTOOLPY_FLASHSIZE_{config[CONF_FLASH_SIZE]}", True
@@ -1546,10 +1143,6 @@ async def to_code(config):
# Disable dynamic log level control to save memory
add_idf_sdkconfig_option("CONFIG_LOG_DYNAMIC_LEVEL_CONTROL", False)
# Disable per-tag log level filtering since dynamic level control is disabled above
# This saves ~250 bytes of RAM (tag cache) and associated code
add_idf_sdkconfig_option("CONFIG_LOG_TAG_LEVEL_IMPL_NONE", True)
# Reduce PHY TX power in the event of a brownout
add_idf_sdkconfig_option("CONFIG_ESP_PHY_REDUCE_TX_POWER", True)
@@ -1560,11 +1153,6 @@ async def to_code(config):
# Apply LWIP optimization settings
advanced = conf[CONF_ADVANCED]
# Re-include any IDF components the user explicitly requested
for component_name in advanced.get(CONF_INCLUDE_BUILTIN_IDF_COMPONENTS, []):
include_builtin_idf_component(component_name)
# DHCP server: only disable if explicitly set to false
# WiFi component handles its own optimization when AP mode is not used
# When using Arduino with Ethernet, DHCP server functions must be available
@@ -1603,18 +1191,11 @@ async def to_code(config):
add_idf_sdkconfig_option("CONFIG_LIBC_LOCKS_PLACE_IN_IRAM", False)
# Disable VFS support for termios (terminal I/O functions)
# USB Serial JTAG VFS functions require termios support.
# Components that need it (e.g., logger when USB_SERIAL_JTAG is supported but not selected
# as the logger output) call require_vfs_termios().
# ESPHome doesn't use termios functions on ESP32 (only used in host UART driver).
# Saves approximately 1.8KB of flash when disabled (default).
if CORE.data.get(KEY_VFS_TERMIOS_REQUIRED, False):
# Component requires VFS termios - force enable regardless of user setting
add_idf_sdkconfig_option("CONFIG_VFS_SUPPORT_TERMIOS", True)
else:
# No component needs it - allow user to control (default: disabled)
add_idf_sdkconfig_option(
"CONFIG_VFS_SUPPORT_TERMIOS", not advanced[CONF_DISABLE_VFS_SUPPORT_TERMIOS]
)
add_idf_sdkconfig_option(
"CONFIG_VFS_SUPPORT_TERMIOS", not advanced[CONF_DISABLE_VFS_SUPPORT_TERMIOS]
)
# Disable VFS support for select() with file descriptors
# ESPHome only uses select() with sockets via lwip_select(), which still works.
@@ -1693,61 +1274,6 @@ async def to_code(config):
add_idf_sdkconfig_option(f"CONFIG_LOG_DEFAULT_LEVEL_{conf[CONF_LOG_LEVEL]}", True)
# Disable OpenOCD debug stubs to save code size
# These are used for on-chip debugging with OpenOCD/JTAG, rarely needed for ESPHome
if advanced[CONF_DISABLE_DEBUG_STUBS]:
add_idf_sdkconfig_option("CONFIG_ESP_DEBUG_STUBS_ENABLE", False)
# Disable OCD-aware exception handlers
# When enabled, the panic handler detects JTAG debugger and halts instead of resetting
# Most ESPHome users don't use JTAG debugging
if advanced[CONF_DISABLE_OCD_AWARE]:
add_idf_sdkconfig_option("CONFIG_ESP_DEBUG_OCDAWARE", False)
# Disable USB Serial/JTAG secondary console
# Components like logger can call require_usb_serial_jtag_secondary() to re-enable
if CORE.data[KEY_ESP32].get(KEY_USB_SERIAL_JTAG_SECONDARY_REQUIRED, False):
add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG", True)
elif advanced[CONF_DISABLE_USB_SERIAL_JTAG_SECONDARY]:
add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_SECONDARY_NONE", True)
# Disable /dev/null VFS initialization
# ESPHome doesn't typically need /dev/null
if advanced[CONF_DISABLE_DEV_NULL_VFS]:
add_idf_sdkconfig_option("CONFIG_VFS_INITIALIZE_DEV_NULL", False)
# Disable keeping peer certificate after TLS handshake
# Saves ~4KB heap per connection, but prevents certificate inspection after handshake
# Components that need it can call require_mbedtls_peer_cert()
if CORE.data[KEY_ESP32].get(KEY_MBEDTLS_PEER_CERT_REQUIRED, False):
add_idf_sdkconfig_option("CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE", True)
elif advanced[CONF_DISABLE_MBEDTLS_PEER_CERT]:
add_idf_sdkconfig_option("CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE", False)
# Disable PKCS#7 support in mbedTLS
# Only needed for specific certificate validation scenarios
# Components that need it can call require_mbedtls_pkcs7()
if CORE.data[KEY_ESP32].get(KEY_MBEDTLS_PKCS7_REQUIRED, False):
# Component called require_mbedtls_pkcs7() - enable regardless of user setting
add_idf_sdkconfig_option("CONFIG_MBEDTLS_PKCS7_C", True)
elif advanced[CONF_DISABLE_MBEDTLS_PKCS7]:
add_idf_sdkconfig_option("CONFIG_MBEDTLS_PKCS7_C", False)
# Disable regi2c control functions in IRAM
# Only needed if using analog peripherals (ADC, DAC, etc.) from ISRs while cache is disabled
if advanced[CONF_DISABLE_REGI2C_IN_IRAM]:
add_idf_sdkconfig_option("CONFIG_ESP_REGI2C_CTRL_FUNC_IN_IRAM", False)
# Disable FATFS support
# Components that need FATFS (SD card, etc.) can call require_fatfs()
if CORE.data[KEY_ESP32].get(KEY_FATFS_REQUIRED, False):
# Component called require_fatfs() - enable regardless of user setting
add_idf_sdkconfig_option("CONFIG_FATFS_LFN_NONE", False)
add_idf_sdkconfig_option("CONFIG_FATFS_VOLUME_COUNT", 2)
elif advanced[CONF_DISABLE_FATFS]:
add_idf_sdkconfig_option("CONFIG_FATFS_LFN_NONE", True)
add_idf_sdkconfig_option("CONFIG_FATFS_VOLUME_COUNT", 0)
for name, value in conf[CONF_SDKCONFIG_OPTIONS].items():
add_idf_sdkconfig_option(name, RawSdkconfigValue(value))
@@ -1756,16 +1282,6 @@ async def to_code(config):
if conf[CONF_COMPONENTS]:
CORE.add_job(_add_yaml_idf_components, conf[CONF_COMPONENTS])
# Write EXCLUDE_COMPONENTS at FINAL priority after all components have had
# a chance to call include_builtin_idf_component() to re-enable components they need.
# Default exclusions are added in set_core_data() during config validation.
CORE.add_job(_write_exclude_components)
# Write Arduino selective compilation sdkconfig at FINAL priority after all
# components have had a chance to call cg.add_library() to enable libraries they need.
if conf[CONF_TYPE] == FRAMEWORK_ARDUINO:
CORE.add_job(_write_arduino_libraries_sdkconfig)
APP_PARTITION_SIZES = {
"2MB": 0x0C0000, # 768 KB
@@ -1846,49 +1362,11 @@ def _write_sdkconfig():
def _write_idf_component_yml():
yml_path = CORE.relative_build_path("src/idf_component.yml")
dependencies: dict[str, dict] = {}
# For Arduino builds, override unused managed components from the Arduino framework
# by pointing them to empty stub directories using override_path
# This prevents the IDF component manager from downloading the real components
if CORE.using_arduino:
# Determine which IDF components are needed by enabled Arduino libraries
enabled_libs = CORE.data[KEY_ESP32].get(KEY_ARDUINO_LIBRARIES, set())
required_idf_components = {
comp
for lib in enabled_libs
for comp in ARDUINO_LIBRARY_IDF_COMPONENTS.get(lib, ())
}
# Only stub components that are not required by any enabled Arduino library
components_to_stub = (
set(ARDUINO_EXCLUDED_IDF_COMPONENTS) - required_idf_components
)
stubs_dir = CORE.relative_build_path("component_stubs")
stubs_dir.mkdir(exist_ok=True)
for component_name in components_to_stub:
# Create stub directory with minimal CMakeLists.txt
stub_path = stubs_dir / _idf_component_stub_name(component_name)
stub_path.mkdir(exist_ok=True)
stub_cmake = stub_path / "CMakeLists.txt"
if not stub_cmake.exists():
stub_cmake.write_text("idf_component_register()\n")
dependencies[_idf_component_dep_name(component_name)] = {
"version": "*",
"override_path": str(stub_path),
}
# Remove stubs for components that are now required by enabled libraries
for component_name in required_idf_components:
stub_path = stubs_dir / _idf_component_stub_name(component_name)
if stub_path.exists():
shutil.rmtree(stub_path)
if CORE.data[KEY_ESP32][KEY_COMPONENTS]:
components: dict = CORE.data[KEY_ESP32][KEY_COMPONENTS]
dependencies = {}
for name, component in components.items():
dependency: dict[str, str] = {}
dependency = {}
if component[KEY_REF]:
dependency["version"] = component[KEY_REF]
if component[KEY_REPO]:
@@ -1896,8 +1374,9 @@ def _write_idf_component_yml():
if component[KEY_PATH]:
dependency["path"] = component[KEY_PATH]
dependencies[name] = dependency
contents = yaml_util.dump({"dependencies": dependencies}) if dependencies else ""
contents = yaml_util.dump({"dependencies": dependencies})
else:
contents = ""
if write_file_if_changed(yml_path, contents):
dependencies_lock = CORE.relative_build_path("dependencies.lock")
if dependencies_lock.is_file():

View File

@@ -175,32 +175,6 @@ ESP32_BOARD_PINS = {
"LED": 13,
"LED_BUILTIN": 13,
},
"adafruit_feather_esp32s3_reversetft": {
"BUTTON": 0,
"A0": 18,
"A1": 17,
"A2": 16,
"A3": 15,
"A4": 14,
"A5": 8,
"SCK": 36,
"MOSI": 35,
"MISO": 37,
"RX": 38,
"TX": 39,
"SCL": 4,
"SDA": 3,
"NEOPIXEL": 33,
"PIN_NEOPIXEL": 33,
"NEOPIXEL_POWER": 21,
"TFT_I2C_POWER": 7,
"TFT_CS": 42,
"TFT_DC": 40,
"TFT_RESET": 41,
"TFT_BACKLIGHT": 45,
"LED": 13,
"LED_BUILTIN": 13,
},
"adafruit_feather_esp32s3_tft": {
"BUTTON": 0,
"A0": 18,

View File

@@ -6,15 +6,12 @@ KEY_FLASH_SIZE = "flash_size"
KEY_VARIANT = "variant"
KEY_SDKCONFIG_OPTIONS = "sdkconfig_options"
KEY_COMPONENTS = "components"
KEY_EXCLUDE_COMPONENTS = "exclude_components"
KEY_ARDUINO_LIBRARIES = "arduino_libraries"
KEY_REPO = "repo"
KEY_REF = "ref"
KEY_REFRESH = "refresh"
KEY_PATH = "path"
KEY_SUBMODULES = "submodules"
KEY_EXTRA_BUILD_FILES = "extra_build_files"
KEY_FULL_CERT_BUNDLE = "full_cert_bundle"
VARIANT_ESP32 = "ESP32"
VARIANT_ESP32C2 = "ESP32C2"

View File

@@ -85,6 +85,7 @@ void ESP32InternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpi
break;
}
gpio_set_intr_type(this->get_pin_num(), idf_type);
gpio_intr_enable(this->get_pin_num());
if (!isr_service_installed) {
auto res = gpio_install_isr_service(ESP_INTR_FLAG_LEVEL3);
if (res != ESP_OK) {
@@ -94,7 +95,6 @@ void ESP32InternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpi
isr_service_installed = true;
}
gpio_isr_handler_add(this->get_pin_num(), func, arg);
gpio_intr_enable(this->get_pin_num());
}
size_t ESP32InternalGPIOPin::dump_summary(char *buffer, size_t len) const {

View File

@@ -19,7 +19,16 @@ static constexpr size_t KEY_BUFFER_SIZE = 12;
struct NVSData {
uint32_t key;
SmallInlineBuffer<8> data; // Most prefs fit in 8 bytes (covers fan, cover, select, etc.)
std::unique_ptr<uint8_t[]> data;
size_t len;
void set_data(const uint8_t *src, size_t size) {
if (!this->data || this->len != size) {
this->data = std::make_unique<uint8_t[]>(size);
this->len = size;
}
memcpy(this->data.get(), src, size);
}
};
static std::vector<NVSData> s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
@@ -32,14 +41,14 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend {
// try find in pending saves and update that
for (auto &obj : s_pending_save) {
if (obj.key == this->key) {
obj.data.set(data, len);
obj.set_data(data, len);
return true;
}
}
NVSData save{};
save.key = this->key;
save.data.set(data, len);
s_pending_save.push_back(std::move(save));
save.set_data(data, len);
s_pending_save.emplace_back(std::move(save));
ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len);
return true;
}
@@ -47,11 +56,11 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend {
// try find in pending saves and load from that
for (auto &obj : s_pending_save) {
if (obj.key == this->key) {
if (obj.data.size() != len) {
if (obj.len != len) {
// size mismatch
return false;
}
memcpy(data, obj.data.data(), len);
memcpy(data, obj.data.get(), len);
return true;
}
}
@@ -127,10 +136,10 @@ class ESP32Preferences : public ESPPreferences {
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str);
if (this->is_changed_(this->nvs_handle, save, key_str)) {
esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.data(), save.data.size());
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size());
esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.get(), save.len);
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.len);
if (err != 0) {
ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.data.size(), esp_err_to_name(err));
ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.len, esp_err_to_name(err));
failed++;
last_err = err;
last_key = save.key;
@@ -138,7 +147,7 @@ class ESP32Preferences : public ESPPreferences {
}
written++;
} else {
ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.data.size());
ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.len);
cached++;
}
s_pending_save.erase(s_pending_save.begin() + i);
@@ -169,7 +178,7 @@ class ESP32Preferences : public ESPPreferences {
return true;
}
// Check size first before allocating memory
if (actual_len != to_save.data.size()) {
if (actual_len != to_save.len) {
return true;
}
// Most preferences are small, use stack buffer with heap fallback for large ones
@@ -179,7 +188,7 @@ class ESP32Preferences : public ESPPreferences {
ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err));
return true;
}
return memcmp(to_save.data.data(), stored_data.get(), to_save.data.size()) != 0;
return memcmp(to_save.data.get(), stored_data.get(), to_save.len) != 0;
}
bool reset() override {

View File

@@ -1,57 +0,0 @@
/*
* Linker wrap stubs for std::__throw_* functions.
*
* ESP-IDF compiles with -fno-exceptions, so C++ exceptions always abort.
* However, ESP-IDF only wraps low-level functions (__cxa_throw, etc.),
* not the std::__throw_* functions that construct exception objects first.
* This pulls in ~3KB of dead exception class code that can never run.
*
* ESP8266 Arduino already solved this: their toolchain rebuilds libstdc++
* with throw functions that just call abort(). We achieve the same result
* using linker --wrap without requiring toolchain changes.
*
* These stubs abort immediately with a descriptive message, allowing
* the linker to dead-code eliminate the exception class infrastructure.
*
* Wrapped functions and their callers:
* - std::__throw_length_error: std::string::reserve, std::vector::reserve
* - std::__throw_logic_error: std::promise, std::packaged_task
* - std::__throw_out_of_range: std::string::at, std::vector::at
* - std::__throw_out_of_range_fmt: std::bitset::to_ulong
* - std::__throw_bad_alloc: operator new
* - std::__throw_bad_function_call: std::function::operator()
*/
#ifdef USE_ESP_IDF
#include "esp_system.h"
namespace esphome::esp32 {}
// Linker wraps for std::__throw_* - must be extern "C" at global scope.
// Names must be __wrap_ + mangled name for the linker's --wrap option.
// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
extern "C" {
// std::__throw_length_error(char const*) - called when container size exceeds max_size()
void __wrap__ZSt20__throw_length_errorPKc(const char *) { esp_system_abort("std::length_error"); }
// std::__throw_logic_error(char const*) - called for logic errors (e.g., promise already satisfied)
void __wrap__ZSt19__throw_logic_errorPKc(const char *) { esp_system_abort("std::logic_error"); }
// std::__throw_out_of_range(char const*) - called by at() when index is out of bounds
void __wrap__ZSt20__throw_out_of_rangePKc(const char *) { esp_system_abort("std::out_of_range"); }
// std::__throw_out_of_range_fmt(char const*, ...) - called by bitset::to_ulong when value doesn't fit
void __wrap__ZSt24__throw_out_of_range_fmtPKcz(const char *, ...) { esp_system_abort("std::out_of_range"); }
// std::__throw_bad_alloc() - called when operator new fails
void __wrap__ZSt17__throw_bad_allocv() { esp_system_abort("std::bad_alloc"); }
// std::__throw_bad_function_call() - called when invoking empty std::function
void __wrap__ZSt25__throw_bad_function_callv() { esp_system_abort("std::bad_function_call"); }
} // extern "C"
// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
#endif // USE_ESP_IDF

View File

@@ -98,10 +98,6 @@ void ESP32BLE::advertising_set_service_data(const std::vector<uint8_t> &data) {
}
void ESP32BLE::advertising_set_manufacturer_data(const std::vector<uint8_t> &data) {
this->advertising_set_manufacturer_data(std::span<const uint8_t>(data));
}
void ESP32BLE::advertising_set_manufacturer_data(std::span<const uint8_t> data) {
this->advertising_init_();
this->advertising_->set_manufacturer_data(data);
this->advertising_start();

View File

@@ -118,7 +118,6 @@ class ESP32BLE : public Component {
void advertising_start();
void advertising_set_service_data(const std::vector<uint8_t> &data);
void advertising_set_manufacturer_data(const std::vector<uint8_t> &data);
void advertising_set_manufacturer_data(std::span<const uint8_t> data);
void advertising_set_appearance(uint16_t appearance) { this->appearance_ = appearance; }
void advertising_set_service_data_and_name(std::span<const uint8_t> data, bool include_name);
void advertising_add_service_uuid(ESPBTUUID uuid);

View File

@@ -59,10 +59,6 @@ void BLEAdvertising::set_service_data(const std::vector<uint8_t> &data) {
}
void BLEAdvertising::set_manufacturer_data(const std::vector<uint8_t> &data) {
this->set_manufacturer_data(std::span<const uint8_t>(data));
}
void BLEAdvertising::set_manufacturer_data(std::span<const uint8_t> data) {
delete[] this->advertising_data_.p_manufacturer_data;
this->advertising_data_.p_manufacturer_data = nullptr;
this->advertising_data_.manufacturer_len = data.size();

View File

@@ -37,7 +37,6 @@ class BLEAdvertising {
void set_scan_response(bool scan_response) { this->scan_response_ = scan_response; }
void set_min_preferred_interval(uint16_t interval) { this->advertising_data_.min_interval = interval; }
void set_manufacturer_data(const std::vector<uint8_t> &data);
void set_manufacturer_data(std::span<const uint8_t> data);
void set_appearance(uint16_t appearance) { this->advertising_data_.appearance = appearance; }
void set_service_data(const std::vector<uint8_t> &data);
void set_service_data(std::span<const uint8_t> data);

View File

@@ -48,7 +48,7 @@ class ESPBTUUID {
// Remove before 2026.8.0
ESPDEPRECATED("Use to_str() instead. Removed in 2026.8.0", "2026.2.0")
std::string to_string() const; // NOLINT
std::string to_string() const;
const char *to_str(std::span<char, UUID_STR_LEN> output) const;
protected:

View File

@@ -1,6 +1,5 @@
#include "esp32_ble_beacon.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#ifdef USE_ESP32

View File

@@ -15,10 +15,7 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_characteristic_on_w
Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory)
new Trigger<std::vector<uint8_t>, uint16_t>();
characteristic->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) {
// Convert span to vector for trigger - copy is necessary because:
// 1. Trigger stores the data for use in automation actions that execute later
// 2. The span is only valid during this callback (points to temporary BLE stack data)
// 3. User lambdas in automations need persistent data they can access asynchronously
// Convert span to vector for trigger
on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id);
});
return on_write_trigger;
@@ -30,10 +27,7 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_descriptor_on_write
Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory)
new Trigger<std::vector<uint8_t>, uint16_t>();
descriptor->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) {
// Convert span to vector for trigger - copy is necessary because:
// 1. Trigger stores the data for use in automation actions that execute later
// 2. The span is only valid during this callback (points to temporary BLE stack data)
// 3. User lambdas in automations need persistent data they can access asynchronously
// Convert span to vector for trigger
on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id);
});
return on_write_trigger;

View File

@@ -15,7 +15,6 @@ from esphome.components.esp32 import (
VARIANT_ESP32S2,
VARIANT_ESP32S3,
get_esp32_variant,
include_builtin_idf_component,
)
import esphome.config_validation as cv
from esphome.const import (
@@ -122,10 +121,6 @@ def get_default_tx_enqueue_timeout(bit_rate):
async def to_code(config):
# Legacy driver component provides driver/twai.h header
include_builtin_idf_component("driver")
# Also enable esp_driver_twai for future migration to new API
include_builtin_idf_component("esp_driver_twai")
var = cg.new_Pvariable(config[CONF_ID])
await canbus.register_canbus(var, config)

View File

@@ -1,12 +1,7 @@
from esphome import pins
import esphome.codegen as cg
from esphome.components import output
from esphome.components.esp32 import (
VARIANT_ESP32,
VARIANT_ESP32S2,
get_esp32_variant,
include_builtin_idf_component,
)
from esphome.components.esp32 import VARIANT_ESP32, VARIANT_ESP32S2, get_esp32_variant
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_NUMBER, CONF_PIN
@@ -43,7 +38,6 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
async def to_code(config):
include_builtin_idf_component("esp_driver_dac")
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await output.register_output(var, config)

View File

@@ -12,7 +12,6 @@ from esphome.const import (
KEY_FRAMEWORK_VERSION,
)
from esphome.core import CORE
from esphome.cpp_generator import add_define
CODEOWNERS = ["@swoboda1337"]
@@ -43,7 +42,6 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
add_define("USE_ESP32_HOSTED")
if config[CONF_ACTIVE_HIGH]:
esp32.add_idf_sdkconfig_option(
"CONFIG_ESP_HOSTED_SDIO_RESET_ACTIVE_HIGH",

View File

@@ -34,29 +34,14 @@ static const char *const ESP_HOSTED_VERSION_STR = STRINGIFY(ESP_HOSTED_VERSION_M
ESP_HOSTED_VERSION_MINOR_1) "." STRINGIFY(ESP_HOSTED_VERSION_PATCH_1);
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
// Parse an integer from str, advancing ptr past the number
// Returns false if no digits were parsed
static bool parse_int(const char *&ptr, int &value) {
char *end;
value = static_cast<int>(strtol(ptr, &end, 10));
if (end == ptr)
return false;
ptr = end;
return true;
}
// Parse version string "major.minor.patch" into components
// Returns true if at least major.minor was parsed
// Returns true if parsing succeeded
static bool parse_version(const std::string &version_str, int &major, int &minor, int &patch) {
major = minor = patch = 0;
const char *ptr = version_str.c_str();
if (!parse_int(ptr, major) || *ptr++ != '.' || !parse_int(ptr, minor))
return false;
if (*ptr == '.')
parse_int(++ptr, patch);
return true;
if (sscanf(version_str.c_str(), "%d.%d.%d", &major, &minor, &patch) >= 2) {
return true;
}
return false;
}
// Compare two versions, returns:
@@ -211,14 +196,11 @@ bool Esp32HostedUpdate::fetch_manifest_() {
int read_or_error = container->read(buf, sizeof(buf));
App.feed_wdt();
yield();
auto result =
http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout, container->is_read_complete());
auto result = http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout);
if (result == http_request::HttpReadLoopResult::RETRY)
continue;
// Note: COMPLETE is currently unreachable since the loop condition checks bytes_read < content_length,
// but this is defensive code in case chunked transfer encoding support is added in the future.
if (result != http_request::HttpReadLoopResult::DATA)
break; // COMPLETE, ERROR, or TIMEOUT
break; // ERROR or TIMEOUT
json_str.append(reinterpret_cast<char *>(buf), read_or_error);
}
container->end();
@@ -339,14 +321,9 @@ bool Esp32HostedUpdate::stream_firmware_to_coprocessor_() {
App.feed_wdt();
yield();
auto result =
http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout, container->is_read_complete());
auto result = http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout);
if (result == http_request::HttpReadLoopResult::RETRY)
continue;
// Note: COMPLETE is currently unreachable since the loop condition checks bytes_read < content_length,
// but this is defensive code in case chunked transfer encoding support is added in the future.
if (result == http_request::HttpReadLoopResult::COMPLETE)
break;
if (result != http_request::HttpReadLoopResult::DATA) {
if (result == http_request::HttpReadLoopResult::TIMEOUT) {
ESP_LOGE(TAG, "Timeout reading firmware data");

View File

@@ -5,7 +5,6 @@ from esphome import pins
import esphome.codegen as cg
from esphome.components import esp32, light
from esphome.components.const import CONF_USE_PSRAM
from esphome.components.esp32 import include_builtin_idf_component
import esphome.config_validation as cv
from esphome.const import (
CONF_CHIPSET,
@@ -130,9 +129,6 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
# Re-enable ESP-IDF's RMT driver (excluded by default to save compile time)
include_builtin_idf_component("esp_driver_rmt")
var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
await light.register_light(var, config)
await cg.register_component(var, config)

View File

@@ -6,7 +6,6 @@ from esphome.components.esp32 import (
VARIANT_ESP32S3,
get_esp32_variant,
gpio,
include_builtin_idf_component,
)
import esphome.config_validation as cv
from esphome.const import (
@@ -267,11 +266,6 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
# Re-enable ESP-IDF's touch sensor driver (excluded by default to save compile time)
include_builtin_idf_component("esp_driver_touch_sens")
# Legacy driver component provides driver/touch_sensor.h header
include_builtin_idf_component("driver")
touch = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(touch, config)

View File

@@ -205,7 +205,6 @@ async def to_code(config):
"pre:testing_mode.py",
"pre:exclude_updater.py",
"pre:exclude_waveform.py",
"pre:remove_float_printf.py",
"post:post_build.py",
],
)
@@ -343,8 +342,3 @@ def copy_files() -> None:
exclude_waveform_file,
CORE.relative_build_path("exclude_waveform.py"),
)
remove_float_printf_file = dir / "remove_float_printf.py.script"
copy_file_if_changed(
remove_float_printf_file,
CORE.relative_build_path("remove_float_printf.py"),
)

View File

@@ -1,45 +0,0 @@
# pylint: disable=E0602
Import("env") # noqa
# Remove float printf/scanf support from linker flags
# The Arduino ESP8266 framework unconditionally adds:
# -u _printf_float -u _scanf_float
# This forces inclusion of float formatting code (~7KB) even when not used.
#
# ESPHome avoids %f format specifiers in logging to not require this code.
# This script removes those flags to save flash space.
#
# Savings:
# - _dtoa_r: ~3.4KB (double-to-ASCII conversion)
# - _strtod_l: ~3.7KB (string-to-double conversion)
# - _printf_float: ~1.3KB
# - _scanf_float: ~1.3KB
# - Additional float math helpers
def remove_float_printf_flags(source, target, env):
"""Remove -u _printf_float and -u _scanf_float from linker flags.
This is called as a pre-action before the link step.
"""
linkflags = env.get("LINKFLAGS", [])
new_linkflags = []
i = 0
while i < len(linkflags):
flag = linkflags[i]
if flag == "-u" and i + 1 < len(linkflags):
next_flag = linkflags[i + 1]
if next_flag in ("_printf_float", "_scanf_float"):
print(f"ESPHome: Removing float printf support ({next_flag})")
i += 2 # Skip both -u and the symbol
continue
new_linkflags.append(flag)
i += 1
env.Replace(LINKFLAGS=new_linkflags)
# Register the callback to run before the link step
# This ensures it runs after the framework has added its flags
env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", remove_float_printf_flags)

View File

@@ -331,7 +331,7 @@ void ESPHomeOTAComponent::handle_data_() {
if (now - last_progress > 1000) {
last_progress = now;
float percentage = (total * 100.0f) / ota_size;
ESP_LOGD(TAG, "Progress: %s%d.%d%%", DECIMAL_1(percentage));
ESP_LOGD(TAG, "Progress: %0.1f%%", percentage);
#ifdef USE_OTA_STATE_LISTENER
this->notify_state_(ota::OTA_IN_PROGRESS, percentage, 0);
#endif

View File

@@ -13,7 +13,7 @@ from esphome.const import (
CONF_TRIGGER_ID,
CONF_WIFI,
)
from esphome.core import HexInt
from esphome.core import CORE, HexInt
from esphome.types import ConfigType
CODEOWNERS = ["@jesserockz"]
@@ -124,6 +124,9 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
if CORE.using_arduino:
cg.add_library("WiFi", None)
# ESP-NOW uses wake_loop_threadsafe() to wake the main loop from ESP-NOW callbacks
# This enables low-latency event processing instead of waiting for select() timeout
socket.require_wake_loop_threadsafe()

View File

@@ -14,7 +14,6 @@ from esphome.components.esp32 import (
add_idf_component,
add_idf_sdkconfig_option,
get_esp32_variant,
include_builtin_idf_component,
)
from esphome.components.network import ip_address_literal
from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface
@@ -420,13 +419,13 @@ async def to_code(config):
# Also disable WiFi/BT coexistence since WiFi is disabled
add_idf_sdkconfig_option("CONFIG_SW_COEXIST_ENABLE", False)
# Re-enable ESP-IDF's Ethernet driver (excluded by default to save compile time)
include_builtin_idf_component("esp_eth")
if config[CONF_TYPE] == "LAN8670":
# Add LAN867x 10BASE-T1S PHY support component
add_idf_component(name="espressif/lan867x", ref="2.0.0")
if CORE.using_arduino:
cg.add_library("WiFi", None)
CORE.add_job(final_step)

View File

@@ -802,8 +802,8 @@ void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) {
ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed");
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
char hex_buf[format_hex_pretty_size(PHY_REG_SIZE)];
ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE));
#endif
ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE));
/*
* Bit 7 is `RMII Reference Clock Select`. Default is `0`.
@@ -820,10 +820,8 @@ void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) {
ESPHL_ERROR_CHECK(err, "Write PHY Control 2 failed");
err = mac->read_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, &(phy_control_2));
ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed");
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s",
format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE));
#endif
}
}
#endif // USE_ETHERNET_KSZ8081

View File

@@ -16,8 +16,12 @@ namespace event {
#define LOG_EVENT(prefix, type, obj) \
if ((obj) != nullptr) { \
ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \
LOG_ENTITY_ICON(TAG, prefix, *(obj)); \
LOG_ENTITY_DEVICE_CLASS(TAG, prefix, *(obj)); \
if (!(obj)->get_icon_ref().empty()) { \
ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \
} \
if (!(obj)->get_device_class_ref().empty()) { \
ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class_ref().c_str()); \
} \
}
class Event : public EntityBase, public EntityBase_DeviceClass {

View File

@@ -3,7 +3,6 @@
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include <cinttypes>
@@ -20,8 +19,7 @@ static bool was_power_cycled() {
#endif
#ifdef USE_ESP8266
auto reset_reason = EspClass::getResetReason();
return ESPHOME_strcasecmp_P(reset_reason.c_str(), ESPHOME_PSTR("power On")) == 0 ||
ESPHOME_strcasecmp_P(reset_reason.c_str(), ESPHOME_PSTR("external system")) == 0;
return strcasecmp(reset_reason.c_str(), "power On") == 0 || strcasecmp(reset_reason.c_str(), "external system") == 0;
#endif
#ifdef USE_LIBRETINY
auto reason = lt_get_reboot_reason();

View File

@@ -71,7 +71,7 @@ void FanCall::validate_() {
auto traits = this->parent_.get_traits();
if (this->speed_.has_value()) {
this->speed_ = clamp(*this->speed_, 1, static_cast<int>(traits.supported_speed_count()));
this->speed_ = clamp(*this->speed_, 1, traits.supported_speed_count());
// https://developers.home-assistant.io/docs/core/entity/fan/#preset-modes
// "Manually setting a speed must disable any set preset mode"
@@ -227,7 +227,8 @@ void Fan::publish_state() {
constexpr uint32_t RESTORE_STATE_VERSION = 0x71700ABA;
optional<FanRestoreState> Fan::restore_state_() {
FanRestoreState recovered{};
this->rtc_ = this->make_entity_preference<FanRestoreState>(RESTORE_STATE_VERSION);
this->rtc_ =
global_preferences->make_preference<FanRestoreState>(this->get_preference_hash() ^ RESTORE_STATE_VERSION);
bool restored = this->rtc_.load(&recovered);
switch (this->restore_mode_) {

View File

@@ -11,7 +11,7 @@ namespace fan {
class FanTraits {
public:
FanTraits() = default;
FanTraits(bool oscillation, bool speed, bool direction, uint8_t speed_count)
FanTraits(bool oscillation, bool speed, bool direction, int speed_count)
: oscillation_(oscillation), speed_(speed), direction_(direction), speed_count_(speed_count) {}
/// Return if this fan supports oscillation.
@@ -23,9 +23,9 @@ class FanTraits {
/// Set whether this fan supports speed levels.
void set_speed(bool speed) { this->speed_ = speed; }
/// Return how many speed levels the fan has
uint8_t supported_speed_count() const { return this->speed_count_; }
int supported_speed_count() const { return this->speed_count_; }
/// Set how many speed levels this fan has.
void set_supported_speed_count(uint8_t speed_count) { this->speed_count_ = speed_count; }
void set_supported_speed_count(int speed_count) { this->speed_count_ = speed_count; }
/// Return if this fan supports changing direction
bool supports_direction() const { return this->direction_; }
/// Set whether this fan supports changing direction
@@ -64,7 +64,7 @@ class FanTraits {
bool oscillation_{false};
bool speed_{false};
bool direction_{false};
uint8_t speed_count_{};
int speed_count_{};
std::vector<const char *> preset_modes_{};
};

View File

@@ -9,55 +9,29 @@ from esphome.const import (
CONF_VALUE,
)
from esphome.core import CoroPriority, coroutine_with_priority
from esphome.types import ConfigType
CODEOWNERS = ["@esphome/core"]
globals_ns = cg.esphome_ns.namespace("globals")
GlobalsComponent = globals_ns.class_("GlobalsComponent", cg.Component)
RestoringGlobalsComponent = globals_ns.class_(
"RestoringGlobalsComponent", cg.PollingComponent
)
RestoringGlobalsComponent = globals_ns.class_("RestoringGlobalsComponent", cg.Component)
RestoringGlobalStringComponent = globals_ns.class_(
"RestoringGlobalStringComponent", cg.PollingComponent
"RestoringGlobalStringComponent", cg.Component
)
GlobalVarSetAction = globals_ns.class_("GlobalVarSetAction", automation.Action)
CONF_MAX_RESTORE_DATA_LENGTH = "max_restore_data_length"
# Base schema fields shared by both variants
_BASE_SCHEMA = {
cv.Required(CONF_ID): cv.declare_id(GlobalsComponent),
cv.Required(CONF_TYPE): cv.string_strict,
cv.Optional(CONF_INITIAL_VALUE): cv.string_strict,
cv.Optional(CONF_MAX_RESTORE_DATA_LENGTH): cv.int_range(0, 254),
}
# Non-restoring globals: regular Component (no polling needed)
_NON_RESTORING_SCHEMA = cv.Schema(
{
**_BASE_SCHEMA,
cv.Optional(CONF_RESTORE_VALUE, default=False): cv.boolean,
}
).extend(cv.COMPONENT_SCHEMA)
# Restoring globals: PollingComponent with configurable update_interval
_RESTORING_SCHEMA = cv.Schema(
{
**_BASE_SCHEMA,
cv.Optional(CONF_RESTORE_VALUE, default=True): cv.boolean,
}
).extend(cv.polling_component_schema("1s"))
def _globals_schema(config: ConfigType) -> ConfigType:
"""Select schema based on restore_value setting."""
if config.get(CONF_RESTORE_VALUE, False):
return _RESTORING_SCHEMA(config)
return _NON_RESTORING_SCHEMA(config)
MULTI_CONF = True
CONFIG_SCHEMA = _globals_schema
CONFIG_SCHEMA = cv.Schema(
{
cv.Required(CONF_ID): cv.declare_id(GlobalsComponent),
cv.Required(CONF_TYPE): cv.string_strict,
cv.Optional(CONF_INITIAL_VALUE): cv.string_strict,
cv.Optional(CONF_RESTORE_VALUE, default=False): cv.boolean,
cv.Optional(CONF_MAX_RESTORE_DATA_LENGTH): cv.int_range(0, 254),
}
).extend(cv.COMPONENT_SCHEMA)
# Run with low priority so that namespaces are registered first

View File

@@ -5,7 +5,8 @@
#include "esphome/core/helpers.h"
#include <cstring>
namespace esphome::globals {
namespace esphome {
namespace globals {
template<typename T> class GlobalsComponent : public Component {
public:
@@ -23,14 +24,13 @@ template<typename T> class GlobalsComponent : public Component {
T value_{};
};
template<typename T> class RestoringGlobalsComponent : public PollingComponent {
template<typename T> class RestoringGlobalsComponent : public Component {
public:
using value_type = T;
explicit RestoringGlobalsComponent() : PollingComponent(1000) {}
explicit RestoringGlobalsComponent(T initial_value) : PollingComponent(1000), value_(initial_value) {}
explicit RestoringGlobalsComponent() = default;
explicit RestoringGlobalsComponent(T initial_value) : value_(initial_value) {}
explicit RestoringGlobalsComponent(
std::array<typename std::remove_extent<T>::type, std::extent<T>::value> initial_value)
: PollingComponent(1000) {
std::array<typename std::remove_extent<T>::type, std::extent<T>::value> initial_value) {
memcpy(this->value_, initial_value.data(), sizeof(T));
}
@@ -44,7 +44,7 @@ template<typename T> class RestoringGlobalsComponent : public PollingComponent {
float get_setup_priority() const override { return setup_priority::HARDWARE; }
void update() override { store_value_(); }
void loop() override { store_value_(); }
void on_shutdown() override { store_value_(); }
@@ -66,14 +66,13 @@ template<typename T> class RestoringGlobalsComponent : public PollingComponent {
};
// Use with string or subclasses of strings
template<typename T, uint8_t SZ> class RestoringGlobalStringComponent : public PollingComponent {
template<typename T, uint8_t SZ> class RestoringGlobalStringComponent : public Component {
public:
using value_type = T;
explicit RestoringGlobalStringComponent() : PollingComponent(1000) {}
explicit RestoringGlobalStringComponent(T initial_value) : PollingComponent(1000) { this->value_ = initial_value; }
explicit RestoringGlobalStringComponent() = default;
explicit RestoringGlobalStringComponent(T initial_value) { this->value_ = initial_value; }
explicit RestoringGlobalStringComponent(
std::array<typename std::remove_extent<T>::type, std::extent<T>::value> initial_value)
: PollingComponent(1000) {
std::array<typename std::remove_extent<T>::type, std::extent<T>::value> initial_value) {
memcpy(this->value_, initial_value.data(), sizeof(T));
}
@@ -91,7 +90,7 @@ template<typename T, uint8_t SZ> class RestoringGlobalStringComponent : public P
float get_setup_priority() const override { return setup_priority::HARDWARE; }
void update() override { store_value_(); }
void loop() override { store_value_(); }
void on_shutdown() override { store_value_(); }
@@ -145,4 +144,5 @@ template<typename T> T &id(GlobalsComponent<T> *value) { return value->value();
template<typename T> T &id(RestoringGlobalsComponent<T> *value) { return value->value(); }
template<typename T, uint8_t SZ> T &id(RestoringGlobalStringComponent<T, SZ> *value) { return value->value(); }
} // namespace esphome::globals
} // namespace globals
} // namespace esphome

View File

@@ -350,7 +350,8 @@ ClimateTraits HaierClimateBase::traits() { return traits_; }
void HaierClimateBase::initialization() {
constexpr uint32_t restore_settings_version = 0xA77D21EF;
this->base_rtc_ = this->make_entity_preference<HaierBaseSettings>(restore_settings_version);
this->base_rtc_ =
global_preferences->make_preference<HaierBaseSettings>(this->get_preference_hash() ^ restore_settings_version);
HaierBaseSettings recovered;
if (!this->base_rtc_.load(&recovered)) {
recovered = {false, true};

View File

@@ -515,7 +515,8 @@ haier_protocol::HaierMessage HonClimate::get_power_message(bool state) {
void HonClimate::initialization() {
HaierClimateBase::initialization();
constexpr uint32_t restore_settings_version = 0x57EB59DDUL;
this->hon_rtc_ = this->make_entity_preference<HonSettings>(restore_settings_version);
this->hon_rtc_ =
global_preferences->make_preference<HonSettings>(this->get_preference_hash() ^ restore_settings_version);
HonSettings recovered;
if (this->hon_rtc_.load(&recovered)) {
this->settings_ = recovered;

View File

@@ -39,7 +39,7 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_DECAY_MODE, default="SLOW"): cv.enum(
DECAY_MODE_OPTIONS, upper=True
),
cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1, max=255),
cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1),
cv.Optional(CONF_ENABLE_PIN): cv.use_id(output.FloatOutput),
cv.Optional(CONF_PRESET_MODES): validate_preset_modes,
}

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