Compare commits

..

17 Commits

Author SHA1 Message Date
J. Nick Koston
b445d46888 Merge remote-tracking branch 'upstream/dev' into mqtt_enum_flash
# Conflicts:
#	esphome/components/mqtt/mqtt_alarm_control_panel.cpp
#	esphome/components/mqtt/mqtt_component.cpp
#	esphome/components/mqtt/mqtt_component.h
#	esphome/components/mqtt/mqtt_cover.cpp
#	esphome/components/mqtt/mqtt_valve.cpp
2026-01-26 17:30:37 -10:00
J. Nick Koston
6da4f95258 [mqtt] Refactor state publishing with dedicated enum-to-string helpers 2026-01-25 23:04:33 -10:00
J. Nick Koston
0207e6e8b5 [mqtt] Refactor state publishing with dedicated enum-to-string helpers 2026-01-25 23:04:26 -10:00
J. Nick Koston
ee6e12913c [mqtt] Refactor state publishing with dedicated enum-to-string helpers 2026-01-25 23:02:18 -10:00
J. Nick Koston
d95ef154aa Merge branch 'dev' into mqtt_stack_part_2 2026-01-25 22:37:19 -10:00
J. Nick Koston
e006216ad3 Merge branch 'dev' into mqtt_stack_part_2 2026-01-21 18:37:55 -10:00
J. Nick Koston
d66d05dbfc [mqtt] Use stack buffers for publish_state() topic building 2026-01-21 11:08:06 -10:00
J. Nick Koston
bba447e656 Merge branch 'dev' into mqtt_formatting 2026-01-21 11:06:26 -10:00
J. Nick Koston
77b6720a25 Merge branch 'dev' into mqtt_formatting 2026-01-19 17:40:37 -10:00
J. Nick Koston
2f8f052f43 Merge branch 'dev' into mqtt_formatting 2026-01-18 18:44:11 -10:00
J. Nick Koston
86e70c7e76 more 2026-01-17 07:38:51 -10:00
J. Nick Koston
40025bb277 tweaks to reduce RAM 2026-01-17 07:34:22 -10:00
J. Nick Koston
438bb96687 tweaks to reduce RAM 2026-01-17 07:28:44 -10:00
J. Nick Koston
c1e1325af2 Merge branch 'dev' into mqtt_formatting 2026-01-17 07:24:11 -10:00
pre-commit-ci-lite[bot]
944194e04e [pre-commit.ci lite] apply automatic fixes 2026-01-15 00:02:35 +00:00
J. Nick Koston
d27d6d64da Update esphome/components/mqtt/mqtt_component.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-14 14:00:57 -10:00
J. Nick Koston
2182d1e9f0 [mqtt] Use stack buffers for discovery message formatting 2026-01-14 13:57:45 -10:00
316 changed files with 10986 additions and 15085 deletions

View File

@@ -1 +1 @@
069fa9526c52f7c580a9ec17c7678d12f142221387e9b561c18f95394d4629a3
d565b0589e35e692b5f2fc0c14723a99595b4828a3a3ef96c442e86a23176c00

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@6bc82e05fd0ea64601dd4b465378bbcf57de0314 # v4.32.1
uses: github/codeql-action/init@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -86,6 +86,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@6bc82e05fd0ea64601dd4b465378bbcf57de0314 # v4.32.1
uses: github/codeql-action/analyze@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
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

@@ -104,7 +104,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 +133,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

View File

@@ -4,8 +4,6 @@ from __future__ import annotations
from collections import defaultdict
from collections.abc import Callable
import heapq
from operator import itemgetter
import sys
from typing import TYPE_CHECKING
@@ -31,10 +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
# Width for symbol name display in top symbols report
COL_TOP_SYMBOL_NAME: int = 55
# Column width constants
COL_COMPONENT: int = 29
@@ -153,37 +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: (symbol, demangled, size, section, component)
all_symbols = [
(symbol, demangled, size, section, component)
for component, symbols in self._component_symbols.items()
for symbol, demangled, size, section in symbols
]
# Get top N symbols by size using heapq for efficiency
top_symbols = heapq.nlargest(
self.TOP_SYMBOLS_LIMIT, all_symbols, key=itemgetter(2)
)
lines.append("")
lines.append(f"Top {self.TOP_SYMBOLS_LIMIT} Largest Symbols:")
# Calculate truncation limit from column width (leaving room for "...")
truncate_limit = self.COL_TOP_SYMBOL_NAME - 3
for i, (_, demangled, size, section, component) in enumerate(top_symbols):
# Format section label
section_label = f"[{section[1:]}]" if section else ""
# Truncate demangled name if too long
demangled_display = (
f"{demangled[:truncate_limit]}..."
if len(demangled) > self.COL_TOP_SYMBOL_NAME
else demangled
)
lines.append(
f"{i + 1:>2}. {size:>7,} B {section_label:<8} {demangled_display:<{self.COL_TOP_SYMBOL_NAME}} {component}"
)
def generate_report(self, detailed: bool = False) -> str:
"""Generate a formatted memory report."""
components = sorted(
@@ -285,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")

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

@@ -1385,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));

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

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

@@ -211,7 +211,7 @@ void APIServer::loop() {
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
// Fire trigger after client is removed so api.connected reflects the true state
this->client_disconnected_trigger_.trigger(client_name, client_peername);
this->client_disconnected_trigger_->trigger(client_name, client_peername);
#endif
// Don't increment client_index since we need to process the swapped element
}

View File

@@ -227,10 +227,12 @@ class APIServer : public Component,
#endif
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
Trigger<std::string, std::string> *get_client_connected_trigger() { return &this->client_connected_trigger_; }
Trigger<std::string, std::string> *get_client_connected_trigger() const { return this->client_connected_trigger_; }
#endif
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
Trigger<std::string, std::string> *get_client_disconnected_trigger() { return &this->client_disconnected_trigger_; }
Trigger<std::string, std::string> *get_client_disconnected_trigger() const {
return this->client_disconnected_trigger_;
}
#endif
protected:
@@ -251,10 +253,10 @@ class APIServer : public Component,
// Pointers and pointer-like types first (4 bytes each)
std::unique_ptr<socket::Socket> socket_ = nullptr;
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
Trigger<std::string, std::string> client_connected_trigger_;
Trigger<std::string, std::string> *client_connected_trigger_ = new Trigger<std::string, std::string>();
#endif
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
Trigger<std::string, std::string> client_disconnected_trigger_;
Trigger<std::string, std::string> *client_disconnected_trigger_ = new Trigger<std::string, std::string>();
#endif
// 4-byte aligned types

View File

@@ -136,10 +136,12 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
void set_wants_response() { this->flags_.wants_response = true; }
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
Trigger<JsonObjectConst, Ts...> *get_success_trigger_with_response() { return &this->success_trigger_with_response_; }
Trigger<JsonObjectConst, Ts...> *get_success_trigger_with_response() const {
return this->success_trigger_with_response_;
}
#endif
Trigger<Ts...> *get_success_trigger() { return &this->success_trigger_; }
Trigger<std::string, Ts...> *get_error_trigger() { return &this->error_trigger_; }
Trigger<Ts...> *get_success_trigger() const { return this->success_trigger_; }
Trigger<std::string, Ts...> *get_error_trigger() const { return this->error_trigger_; }
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
void play(const Ts &...x) override {
@@ -185,14 +187,14 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
if (response.is_success()) {
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
if (this->flags_.wants_response) {
this->success_trigger_with_response_.trigger(response.get_json(), args...);
this->success_trigger_with_response_->trigger(response.get_json(), args...);
} else
#endif
{
this->success_trigger_.trigger(args...);
this->success_trigger_->trigger(args...);
}
} else {
this->error_trigger_.trigger(response.get_error_message(), args...);
this->error_trigger_->trigger(response.get_error_message(), args...);
}
},
captured_args);
@@ -249,10 +251,10 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
TemplatableStringValue<Ts...> response_template_{""};
Trigger<JsonObjectConst, Ts...> success_trigger_with_response_;
Trigger<JsonObjectConst, Ts...> *success_trigger_with_response_ = new Trigger<JsonObjectConst, Ts...>();
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
Trigger<Ts...> success_trigger_;
Trigger<std::string, Ts...> error_trigger_;
Trigger<Ts...> *success_trigger_ = new Trigger<Ts...>();
Trigger<std::string, Ts...> *error_trigger_ = new Trigger<std::string, Ts...>();
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
struct Flags {

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

@@ -6,7 +6,8 @@ namespace bang_bang {
static const char *const TAG = "bang_bang.climate";
BangBangClimate::BangBangClimate() = default;
BangBangClimate::BangBangClimate()
: idle_trigger_(new Trigger<>()), cool_trigger_(new Trigger<>()), heat_trigger_(new Trigger<>()) {}
void BangBangClimate::setup() {
this->sensor_->add_on_state_callback([this](float state) {
@@ -159,13 +160,13 @@ void BangBangClimate::switch_to_action_(climate::ClimateAction action) {
switch (action) {
case climate::CLIMATE_ACTION_OFF:
case climate::CLIMATE_ACTION_IDLE:
trig = &this->idle_trigger_;
trig = this->idle_trigger_;
break;
case climate::CLIMATE_ACTION_COOLING:
trig = &this->cool_trigger_;
trig = this->cool_trigger_;
break;
case climate::CLIMATE_ACTION_HEATING:
trig = &this->heat_trigger_;
trig = this->heat_trigger_;
break;
default:
trig = nullptr;
@@ -203,9 +204,9 @@ void BangBangClimate::set_away_config(const BangBangClimateTargetTempConfig &awa
void BangBangClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; }
void BangBangClimate::set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
Trigger<> *BangBangClimate::get_idle_trigger() { return &this->idle_trigger_; }
Trigger<> *BangBangClimate::get_cool_trigger() { return &this->cool_trigger_; }
Trigger<> *BangBangClimate::get_heat_trigger() { return &this->heat_trigger_; }
Trigger<> *BangBangClimate::get_idle_trigger() const { return this->idle_trigger_; }
Trigger<> *BangBangClimate::get_cool_trigger() const { return this->cool_trigger_; }
Trigger<> *BangBangClimate::get_heat_trigger() const { return this->heat_trigger_; }
void BangBangClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
void BangBangClimate::set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; }

View File

@@ -30,9 +30,9 @@ class BangBangClimate : public climate::Climate, public Component {
void set_normal_config(const BangBangClimateTargetTempConfig &normal_config);
void set_away_config(const BangBangClimateTargetTempConfig &away_config);
Trigger<> *get_idle_trigger();
Trigger<> *get_cool_trigger();
Trigger<> *get_heat_trigger();
Trigger<> *get_idle_trigger() const;
Trigger<> *get_cool_trigger() const;
Trigger<> *get_heat_trigger() const;
protected:
/// Override control to change settings of the climate device.
@@ -57,13 +57,17 @@ class BangBangClimate : public climate::Climate, public Component {
*
* In idle mode, the controller is assumed to have both heating and cooling disabled.
*/
Trigger<> idle_trigger_;
Trigger<> *idle_trigger_{nullptr};
/** The trigger to call when the controller should switch to cooling mode.
*/
Trigger<> cool_trigger_;
Trigger<> *cool_trigger_{nullptr};
/** The trigger to call when the controller should switch to heating mode.
*
* A null value for this attribute means that the controller has no heating action
* For example window blinds, where only cooling (blinds closed) and not-cooling
* (blinds open) is possible.
*/
Trigger<> heat_trigger_;
Trigger<> *heat_trigger_{nullptr};
/** A reference to the trigger that was previously active.
*
* This is so that the previous trigger can be stopped before enabling a new one.

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

@@ -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

@@ -156,7 +156,7 @@ void CC1101Component::call_listeners_(const std::vector<uint8_t> &packet, float
for (auto &listener : this->listeners_) {
listener->on_packet(packet, freq_offset, rssi, lqi);
}
this->packet_trigger_.trigger(packet, freq_offset, rssi, lqi);
this->packet_trigger_->trigger(packet, freq_offset, rssi, lqi);
}
void CC1101Component::loop() {

View File

@@ -79,7 +79,7 @@ class CC1101Component : public Component,
// Packet mode operations
CC1101Error transmit_packet(const std::vector<uint8_t> &packet);
void register_listener(CC1101Listener *listener) { this->listeners_.push_back(listener); }
Trigger<std::vector<uint8_t>, float, float, uint8_t> *get_packet_trigger() { return &this->packet_trigger_; }
Trigger<std::vector<uint8_t>, float, float, uint8_t> *get_packet_trigger() const { return this->packet_trigger_; }
protected:
uint16_t chip_id_{0};
@@ -96,7 +96,8 @@ class CC1101Component : public Component,
// Packet handling
void call_listeners_(const std::vector<uint8_t> &packet, float freq_offset, float rssi, uint8_t lqi);
Trigger<std::vector<uint8_t>, float, float, uint8_t> packet_trigger_;
Trigger<std::vector<uint8_t>, float, float, uint8_t> *packet_trigger_{
new Trigger<std::vector<uint8_t>, float, float, uint8_t>()};
std::vector<uint8_t> packet_;
std::vector<CC1101Listener *> listeners_;

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

@@ -360,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 {};

View File

@@ -187,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

@@ -152,10 +152,6 @@ void CSE7766Component::parse_data_() {
if (this->power_sensor_ != nullptr) {
this->power_sensor_->publish_state(power);
}
} else if (this->power_sensor_ != nullptr) {
// No valid power measurement from chip - publish 0W to avoid stale readings
// This typically happens when current is below the measurable threshold (~50mA)
this->power_sensor_->publish_state(0.0f);
}
float current = 0.0f;

View File

@@ -66,7 +66,7 @@ void CurrentBasedCover::loop() {
if (this->current_operation == COVER_OPERATION_OPENING) {
if (this->malfunction_detection_ && this->is_closing_()) { // Malfunction
this->direction_idle_();
this->malfunction_trigger_.trigger();
this->malfunction_trigger_->trigger();
ESP_LOGI(TAG, "'%s' - Malfunction detected during opening. Current flow detected in close circuit",
this->name_.c_str());
} else if (this->is_opening_blocked_()) { // Blocked
@@ -87,7 +87,7 @@ void CurrentBasedCover::loop() {
} else if (this->current_operation == COVER_OPERATION_CLOSING) {
if (this->malfunction_detection_ && this->is_opening_()) { // Malfunction
this->direction_idle_();
this->malfunction_trigger_.trigger();
this->malfunction_trigger_->trigger();
ESP_LOGI(TAG, "'%s' - Malfunction detected during closing. Current flow detected in open circuit",
this->name_.c_str());
} else if (this->is_closing_blocked_()) { // Blocked
@@ -221,15 +221,15 @@ void CurrentBasedCover::start_direction_(CoverOperation dir) {
Trigger<> *trig;
switch (dir) {
case COVER_OPERATION_IDLE:
trig = &this->stop_trigger_;
trig = this->stop_trigger_;
break;
case COVER_OPERATION_OPENING:
this->last_operation_ = dir;
trig = &this->open_trigger_;
trig = this->open_trigger_;
break;
case COVER_OPERATION_CLOSING:
this->last_operation_ = dir;
trig = &this->close_trigger_;
trig = this->close_trigger_;
break;
default:
return;

View File

@@ -16,9 +16,9 @@ class CurrentBasedCover : public cover::Cover, public Component {
void dump_config() override;
float get_setup_priority() const override;
Trigger<> *get_stop_trigger() { return &this->stop_trigger_; }
Trigger<> *get_stop_trigger() const { return this->stop_trigger_; }
Trigger<> *get_open_trigger() { return &this->open_trigger_; }
Trigger<> *get_open_trigger() const { return this->open_trigger_; }
void set_open_sensor(sensor::Sensor *open_sensor) { this->open_sensor_ = open_sensor; }
void set_open_moving_current_threshold(float open_moving_current_threshold) {
this->open_moving_current_threshold_ = open_moving_current_threshold;
@@ -28,7 +28,7 @@ class CurrentBasedCover : public cover::Cover, public Component {
}
void set_open_duration(uint32_t open_duration) { this->open_duration_ = open_duration; }
Trigger<> *get_close_trigger() { return &this->close_trigger_; }
Trigger<> *get_close_trigger() const { return this->close_trigger_; }
void set_close_sensor(sensor::Sensor *close_sensor) { this->close_sensor_ = close_sensor; }
void set_close_moving_current_threshold(float close_moving_current_threshold) {
this->close_moving_current_threshold_ = close_moving_current_threshold;
@@ -44,7 +44,7 @@ class CurrentBasedCover : public cover::Cover, public Component {
void set_malfunction_detection(bool malfunction_detection) { this->malfunction_detection_ = malfunction_detection; }
void set_start_sensing_delay(uint32_t start_sensing_delay) { this->start_sensing_delay_ = start_sensing_delay; }
Trigger<> *get_malfunction_trigger() { return &this->malfunction_trigger_; }
Trigger<> *get_malfunction_trigger() const { return this->malfunction_trigger_; }
cover::CoverTraits get_traits() override;
@@ -64,23 +64,23 @@ class CurrentBasedCover : public cover::Cover, public Component {
void recompute_position_();
Trigger<> stop_trigger_;
Trigger<> *stop_trigger_{new Trigger<>()};
sensor::Sensor *open_sensor_{nullptr};
Trigger<> open_trigger_;
Trigger<> *open_trigger_{new Trigger<>()};
float open_moving_current_threshold_;
float open_obstacle_current_threshold_{FLT_MAX};
uint32_t open_duration_;
sensor::Sensor *close_sensor_{nullptr};
Trigger<> close_trigger_;
Trigger<> *close_trigger_{new Trigger<>()};
float close_moving_current_threshold_;
float close_obstacle_current_threshold_{FLT_MAX};
uint32_t close_duration_;
uint32_t max_duration_{UINT32_MAX};
bool malfunction_detection_{true};
Trigger<> malfunction_trigger_;
Trigger<> *malfunction_trigger_{new Trigger<>()};
uint32_t start_sensing_delay_;
float obstacle_rollback_;

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
@@ -222,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

@@ -141,15 +141,15 @@ void EndstopCover::start_direction_(CoverOperation dir) {
Trigger<> *trig;
switch (dir) {
case COVER_OPERATION_IDLE:
trig = &this->stop_trigger_;
trig = this->stop_trigger_;
break;
case COVER_OPERATION_OPENING:
this->last_operation_ = dir;
trig = &this->open_trigger_;
trig = this->open_trigger_;
break;
case COVER_OPERATION_CLOSING:
this->last_operation_ = dir;
trig = &this->close_trigger_;
trig = this->close_trigger_;
break;
default:
return;

View File

@@ -15,9 +15,9 @@ class EndstopCover : public cover::Cover, public Component {
void dump_config() override;
float get_setup_priority() const override;
Trigger<> *get_open_trigger() { return &this->open_trigger_; }
Trigger<> *get_close_trigger() { return &this->close_trigger_; }
Trigger<> *get_stop_trigger() { return &this->stop_trigger_; }
Trigger<> *get_open_trigger() const { return this->open_trigger_; }
Trigger<> *get_close_trigger() const { return this->close_trigger_; }
Trigger<> *get_stop_trigger() const { return this->stop_trigger_; }
void set_open_endstop(binary_sensor::BinarySensor *open_endstop) { this->open_endstop_ = open_endstop; }
void set_close_endstop(binary_sensor::BinarySensor *close_endstop) { this->close_endstop_ = close_endstop; }
void set_open_duration(uint32_t open_duration) { this->open_duration_ = open_duration; }
@@ -39,11 +39,11 @@ class EndstopCover : public cover::Cover, public Component {
binary_sensor::BinarySensor *open_endstop_;
binary_sensor::BinarySensor *close_endstop_;
Trigger<> open_trigger_;
Trigger<> *open_trigger_{new Trigger<>()};
uint32_t open_duration_;
Trigger<> close_trigger_;
Trigger<> *close_trigger_{new Trigger<>()};
uint32_t close_duration_;
Trigger<> stop_trigger_;
Trigger<> *stop_trigger_{new Trigger<>()};
uint32_t max_duration_{UINT32_MAX};
Trigger<> *prev_command_trigger_{nullptr};

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

@@ -53,10 +53,8 @@ from .const import ( # noqa
KEY_BOARD,
KEY_COMPONENTS,
KEY_ESP32,
KEY_EXCLUDE_COMPONENTS,
KEY_EXTRA_BUILD_FILES,
KEY_FLASH_SIZE,
KEY_FULL_CERT_BUNDLE,
KEY_PATH,
KEY_REF,
KEY_REPO,
@@ -87,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"
@@ -116,42 +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
)
# ESP32 (original) chip revision options
# Setting minimum revision to 3.0 or higher:
# - Reduces flash size by excluding workaround code for older chip bugs
@@ -241,9 +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
CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS] = set(DEFAULT_EXCLUDED_IDF_COMPONENTS)
CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse(
config[CONF_FRAMEWORK][CONF_VERSION]
)
@@ -369,28 +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 add_extra_script(stage: str, filename: str, path: Path):
"""Add an extra script to the project."""
key = f"{stage}:{filename}"
@@ -734,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:
@@ -775,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 *)
@@ -914,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(
@@ -1119,19 +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 _add_yaml_idf_components(components: list[ConfigType]):
"""Add IDF components from YAML config with final priority to override code-added components."""
@@ -1215,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")
@@ -1260,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
@@ -1335,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)
@@ -1349,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
@@ -1392,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.
@@ -1482,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))
@@ -1545,11 +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)
APP_PARTITION_SIZES = {
"2MB": 0x0C0000, # 768 KB

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,14 +6,12 @@ KEY_FLASH_SIZE = "flash_size"
KEY_VARIANT = "variant"
KEY_SDKCONFIG_OPTIONS = "sdkconfig_options"
KEY_COMPONENTS = "components"
KEY_EXCLUDE_COMPONENTS = "exclude_components"
KEY_REPO = "repo"
KEY_REF = "ref"
KEY_REFRESH = "refresh"
KEY_PATH = "path"
KEY_SUBMODULES = "submodules"
KEY_EXTRA_BUILD_FILES = "extra_build_files"
KEY_FULL_CERT_BUNDLE = "full_cert_bundle"
VARIANT_ESP32 = "ESP32"
VARIANT_ESP32C2 = "ESP32C2"

View File

@@ -203,11 +203,10 @@ class ESP32Preferences : public ESPPreferences {
}
};
static ESP32Preferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void setup_preferences() {
s_preferences.open();
global_preferences = &s_preferences;
auto *prefs = new ESP32Preferences(); // NOLINT(cppcoreguidelines-owning-memory)
prefs->open();
global_preferences = prefs;
}
} // namespace esp32

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

@@ -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

@@ -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

@@ -17,6 +17,10 @@ namespace esphome::esp8266 {
static const char *const TAG = "esp8266.preferences";
static uint32_t *s_flash_storage = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static bool s_prevent_write = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static bool s_flash_dirty = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static constexpr uint32_t ESP_RTC_USER_MEM_START = 0x60001200;
static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_WORDS = 128;
static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_BYTES = ESP_RTC_USER_MEM_SIZE_WORDS * 4;
@@ -39,11 +43,6 @@ static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = 128;
static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = 64;
#endif
static uint32_t
s_flash_storage[ESP8266_FLASH_STORAGE_SIZE]; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static bool s_prevent_write = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static bool s_flash_dirty = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static inline bool esp_rtc_user_mem_read(uint32_t index, uint32_t *dest) {
if (index >= ESP_RTC_USER_MEM_SIZE_WORDS) {
return false;
@@ -181,6 +180,7 @@ class ESP8266Preferences : public ESPPreferences {
uint32_t current_flash_offset = 0; // in words
void setup() {
s_flash_storage = new uint32_t[ESP8266_FLASH_STORAGE_SIZE]; // NOLINT
ESP_LOGVV(TAG, "Loading preferences from flash");
{
@@ -283,11 +283,10 @@ class ESP8266Preferences : public ESPPreferences {
}
};
static ESP8266Preferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void setup_preferences() {
s_preferences.setup();
global_preferences = &s_preferences;
auto *pref = new ESP8266Preferences(); // NOLINT(cppcoreguidelines-owning-memory)
pref->setup();
global_preferences = pref;
}
void preferences_prevent_write(bool prevent) { s_prevent_write = prevent; }

View File

@@ -1,6 +1,6 @@
import logging
from esphome import automation, pins
from esphome import pins
import esphome.codegen as cg
from esphome.components.esp32 import (
VARIANT_ESP32,
@@ -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
@@ -35,8 +34,6 @@ from esphome.const import (
CONF_MODE,
CONF_MOSI_PIN,
CONF_NUMBER,
CONF_ON_CONNECT,
CONF_ON_DISCONNECT,
CONF_PAGE_ID,
CONF_PIN,
CONF_POLLING_INTERVAL,
@@ -239,8 +236,6 @@ BASE_SCHEMA = cv.Schema(
cv.Optional(CONF_DOMAIN, default=".local"): cv.domain_name,
cv.Optional(CONF_USE_ADDRESS): cv.string_strict,
cv.Optional(CONF_MAC_ADDRESS): cv.mac_address,
cv.Optional(CONF_ON_CONNECT): automation.validate_automation(single=True),
cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation(single=True),
}
).extend(cv.COMPONENT_SCHEMA)
@@ -424,9 +419,6 @@ 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")
@@ -434,18 +426,6 @@ async def to_code(config):
if CORE.using_arduino:
cg.add_library("WiFi", None)
if on_connect_config := config.get(CONF_ON_CONNECT):
cg.add_define("USE_ETHERNET_CONNECT_TRIGGER")
await automation.build_automation(
var.get_connect_trigger(), [], on_connect_config
)
if on_disconnect_config := config.get(CONF_ON_DISCONNECT):
cg.add_define("USE_ETHERNET_DISCONNECT_TRIGGER")
await automation.build_automation(
var.get_disconnect_trigger(), [], on_disconnect_config
)
CORE.add_job(final_step)

View File

@@ -309,9 +309,6 @@ void EthernetComponent::loop() {
this->dump_connect_params_();
this->status_clear_warning();
#ifdef USE_ETHERNET_CONNECT_TRIGGER
this->connect_trigger_.trigger();
#endif
} else if (now - this->connect_begin_ > 15000) {
ESP_LOGW(TAG, "Connecting failed; reconnecting");
this->start_connect_();
@@ -321,16 +318,10 @@ void EthernetComponent::loop() {
if (!this->started_) {
ESP_LOGI(TAG, "Stopped connection");
this->state_ = EthernetComponentState::STOPPED;
#ifdef USE_ETHERNET_DISCONNECT_TRIGGER
this->disconnect_trigger_.trigger();
#endif
} else if (!this->connected_) {
ESP_LOGW(TAG, "Connection lost; reconnecting");
this->state_ = EthernetComponentState::CONNECTING;
this->start_connect_();
#ifdef USE_ETHERNET_DISCONNECT_TRIGGER
this->disconnect_trigger_.trigger();
#endif
} else {
this->finish_connect_();
// When connected and stable, disable the loop to save CPU cycles

View File

@@ -4,7 +4,6 @@
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/automation.h"
#include "esphome/components/network/ip_address.h"
#ifdef USE_ESP32
@@ -120,12 +119,6 @@ class EthernetComponent : public Component {
void add_ip_state_listener(EthernetIPStateListener *listener) { this->ip_state_listeners_.push_back(listener); }
#endif
#ifdef USE_ETHERNET_CONNECT_TRIGGER
Trigger<> *get_connect_trigger() { return &this->connect_trigger_; }
#endif
#ifdef USE_ETHERNET_DISCONNECT_TRIGGER
Trigger<> *get_disconnect_trigger() { return &this->disconnect_trigger_; }
#endif
protected:
static void eth_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data);
static void got_ip_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data);
@@ -197,12 +190,6 @@ class EthernetComponent : public Component {
StaticVector<EthernetIPStateListener *, ESPHOME_ETHERNET_IP_STATE_LISTENERS> ip_state_listeners_;
#endif
#ifdef USE_ETHERNET_CONNECT_TRIGGER
Trigger<> connect_trigger_;
#endif
#ifdef USE_ETHERNET_DISCONNECT_TRIGGER
Trigger<> disconnect_trigger_;
#endif
private:
// Stores a pointer to a string literal (static storage duration).
// ONLY set from Python-generated code with string literals - never dynamic strings.

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

@@ -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

@@ -335,18 +335,18 @@ void FeedbackCover::start_direction_(CoverOperation dir) {
switch (dir) {
case COVER_OPERATION_IDLE:
trig = &this->stop_trigger_;
trig = this->stop_trigger_;
break;
case COVER_OPERATION_OPENING:
this->last_operation_ = dir;
trig = &this->open_trigger_;
trig = this->open_trigger_;
#ifdef USE_BINARY_SENSOR
obstacle = this->open_obstacle_;
#endif
break;
case COVER_OPERATION_CLOSING:
this->last_operation_ = dir;
trig = &this->close_trigger_;
trig = this->close_trigger_;
#ifdef USE_BINARY_SENSOR
obstacle = this->close_obstacle_;
#endif

View File

@@ -17,9 +17,9 @@ class FeedbackCover : public cover::Cover, public Component {
void loop() override;
void dump_config() override;
Trigger<> *get_open_trigger() { return &this->open_trigger_; }
Trigger<> *get_close_trigger() { return &this->close_trigger_; }
Trigger<> *get_stop_trigger() { return &this->stop_trigger_; }
Trigger<> *get_open_trigger() const { return this->open_trigger_; }
Trigger<> *get_close_trigger() const { return this->close_trigger_; }
Trigger<> *get_stop_trigger() const { return this->stop_trigger_; }
#ifdef USE_BINARY_SENSOR
void set_open_endstop(binary_sensor::BinarySensor *open_endstop);
@@ -61,9 +61,9 @@ class FeedbackCover : public cover::Cover, public Component {
binary_sensor::BinarySensor *close_obstacle_{nullptr};
#endif
Trigger<> open_trigger_;
Trigger<> close_trigger_;
Trigger<> stop_trigger_;
Trigger<> *open_trigger_{new Trigger<>()};
Trigger<> *close_trigger_{new Trigger<>()};
Trigger<> *stop_trigger_{new Trigger<>()};
uint32_t open_duration_{0};
uint32_t close_duration_{0};

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

@@ -66,11 +66,10 @@ ESPPreferenceObject HostPreferences::make_preference(size_t length, uint32_t typ
return ESPPreferenceObject(backend);
};
static HostPreferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void setup_preferences() {
host_preferences = &s_preferences;
global_preferences = &s_preferences;
auto *pref = new HostPreferences(); // NOLINT(cppcoreguidelines-owning-memory)
host_preferences = pref;
global_preferences = pref;
}
bool HostPreferenceBackend::save(const uint8_t *data, size_t len) {

View File

@@ -155,9 +155,6 @@ async def to_code(config):
cg.add(var.set_watchdog_timeout(timeout_ms))
if CORE.is_esp32:
# Re-enable ESP-IDF's HTTP client (excluded by default to save compile time)
esp32.include_builtin_idf_component("esp_http_client")
cg.add(var.set_buffer_size_rx(config[CONF_BUFFER_SIZE_RX]))
cg.add(var.set_buffer_size_tx(config[CONF_BUFFER_SIZE_TX]))
cg.add(var.set_verify_ssl(config[CONF_VERIFY_SSL]))
@@ -168,16 +165,6 @@ async def to_code(config):
ca_cert_content = f.read()
cg.add(var.set_ca_certificate(ca_cert_content))
else:
# Uses the certificate bundle configured in esp32 component.
# By default, ESPHome uses the CMN (common CAs) bundle which covers
# ~99% of websites including GitHub, Let's Encrypt, DigiCert, etc.
# If connecting to services with uncommon CAs, components can call:
# esp32.require_full_certificate_bundle()
# Or users can set in their config:
# esp32:
# framework:
# advanced:
# use_full_certificate_bundle: true
esp32.add_idf_sdkconfig_option(
"CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True
)

View File

@@ -26,7 +26,6 @@ struct Header {
enum HttpStatus {
HTTP_STATUS_OK = 200,
HTTP_STATUS_NO_CONTENT = 204,
HTTP_STATUS_RESET_CONTENT = 205,
HTTP_STATUS_PARTIAL_CONTENT = 206,
/* 3xx - Redirection */
@@ -127,21 +126,19 @@ struct HttpReadResult {
/// Result of processing a non-blocking read with timeout (for manual loops)
enum class HttpReadLoopResult : uint8_t {
DATA, ///< Data was read, process it
COMPLETE, ///< All content has been read, caller should exit loop
RETRY, ///< No data yet, already delayed, caller should continue loop
ERROR, ///< Read error, caller should exit loop
TIMEOUT, ///< Timeout waiting for data, caller should exit loop
DATA, ///< Data was read, process it
RETRY, ///< No data yet, already delayed, caller should continue loop
ERROR, ///< Read error, caller should exit loop
TIMEOUT, ///< Timeout waiting for data, caller should exit loop
};
/// Process a read result with timeout tracking and delay handling
/// @param bytes_read_or_error Return value from read() - positive for bytes read, negative for error
/// @param last_data_time Time of last successful read, updated when data received
/// @param timeout_ms Maximum time to wait for data
/// @param is_read_complete Whether all expected content has been read (from HttpContainer::is_read_complete())
/// @return How the caller should proceed - see HttpReadLoopResult enum
inline HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_t &last_data_time, uint32_t timeout_ms,
bool is_read_complete) {
/// @return DATA if data received, RETRY if should continue loop, ERROR/TIMEOUT if should exit
inline HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_t &last_data_time,
uint32_t timeout_ms) {
if (bytes_read_or_error > 0) {
last_data_time = millis();
return HttpReadLoopResult::DATA;
@@ -149,10 +146,7 @@ inline HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_
if (bytes_read_or_error < 0) {
return HttpReadLoopResult::ERROR;
}
// bytes_read_or_error == 0: either "no data yet" or "all content read"
if (is_read_complete) {
return HttpReadLoopResult::COMPLETE;
}
// bytes_read_or_error == 0: no data available yet
if (millis() - last_data_time >= timeout_ms) {
return HttpReadLoopResult::TIMEOUT;
}
@@ -165,9 +159,9 @@ class HttpRequestComponent;
class HttpContainer : public Parented<HttpRequestComponent> {
public:
virtual ~HttpContainer() = default;
size_t content_length{0};
int status_code{-1}; ///< -1 indicates no response received yet
uint32_t duration_ms{0};
size_t content_length;
int status_code;
uint32_t duration_ms;
/**
* @brief Read data from the HTTP response body.
@@ -200,24 +194,9 @@ class HttpContainer : public Parented<HttpRequestComponent> {
virtual void end() = 0;
void set_secure(bool secure) { this->secure_ = secure; }
void set_chunked(bool chunked) { this->is_chunked_ = chunked; }
size_t get_bytes_read() const { return this->bytes_read_; }
/// Check if all expected content has been read
/// For chunked responses, returns false (completion detected via read() returning error/EOF)
bool is_read_complete() const {
// Per RFC 9112, these responses have no body:
// - 1xx (Informational), 204 No Content, 205 Reset Content, 304 Not Modified
if ((this->status_code >= 100 && this->status_code < 200) || this->status_code == HTTP_STATUS_NO_CONTENT ||
this->status_code == HTTP_STATUS_RESET_CONTENT || this->status_code == HTTP_STATUS_NOT_MODIFIED) {
return true;
}
// For non-chunked responses, complete when bytes_read >= content_length
// This handles both Content-Length: 0 and Content-Length: N cases
return !this->is_chunked_ && this->bytes_read_ >= this->content_length;
}
/**
* @brief Get response headers.
*
@@ -230,7 +209,6 @@ class HttpContainer : public Parented<HttpRequestComponent> {
protected:
size_t bytes_read_{0};
bool secure_{false};
bool is_chunked_{false}; ///< True if response uses chunked transfer encoding
std::map<std::string, std::list<std::string>> response_headers_{};
};
@@ -241,7 +219,7 @@ class HttpContainer : public Parented<HttpRequestComponent> {
/// @param total_size Total bytes to read
/// @param chunk_size Maximum bytes per read call
/// @param timeout_ms Read timeout in milliseconds
/// @return HttpReadResult with status and error_code on failure; use container->get_bytes_read() for total bytes read
/// @return HttpReadResult with status and error_code on failure
inline HttpReadResult http_read_fully(HttpContainer *container, uint8_t *buffer, size_t total_size, size_t chunk_size,
uint32_t timeout_ms) {
size_t read_index = 0;
@@ -253,11 +231,9 @@ inline HttpReadResult http_read_fully(HttpContainer *container, uint8_t *buffer,
App.feed_wdt();
yield();
auto result = http_read_loop_result(read_bytes_or_error, last_data_time, timeout_ms, container->is_read_complete());
auto result = http_read_loop_result(read_bytes_or_error, last_data_time, timeout_ms);
if (result == HttpReadLoopResult::RETRY)
continue;
if (result == HttpReadLoopResult::COMPLETE)
break; // Server sent less data than requested, but transfer is complete
if (result == HttpReadLoopResult::ERROR)
return {HttpReadStatus::ERROR, read_bytes_or_error};
if (result == HttpReadLoopResult::TIMEOUT)
@@ -356,13 +332,13 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
void set_json(std::function<void(Ts..., JsonObject)> json_func) { this->json_func_ = json_func; }
#ifdef USE_HTTP_REQUEST_RESPONSE
Trigger<std::shared_ptr<HttpContainer>, std::string &, Ts...> *get_success_trigger_with_response() {
return &this->success_trigger_with_response_;
Trigger<std::shared_ptr<HttpContainer>, std::string &, Ts...> *get_success_trigger_with_response() const {
return this->success_trigger_with_response_;
}
#endif
Trigger<std::shared_ptr<HttpContainer>, Ts...> *get_success_trigger() { return &this->success_trigger_; }
Trigger<std::shared_ptr<HttpContainer>, Ts...> *get_success_trigger() const { return this->success_trigger_; }
Trigger<Ts...> *get_error_trigger() { return &this->error_trigger_; }
Trigger<Ts...> *get_error_trigger() const { return this->error_trigger_; }
void set_max_response_buffer_size(size_t max_response_buffer_size) {
this->max_response_buffer_size_ = max_response_buffer_size;
@@ -396,7 +372,7 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
auto captured_args = std::make_tuple(x...);
if (container == nullptr) {
std::apply([this](Ts... captured_args_inner) { this->error_trigger_.trigger(captured_args_inner...); },
std::apply([this](Ts... captured_args_inner) { this->error_trigger_->trigger(captured_args_inner...); },
captured_args);
return;
}
@@ -417,12 +393,11 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
int read_or_error = container->read(buf + read_index, std::min<size_t>(max_length - read_index, 512));
App.feed_wdt();
yield();
auto result =
http_read_loop_result(read_or_error, last_data_time, read_timeout, container->is_read_complete());
auto result = http_read_loop_result(read_or_error, last_data_time, read_timeout);
if (result == HttpReadLoopResult::RETRY)
continue;
if (result != HttpReadLoopResult::DATA)
break; // COMPLETE, ERROR, or TIMEOUT
break; // ERROR or TIMEOUT
read_index += read_or_error;
}
response_body.reserve(read_index);
@@ -431,14 +406,14 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
}
std::apply(
[this, &container, &response_body](Ts... captured_args_inner) {
this->success_trigger_with_response_.trigger(container, response_body, captured_args_inner...);
this->success_trigger_with_response_->trigger(container, response_body, captured_args_inner...);
},
captured_args);
} else
#endif
{
std::apply([this, &container](
Ts... captured_args_inner) { this->success_trigger_.trigger(container, captured_args_inner...); },
Ts... captured_args_inner) { this->success_trigger_->trigger(container, captured_args_inner...); },
captured_args);
}
container->end();
@@ -458,10 +433,12 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
std::map<const char *, TemplatableValue<std::string, Ts...>> json_{};
std::function<void(Ts..., JsonObject)> json_func_{nullptr};
#ifdef USE_HTTP_REQUEST_RESPONSE
Trigger<std::shared_ptr<HttpContainer>, std::string &, Ts...> success_trigger_with_response_;
Trigger<std::shared_ptr<HttpContainer>, std::string &, Ts...> *success_trigger_with_response_ =
new Trigger<std::shared_ptr<HttpContainer>, std::string &, Ts...>();
#endif
Trigger<std::shared_ptr<HttpContainer>, Ts...> success_trigger_;
Trigger<Ts...> error_trigger_;
Trigger<std::shared_ptr<HttpContainer>, Ts...> *success_trigger_ =
new Trigger<std::shared_ptr<HttpContainer>, Ts...>();
Trigger<Ts...> *error_trigger_ = new Trigger<Ts...>();
size_t max_response_buffer_size_{SIZE_MAX};
};

View File

@@ -131,27 +131,9 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur
}
}
// HTTPClient::getSize() returns -1 for chunked transfer encoding (no Content-Length).
// When cast to size_t, -1 becomes SIZE_MAX (4294967295 on 32-bit).
// The read() method handles this: bytes_read_ can never reach SIZE_MAX, so the
// early return check (bytes_read_ >= content_length) will never trigger.
//
// TODO: Chunked transfer encoding is NOT properly supported on Arduino.
// The implementation in #7884 was incomplete - it only works correctly on ESP-IDF where
// esp_http_client_read() decodes chunks internally. On Arduino, using getStreamPtr()
// returns raw TCP data with chunk framing (e.g., "12a\r\n{json}\r\n0\r\n\r\n") instead
// of decoded content. This wasn't noticed because requests would complete and payloads
// were only examined on IDF. The long transfer times were also masked by the misleading
// "HTTP on Arduino version >= 3.1 is **very** slow" warning above. This causes two issues:
// 1. Response body is corrupted - contains chunk size headers mixed with data
// 2. Cannot detect end of transfer - connection stays open (keep-alive), causing timeout
// The proper fix would be to use getString() for chunked responses, which decodes chunks
// internally, but this buffers the entire response in memory.
int content_length = container->client_.getSize();
ESP_LOGD(TAG, "Content-Length: %d", content_length);
container->content_length = (size_t) content_length;
// -1 (SIZE_MAX when cast to size_t) means chunked transfer encoding
container->set_chunked(content_length == -1);
container->duration_ms = millis() - start;
return container;
@@ -185,23 +167,17 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
}
int available_data = stream_ptr->available();
// For chunked transfer encoding, HTTPClient::getSize() returns -1, which becomes SIZE_MAX when
// cast to size_t. SIZE_MAX - bytes_read_ is still huge, so it won't limit the read.
size_t remaining = (this->content_length > 0) ? (this->content_length - this->bytes_read_) : max_len;
int bufsize = std::min(max_len, std::min(remaining, (size_t) available_data));
int bufsize = std::min(max_len, std::min(this->content_length - this->bytes_read_, (size_t) available_data));
if (bufsize == 0) {
this->duration_ms += (millis() - start);
// Check if we've read all expected content (non-chunked only)
// For chunked encoding (content_length == SIZE_MAX), is_read_complete() returns false
if (this->is_read_complete()) {
// Check if we've read all expected content
if (this->bytes_read_ >= this->content_length) {
return 0; // All content read successfully
}
// No data available - check if connection is still open
// For chunked encoding, !connected() after reading means EOF (all chunks received)
// For known content_length with bytes_read_ < content_length, it means connection dropped
if (!stream_ptr->connected()) {
return HTTP_ERROR_CONNECTION_CLOSED; // Connection closed or EOF for chunked
return HTTP_ERROR_CONNECTION_CLOSED; // Connection closed prematurely
}
return 0; // No data yet, caller should retry
}

View File

@@ -157,10 +157,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
}
container->feed_wdt();
// esp_http_client_fetch_headers() returns 0 for chunked transfer encoding (no Content-Length header).
// The read() method handles content_length == 0 specially to support chunked responses.
container->content_length = esp_http_client_fetch_headers(client);
container->set_chunked(esp_http_client_is_chunked_response(client));
container->feed_wdt();
container->status_code = esp_http_client_get_status_code(client);
container->feed_wdt();
@@ -196,7 +193,6 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
container->feed_wdt();
container->content_length = esp_http_client_fetch_headers(client);
container->set_chunked(esp_http_client_is_chunked_response(client));
container->feed_wdt();
container->status_code = esp_http_client_get_status_code(client);
container->feed_wdt();
@@ -229,21 +225,14 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
//
// We normalize to HttpContainer::read() contract:
// > 0: bytes read
// 0: all content read (only returned when content_length is known and fully read)
// 0: no data yet / all content read (caller should check bytes_read vs content_length)
// < 0: error/connection closed
//
// Note on chunked transfer encoding:
// esp_http_client_fetch_headers() returns 0 for chunked responses (no Content-Length header).
// We handle this by skipping the content_length check when content_length is 0,
// allowing esp_http_client_read() to handle chunked decoding internally and signal EOF
// by returning 0.
int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
const uint32_t start = millis();
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
// Check if we've already read all expected content (non-chunked only)
// For chunked responses (content_length == 0), esp_http_client_read() handles EOF
if (this->is_read_complete()) {
// Check if we've already read all expected content
if (this->bytes_read_ >= this->content_length) {
return 0; // All content read successfully
}
@@ -258,13 +247,7 @@ int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
return read_len_or_error;
}
// esp_http_client_read() returns 0 in two cases:
// 1. Known content_length: connection closed before all data received (error)
// 2. Chunked encoding (content_length == 0): end of stream reached (EOF)
// For case 1, returning HTTP_ERROR_CONNECTION_CLOSED is correct.
// For case 2, 0 indicates that all chunked data has already been delivered
// in previous successful read() calls, so treating this as a closed
// connection does not cause any loss of response data.
// Connection closed by server before all content received
if (read_len_or_error == 0) {
return HTTP_ERROR_CONNECTION_CLOSED;
}

View File

@@ -130,13 +130,9 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
App.feed_wdt();
yield();
auto result = http_read_loop_result(bufsize_or_error, last_data_time, read_timeout, container->is_read_complete());
auto result = http_read_loop_result(bufsize_or_error, last_data_time, read_timeout);
if (result == 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 for OTA in the future.
if (result == HttpReadLoopResult::COMPLETE)
break;
if (result != HttpReadLoopResult::DATA) {
if (result == HttpReadLoopResult::TIMEOUT) {
ESP_LOGE(TAG, "Timeout reading data");

View File

@@ -2,25 +2,6 @@
from . import BoardConfig
# Huidu HD-WF1
BoardConfig(
"huidu-hd-wf1",
r1_pin=2,
g1_pin=6,
b1_pin=3,
r2_pin=4,
g2_pin=8,
b2_pin=5,
a_pin=39,
b_pin=38,
c_pin=37,
d_pin=36,
e_pin=12,
lat_pin=33,
oe_pin=35,
clk_pin=34,
)
# Huidu HD-WF2
BoardConfig(
"huidu-hd-wf2",

View File

@@ -587,7 +587,7 @@ def _build_config_struct(
async def to_code(config: ConfigType) -> None:
add_idf_component(
name="esphome/esp-hub75",
ref="0.3.2",
ref="0.3.0",
)
# Set compile-time configuration via build flags (so external library sees them)

View File

@@ -11,6 +11,12 @@ namespace i2c {
static const char *const TAG = "i2c";
void I2CBus::i2c_scan_() {
// suppress logs from the IDF I2C library during the scan
#if defined(USE_ESP32) && defined(USE_LOGGER)
auto previous = esp_log_level_get("*");
esp_log_level_set("*", ESP_LOG_NONE);
#endif
for (uint8_t address = 8; address != 120; address++) {
auto err = write_readv(address, nullptr, 0, nullptr, 0);
if (err == ERROR_OK) {
@@ -21,6 +27,9 @@ void I2CBus::i2c_scan_() {
// it takes 16sec to scan on nrf52. It prevents board reset.
arch_feed_wdt();
}
#if defined(USE_ESP32) && defined(USE_LOGGER)
esp_log_level_set("*", previous);
#endif
}
ErrorCode I2CDevice::read_register(uint8_t a_register, uint8_t *data, size_t len) {

View File

@@ -1,11 +1,6 @@
from esphome import pins
import esphome.codegen as cg
from esphome.components.esp32 import (
add_idf_sdkconfig_option,
get_esp32_variant,
include_builtin_idf_component,
)
from esphome.components.esp32.const import (
VARIANT_ESP32,
VARIANT_ESP32C3,
VARIANT_ESP32C5,
@@ -15,6 +10,8 @@ from esphome.components.esp32.const import (
VARIANT_ESP32P4,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
add_idf_sdkconfig_option,
get_esp32_variant,
)
import esphome.config_validation as cv
from esphome.const import CONF_BITS_PER_SAMPLE, CONF_CHANNEL, CONF_ID, CONF_SAMPLE_RATE
@@ -275,10 +272,6 @@ FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
# Re-enable ESP-IDF's I2S driver (excluded by default to save compile time)
include_builtin_idf_component("esp_driver_i2s")
if use_legacy():
cg.add_define("USE_I2S_LEGACY")

View File

@@ -10,7 +10,7 @@ static const char *const TAG = "integration";
void IntegrationSensor::setup() {
if (this->restore_) {
this->pref_ = this->make_entity_preference<float>();
this->pref_ = global_preferences->make_preference<float>(this->get_preference_hash());
float preference_value = 0;
this->pref_.load(&preference_value);
this->result_ = preference_value;

View File

@@ -184,7 +184,7 @@ static inline bool validate_header_footer(const uint8_t *header_footer, const ui
void LD2450Component::setup() {
#ifdef USE_NUMBER
if (this->presence_timeout_number_ != nullptr) {
this->pref_ = this->presence_timeout_number_->make_entity_preference<float>();
this->pref_ = global_preferences->make_preference<float>(this->presence_timeout_number_->get_preference_hash());
this->set_presence_timeout();
}
#endif
@@ -451,7 +451,7 @@ void LD2450Component::handle_periodic_data_() {
int16_t ty = 0;
int16_t td = 0;
int16_t ts = 0;
float angle = 0;
int16_t angle = 0;
uint8_t index = 0;
Direction direction{DIRECTION_UNDEFINED};
bool is_moving = false;

View File

@@ -143,7 +143,6 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
],
icon=ICON_FORMAT_TEXT_ROTATION_ANGLE_UP,
unit_of_measurement=UNIT_DEGREES,
accuracy_decimals=1,
),
cv.Optional(CONF_DISTANCE): sensor.sensor_schema(
device_class=DEVICE_CLASS_DISTANCE,

View File

@@ -189,11 +189,10 @@ class LibreTinyPreferences : public ESPPreferences {
}
};
static LibreTinyPreferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void setup_preferences() {
s_preferences.open();
global_preferences = &s_preferences;
auto *prefs = new LibreTinyPreferences(); // NOLINT(cppcoreguidelines-owning-memory)
prefs->open();
global_preferences = prefs;
}
} // namespace libretiny

View File

@@ -138,20 +138,20 @@ class LambdaLightEffect : public LightEffect {
class AutomationLightEffect : public LightEffect {
public:
AutomationLightEffect(const char *name) : LightEffect(name) {}
void stop() override { this->trig_.stop_action(); }
void stop() override { this->trig_->stop_action(); }
void apply() override {
if (!this->trig_.is_action_running()) {
this->trig_.trigger();
if (!this->trig_->is_action_running()) {
this->trig_->trigger();
}
}
Trigger<> *get_trig() { return &this->trig_; }
Trigger<> *get_trig() const { return trig_; }
/// Get the current effect index for use in automations.
/// Useful for automations that need to know which effect is running.
uint32_t get_current_index() const { return this->get_index(); }
protected:
Trigger<> trig_;
Trigger<> *trig_{new Trigger<>};
};
struct StrobeLightEffectColor {

View File

@@ -44,7 +44,7 @@ void LightState::setup() {
case LIGHT_RESTORE_DEFAULT_ON:
case LIGHT_RESTORE_INVERTED_DEFAULT_OFF:
case LIGHT_RESTORE_INVERTED_DEFAULT_ON:
this->rtc_ = this->make_entity_preference<LightStateRTCState>();
this->rtc_ = global_preferences->make_preference<LightStateRTCState>(this->get_preference_hash());
// Attempt to load from preferences, else fall back to default values
if (!this->rtc_.load(&recovered)) {
recovered.state = (this->restore_mode_ == LIGHT_RESTORE_DEFAULT_ON ||
@@ -57,7 +57,7 @@ void LightState::setup() {
break;
case LIGHT_RESTORE_AND_OFF:
case LIGHT_RESTORE_AND_ON:
this->rtc_ = this->make_entity_preference<LightStateRTCState>();
this->rtc_ = global_preferences->make_preference<LightStateRTCState>(this->get_preference_hash());
this->rtc_.load(&recovered);
recovered.state = (this->restore_mode_ == LIGHT_RESTORE_AND_ON);
break;

View File

@@ -2,7 +2,6 @@
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
namespace esphome::lock {
@@ -85,21 +84,21 @@ LockCall &LockCall::set_state(optional<LockState> state) {
this->state_ = state;
return *this;
}
LockCall &LockCall::set_state(const char *state) {
if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("LOCKED")) == 0) {
LockCall &LockCall::set_state(const std::string &state) {
if (str_equals_case_insensitive(state, "LOCKED")) {
this->set_state(LOCK_STATE_LOCKED);
} else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("UNLOCKED")) == 0) {
} else if (str_equals_case_insensitive(state, "UNLOCKED")) {
this->set_state(LOCK_STATE_UNLOCKED);
} else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("JAMMED")) == 0) {
} else if (str_equals_case_insensitive(state, "JAMMED")) {
this->set_state(LOCK_STATE_JAMMED);
} else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("LOCKING")) == 0) {
} else if (str_equals_case_insensitive(state, "LOCKING")) {
this->set_state(LOCK_STATE_LOCKING);
} else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("UNLOCKING")) == 0) {
} else if (str_equals_case_insensitive(state, "UNLOCKING")) {
this->set_state(LOCK_STATE_UNLOCKING);
} else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("NONE")) == 0) {
} else if (str_equals_case_insensitive(state, "NONE")) {
this->set_state(LOCK_STATE_NONE);
} else {
ESP_LOGW(TAG, "'%s' - Unrecognized state %s", this->parent_->get_name().c_str(), state);
ESP_LOGW(TAG, "'%s' - Unrecognized state %s", this->parent_->get_name().c_str(), state.c_str());
}
return *this;
}

View File

@@ -14,7 +14,9 @@ class Lock;
#define LOG_LOCK(prefix, type, obj) \
if ((obj) != nullptr) { \
ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \
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()); \
} \
if ((obj)->traits.get_assumed_state()) { \
ESP_LOGCONFIG(TAG, "%s Assumed State: YES", prefix); \
} \
@@ -83,8 +85,7 @@ class LockCall {
/// Set the state of the lock device.
LockCall &set_state(optional<LockState> state);
/// Set the state of the lock device based on a string.
LockCall &set_state(const char *state);
LockCall &set_state(const std::string &state) { return this->set_state(state.c_str()); }
LockCall &set_state(const std::string &state);
void perform();

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