Compare commits

...

44 Commits

Author SHA1 Message Date
dependabot[bot]
6de2049076 Bump actions/cache from 5.0.2 to 5.0.3 in /.github/actions/restore-python (#13622)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-29 14:35:52 -06:00
dependabot[bot]
cd43f8474e Bump actions/cache from 5.0.2 to 5.0.3 (#13621)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-29 14:35:32 -06:00
J. Nick Koston
ecc0b366b3 [esp32] Reduce compile time by excluding unused IDF components (#13610) 2026-01-29 13:21:12 -06:00
tomaszduda23
6a17db8857 [nrf52,zigbee] Support for number component (#13581) 2026-01-29 11:52:46 -05:00
Keith Burzinski
0843ec6ae8 [const] Move CONF_AUDIO_DAC (#13614) 2026-01-29 04:39:40 +00:00
J. Nick Koston
74c84c8747 [esp32] Add advanced sdkconfig options to reduce build time and binary size (#13611) 2026-01-28 18:20:39 -10:00
rwrozelle
3e9a6c582e [mdns] Do not broadcast registration when using openthread component (#13592) 2026-01-28 18:16:59 -10:00
Keith Burzinski
084113926c [es8156] Add bits_per_sample validation, comment code (#13612) 2026-01-28 22:03:50 -06:00
J. Nick Koston
a5f60750c2 [tx20] Eliminate heap allocations in wind sensor (#13298) 2026-01-29 16:07:41 +13:00
Clyde Stubbs
a382383d83 [workflows] Add deprecation check (#13584) 2026-01-29 12:08:45 +13:00
Clyde Stubbs
03cfd87b16 [waveshare_epaper] Add deprecation message (#13583) 2026-01-29 09:44:21 +13:00
Clyde Stubbs
6d8294c2d3 [workflows] Refactor auto-label-pr script into modular JS (#13582) 2026-01-29 09:42:55 +13:00
J. Nick Koston
6a3205f4db [globals] Convert restoring globals to PollingComponent to reduce CPU usage (#13345) 2026-01-28 20:35:26 +00:00
dependabot[bot]
6f22509883 Bump docker/login-action from 3.6.0 to 3.7.0 in the docker-actions group (#13606)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-28 09:42:05 -10:00
J. Nick Koston
455ade0dca [http_request] Fix empty body for chunked transfer encoding responses (#13599) 2026-01-28 09:41:42 -10:00
J. Nick Koston
87fcfc9d76 [wifi] Fix ESP8266 yield panic when WiFi scan fails (#13603) 2026-01-28 09:40:00 -10:00
tomaszduda23
d86048cc2d [nrf52,zigbee] Address change (#13580) 2026-01-28 11:41:04 -05:00
J. Nick Koston
e1355de4cb [runtime_stats] Eliminate heap churn by using stack-allocated buffer for sorting (#13586) 2026-01-28 16:06:33 +00:00
Cody Cutrer
7385c4cf3d [ld2450] preserve precision of angle (#13600) 2026-01-28 11:04:43 -05:00
tomaszduda23
3bd6ec4ec7 [nrf52,zigbee] Time synchronization (#12236)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-01-28 15:51:17 +00:00
J. Nick Koston
051604f284 [wifi] Filter scan results to only store matching networks (#13409)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-28 05:37:05 -10:00
Dan Schafer
10dfd95ff2 [esp32] Add pin definitions for adafruit_feather_esp32s3_reversetft (#13273) 2026-01-28 09:50:19 -05:00
Hypothalamus
22e0a8ce2e [hub75] Add Huidu HD-WF1 board configuration (#13341) 2026-01-27 20:10:49 -10:00
J. Nick Koston
b4f63fd992 [core] Add LOG_ENTITY_ICON/DEVICE_CLASS/UNIT_OF_MEASUREMENT macros (#13578) 2026-01-28 05:11:30 +00:00
tomaszduda23
ded835ab63 [nrf52] Move toolchain to platform (#13498)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-01-28 04:51:18 +00:00
J. Nick Koston
73a249c075 [esp32] Default to CMN certificate bundle, saving ~51KB flash (#13574) 2026-01-28 04:02:01 +00:00
J. Nick Koston
fe6f27c526 [text_sensor] Use in-place mutation for filters to reduce heap allocations (#13475) 2026-01-27 17:33:46 -10:00
J. Nick Koston
f73c539ea7 [web_server] Add RP2040 platform support (#13576) 2026-01-27 17:18:31 -10:00
Edward Firmo
f87aa384d0 [nextion] Fix alternative code path for dump_device_info (#13566)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-01-27 16:31:00 -10:00
J. Nick Koston
f9687a2a31 [web_server_idf] Replace heap-allocated url() with stack-based url_to() (#13407) 2026-01-28 14:02:19 +13:00
Stuart Parmenter
f084d320fc [hub75] Update esp-hub75 to 0.3.2 (#13572)
Co-authored-by: Claude <noreply@anthropic.com>
2026-01-27 09:24:13 -10:00
esphomebot
f93382445e Update webserver local assets to 20260127-190637 (#13573)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-01-27 19:21:26 +00:00
J. Nick Koston
463363a08d [web_server] Add name_id to SSE for entity ID format migration (#13535) 2026-01-27 09:08:46 -10:00
J. Nick Koston
a0790f926e [libretiny] Regenerate boards for v1.11.0 (#13539) 2026-01-28 07:59:01 +13:00
J. Nick Koston
ca59ab8f37 [esp32] Eliminate dead exception class code via linker wraps (#13564) 2026-01-27 07:47:34 -10:00
J. Nick Koston
b2474c6de9 [nfc] Use StaticVector for NFC UID storage to eliminate heap allocation (#13507) 2026-01-26 19:43:52 -10:00
J. Nick Koston
3aaf10b6a8 [web_server_base] Update ESPAsyncWebServer to 3.9.5 (#13467) 2026-01-27 04:18:57 +00:00
J. Nick Koston
33f545a8e3 [factory_reset] Store reset reason comparison strings in flash on ESP8266 (#13547)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-01-27 03:50:49 +00:00
J. Nick Koston
d056e1040b [mqtt] Store command comparison strings in flash on ESP8266 (#13546)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-01-27 03:48:06 +00:00
J. Nick Koston
75a78b2bf3 [core] Encapsulate entity preference creation to prepare for hash migration (#13505) 2026-01-26 17:35:45 -10:00
J. Nick Koston
cd6314dc96 [socket] ESP8266: call delay(0) instead of esp_delay(0, cb) for zero timeout (#13530) 2026-01-26 17:34:55 -10:00
J. Nick Koston
f91bffff9a [wifi] Avoid heap allocation when building AP SSID (#13474) 2026-01-26 17:32:58 -10:00
J. Nick Koston
5cbe9af485 [rp2040] Use SmallBufferWithHeapFallback for preferences (#13501) 2026-01-26 17:32:03 -10:00
J. Nick Koston
a7fbecb25c [ci] Soft-deprecate str_sprintf/str_snprintf to prevent hidden heap allocations (#13227) 2026-01-26 17:28:07 -10:00
160 changed files with 11555 additions and 9747 deletions

View File

@@ -1 +1 @@
d565b0589e35e692b5f2fc0c14723a99595b4828a3a3ef96c442e86a23176c00 cf3d341206b4184ec8b7fe85141aef4fe4696aa720c3f8a06d4e57930574bdab

View File

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

View File

@@ -0,0 +1,38 @@
// Constants and markers for PR auto-labeling
module.exports = {
BOT_COMMENT_MARKER: '<!-- auto-label-pr-bot -->',
CODEOWNERS_MARKER: '<!-- codeowners-request -->',
TOO_BIG_MARKER: '<!-- too-big-request -->',
DEPRECATED_COMPONENT_MARKER: '<!-- deprecated-component-request -->',
MANAGED_LABELS: [
'new-component',
'new-platform',
'new-target-platform',
'merging-to-release',
'merging-to-beta',
'chained-pr',
'core',
'small-pr',
'dashboard',
'github-actions',
'by-code-owner',
'has-tests',
'needs-tests',
'needs-docs',
'needs-codeowners',
'too-big',
'labeller-recheck',
'bugfix',
'new-feature',
'breaking-change',
'developer-breaking-change',
'code-quality',
'deprecated-component'
],
DOCS_PR_PATTERNS: [
/https:\/\/github\.com\/esphome\/esphome-docs\/pull\/\d+/,
/esphome\/esphome-docs#\d+/
]
};

View File

@@ -0,0 +1,373 @@
const fs = require('fs');
const { DOCS_PR_PATTERNS } = require('./constants');
// Strategy: Merge branch detection
async function detectMergeBranch(context) {
const labels = new Set();
const baseRef = context.payload.pull_request.base.ref;
if (baseRef === 'release') {
labels.add('merging-to-release');
} else if (baseRef === 'beta') {
labels.add('merging-to-beta');
} else if (baseRef !== 'dev') {
labels.add('chained-pr');
}
return labels;
}
// Strategy: Component and platform labeling
async function detectComponentPlatforms(changedFiles, apiData) {
const labels = new Set();
const componentRegex = /^esphome\/components\/([^\/]+)\//;
const targetPlatformRegex = new RegExp(`^esphome\/components\/(${apiData.targetPlatforms.join('|')})/`);
for (const file of changedFiles) {
const componentMatch = file.match(componentRegex);
if (componentMatch) {
labels.add(`component: ${componentMatch[1]}`);
}
const platformMatch = file.match(targetPlatformRegex);
if (platformMatch) {
labels.add(`platform: ${platformMatch[1]}`);
}
}
return labels;
}
// Strategy: New component detection
async function detectNewComponents(prFiles) {
const labels = new Set();
const addedFiles = prFiles.filter(file => file.status === 'added').map(file => file.filename);
for (const file of addedFiles) {
const componentMatch = file.match(/^esphome\/components\/([^\/]+)\/__init__\.py$/);
if (componentMatch) {
try {
const content = fs.readFileSync(file, 'utf8');
if (content.includes('IS_TARGET_PLATFORM = True')) {
labels.add('new-target-platform');
}
} catch (error) {
console.log(`Failed to read content of ${file}:`, error.message);
}
labels.add('new-component');
}
}
return labels;
}
// Strategy: New platform detection
async function detectNewPlatforms(prFiles, apiData) {
const labels = new Set();
const addedFiles = prFiles.filter(file => file.status === 'added').map(file => file.filename);
for (const file of addedFiles) {
const platformFileMatch = file.match(/^esphome\/components\/([^\/]+)\/([^\/]+)\.py$/);
if (platformFileMatch) {
const [, component, platform] = platformFileMatch;
if (apiData.platformComponents.includes(platform)) {
labels.add('new-platform');
}
}
const platformDirMatch = file.match(/^esphome\/components\/([^\/]+)\/([^\/]+)\/__init__\.py$/);
if (platformDirMatch) {
const [, component, platform] = platformDirMatch;
if (apiData.platformComponents.includes(platform)) {
labels.add('new-platform');
}
}
}
return labels;
}
// Strategy: Core files detection
async function detectCoreChanges(changedFiles) {
const labels = new Set();
const coreFiles = changedFiles.filter(file =>
file.startsWith('esphome/core/') ||
(file.startsWith('esphome/') && file.split('/').length === 2)
);
if (coreFiles.length > 0) {
labels.add('core');
}
return labels;
}
// Strategy: PR size detection
async function detectPRSize(prFiles, totalAdditions, totalDeletions, totalChanges, isMegaPR, SMALL_PR_THRESHOLD, TOO_BIG_THRESHOLD) {
const labels = new Set();
if (totalChanges <= SMALL_PR_THRESHOLD) {
labels.add('small-pr');
return labels;
}
const testAdditions = prFiles
.filter(file => file.filename.startsWith('tests/'))
.reduce((sum, file) => sum + (file.additions || 0), 0);
const testDeletions = prFiles
.filter(file => file.filename.startsWith('tests/'))
.reduce((sum, file) => sum + (file.deletions || 0), 0);
const nonTestChanges = (totalAdditions - testAdditions) - (totalDeletions - testDeletions);
// Don't add too-big if mega-pr label is already present
if (nonTestChanges > TOO_BIG_THRESHOLD && !isMegaPR) {
labels.add('too-big');
}
return labels;
}
// Strategy: Dashboard changes
async function detectDashboardChanges(changedFiles) {
const labels = new Set();
const dashboardFiles = changedFiles.filter(file =>
file.startsWith('esphome/dashboard/') ||
file.startsWith('esphome/components/dashboard_import/')
);
if (dashboardFiles.length > 0) {
labels.add('dashboard');
}
return labels;
}
// Strategy: GitHub Actions changes
async function detectGitHubActionsChanges(changedFiles) {
const labels = new Set();
const githubActionsFiles = changedFiles.filter(file =>
file.startsWith('.github/workflows/')
);
if (githubActionsFiles.length > 0) {
labels.add('github-actions');
}
return labels;
}
// Strategy: Code owner detection
async function detectCodeOwner(github, context, changedFiles) {
const labels = new Set();
const { owner, repo } = context.repo;
try {
const { data: codeownersFile } = await github.rest.repos.getContent({
owner,
repo,
path: 'CODEOWNERS',
});
const codeownersContent = Buffer.from(codeownersFile.content, 'base64').toString('utf8');
const prAuthor = context.payload.pull_request.user.login;
const codeownersLines = codeownersContent.split('\n')
.map(line => line.trim())
.filter(line => line && !line.startsWith('#'));
const codeownersRegexes = codeownersLines.map(line => {
const parts = line.split(/\s+/);
const pattern = parts[0];
const owners = parts.slice(1);
let regex;
if (pattern.endsWith('*')) {
const dir = pattern.slice(0, -1);
regex = new RegExp(`^${dir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`);
} else if (pattern.includes('*')) {
// First escape all regex special chars except *, then replace * with .*
const regexPattern = pattern
.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
.replace(/\*/g, '.*');
regex = new RegExp(`^${regexPattern}$`);
} else {
regex = new RegExp(`^${pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`);
}
return { regex, owners };
});
for (const file of changedFiles) {
for (const { regex, owners } of codeownersRegexes) {
if (regex.test(file) && owners.some(owner => owner === `@${prAuthor}`)) {
labels.add('by-code-owner');
return labels;
}
}
}
} catch (error) {
console.log('Failed to read or parse CODEOWNERS file:', error.message);
}
return labels;
}
// Strategy: Test detection
async function detectTests(changedFiles) {
const labels = new Set();
const testFiles = changedFiles.filter(file => file.startsWith('tests/'));
if (testFiles.length > 0) {
labels.add('has-tests');
}
return labels;
}
// Strategy: PR Template Checkbox detection
async function detectPRTemplateCheckboxes(context) {
const labels = new Set();
const prBody = context.payload.pull_request.body || '';
console.log('Checking PR template checkboxes...');
// Check for checked checkboxes in the "Types of changes" section
const checkboxPatterns = [
{ pattern: /- \[x\] Bugfix \(non-breaking change which fixes an issue\)/i, label: 'bugfix' },
{ pattern: /- \[x\] New feature \(non-breaking change which adds functionality\)/i, label: 'new-feature' },
{ pattern: /- \[x\] Breaking change \(fix or feature that would cause existing functionality to not work as expected\)/i, label: 'breaking-change' },
{ pattern: /- \[x\] Developer breaking change \(an API change that could break external components\)/i, label: 'developer-breaking-change' },
{ pattern: /- \[x\] Code quality improvements to existing code or addition of tests/i, label: 'code-quality' }
];
for (const { pattern, label } of checkboxPatterns) {
if (pattern.test(prBody)) {
console.log(`Found checked checkbox for: ${label}`);
labels.add(label);
}
}
return labels;
}
// Strategy: Deprecated component detection
async function detectDeprecatedComponents(github, context, changedFiles) {
const labels = new Set();
const deprecatedInfo = [];
const { owner, repo } = context.repo;
// Compile regex once for better performance
const componentFileRegex = /^esphome\/components\/([^\/]+)\//;
// Get files that are modified or added in components directory
const componentFiles = changedFiles.filter(file => componentFileRegex.test(file));
if (componentFiles.length === 0) {
return { labels, deprecatedInfo };
}
// Extract unique component names using the same regex
const components = new Set();
for (const file of componentFiles) {
const match = file.match(componentFileRegex);
if (match) {
components.add(match[1]);
}
}
// Get PR head to fetch files from the PR branch
const prNumber = context.payload.pull_request.number;
// Check each component's __init__.py for DEPRECATED_COMPONENT constant
for (const component of components) {
const initFile = `esphome/components/${component}/__init__.py`;
try {
// Fetch file content from PR head using GitHub API
const { data: fileData } = await github.rest.repos.getContent({
owner,
repo,
path: initFile,
ref: `refs/pull/${prNumber}/head`
});
// Decode base64 content
const content = Buffer.from(fileData.content, 'base64').toString('utf8');
// Look for DEPRECATED_COMPONENT = "message" or DEPRECATED_COMPONENT = 'message'
// Support single quotes, double quotes, and triple quotes (for multiline)
const doubleQuoteMatch = content.match(/DEPRECATED_COMPONENT\s*=\s*"""([\s\S]*?)"""/s) ||
content.match(/DEPRECATED_COMPONENT\s*=\s*"((?:[^"\\]|\\.)*)"/);
const singleQuoteMatch = content.match(/DEPRECATED_COMPONENT\s*=\s*'''([\s\S]*?)'''/s) ||
content.match(/DEPRECATED_COMPONENT\s*=\s*'((?:[^'\\]|\\.)*)'/);
const deprecatedMatch = doubleQuoteMatch || singleQuoteMatch;
if (deprecatedMatch) {
labels.add('deprecated-component');
deprecatedInfo.push({
component: component,
message: deprecatedMatch[1].trim()
});
console.log(`Found deprecated component: ${component}`);
}
} catch (error) {
// Only log if it's not a simple "file not found" error (404)
if (error.status !== 404) {
console.log(`Error reading ${initFile}:`, error.message);
}
}
}
return { labels, deprecatedInfo };
}
// Strategy: Requirements detection
async function detectRequirements(allLabels, prFiles, context) {
const labels = new Set();
// Check for missing tests
if ((allLabels.has('new-component') || allLabels.has('new-platform') || allLabels.has('new-feature')) && !allLabels.has('has-tests')) {
labels.add('needs-tests');
}
// Check for missing docs
if (allLabels.has('new-component') || allLabels.has('new-platform') || allLabels.has('new-feature')) {
const prBody = context.payload.pull_request.body || '';
const hasDocsLink = DOCS_PR_PATTERNS.some(pattern => pattern.test(prBody));
if (!hasDocsLink) {
labels.add('needs-docs');
}
}
// Check for missing CODEOWNERS
if (allLabels.has('new-component')) {
const codeownersModified = prFiles.some(file =>
file.filename === 'CODEOWNERS' &&
(file.status === 'modified' || file.status === 'added') &&
(file.additions || 0) > 0
);
if (!codeownersModified) {
labels.add('needs-codeowners');
}
}
return labels;
}
module.exports = {
detectMergeBranch,
detectComponentPlatforms,
detectNewComponents,
detectNewPlatforms,
detectCoreChanges,
detectPRSize,
detectDashboardChanges,
detectGitHubActionsChanges,
detectCodeOwner,
detectTests,
detectPRTemplateCheckboxes,
detectDeprecatedComponents,
detectRequirements
};

187
.github/scripts/auto-label-pr/index.js vendored Normal file
View File

@@ -0,0 +1,187 @@
const { MANAGED_LABELS } = require('./constants');
const {
detectMergeBranch,
detectComponentPlatforms,
detectNewComponents,
detectNewPlatforms,
detectCoreChanges,
detectPRSize,
detectDashboardChanges,
detectGitHubActionsChanges,
detectCodeOwner,
detectTests,
detectPRTemplateCheckboxes,
detectDeprecatedComponents,
detectRequirements
} = require('./detectors');
const { handleReviews } = require('./reviews');
const { applyLabels, removeOldLabels } = require('./labels');
// Fetch API data
async function fetchApiData() {
try {
const response = await fetch('https://data.esphome.io/components.json');
const componentsData = await response.json();
return {
targetPlatforms: componentsData.target_platforms || [],
platformComponents: componentsData.platform_components || []
};
} catch (error) {
console.log('Failed to fetch components data from API:', error.message);
return { targetPlatforms: [], platformComponents: [] };
}
}
module.exports = async ({ github, context }) => {
// Environment variables
const SMALL_PR_THRESHOLD = parseInt(process.env.SMALL_PR_THRESHOLD);
const MAX_LABELS = parseInt(process.env.MAX_LABELS);
const TOO_BIG_THRESHOLD = parseInt(process.env.TOO_BIG_THRESHOLD);
const COMPONENT_LABEL_THRESHOLD = parseInt(process.env.COMPONENT_LABEL_THRESHOLD);
// Global state
const { owner, repo } = context.repo;
const pr_number = context.issue.number;
// Get current labels and PR data
const { data: currentLabelsData } = await github.rest.issues.listLabelsOnIssue({
owner,
repo,
issue_number: pr_number
});
const currentLabels = currentLabelsData.map(label => label.name);
const managedLabels = currentLabels.filter(label =>
label.startsWith('component: ') || MANAGED_LABELS.includes(label)
);
// Check for mega-PR early - if present, skip most automatic labeling
const isMegaPR = currentLabels.includes('mega-pr');
// Get all PR files with automatic pagination
const prFiles = await github.paginate(
github.rest.pulls.listFiles,
{
owner,
repo,
pull_number: pr_number
}
);
// Calculate data from PR files
const changedFiles = prFiles.map(file => file.filename);
const totalAdditions = prFiles.reduce((sum, file) => sum + (file.additions || 0), 0);
const totalDeletions = prFiles.reduce((sum, file) => sum + (file.deletions || 0), 0);
const totalChanges = totalAdditions + totalDeletions;
console.log('Current labels:', currentLabels.join(', '));
console.log('Changed files:', changedFiles.length);
console.log('Total changes:', totalChanges);
if (isMegaPR) {
console.log('Mega-PR detected - applying limited labeling logic');
}
// Fetch API data
const apiData = await fetchApiData();
const baseRef = context.payload.pull_request.base.ref;
// Early exit for release and beta branches only
if (baseRef === 'release' || baseRef === 'beta') {
const branchLabels = await detectMergeBranch(context);
const finalLabels = Array.from(branchLabels);
console.log('Computed labels (merge branch only):', finalLabels.join(', '));
// Apply labels
await applyLabels(github, context, finalLabels);
// Remove old managed labels
await removeOldLabels(github, context, managedLabels, finalLabels);
return;
}
// Run all strategies
const [
branchLabels,
componentLabels,
newComponentLabels,
newPlatformLabels,
coreLabels,
sizeLabels,
dashboardLabels,
actionsLabels,
codeOwnerLabels,
testLabels,
checkboxLabels,
deprecatedResult
] = await Promise.all([
detectMergeBranch(context),
detectComponentPlatforms(changedFiles, apiData),
detectNewComponents(prFiles),
detectNewPlatforms(prFiles, apiData),
detectCoreChanges(changedFiles),
detectPRSize(prFiles, totalAdditions, totalDeletions, totalChanges, isMegaPR, SMALL_PR_THRESHOLD, TOO_BIG_THRESHOLD),
detectDashboardChanges(changedFiles),
detectGitHubActionsChanges(changedFiles),
detectCodeOwner(github, context, changedFiles),
detectTests(changedFiles),
detectPRTemplateCheckboxes(context),
detectDeprecatedComponents(github, context, changedFiles)
]);
// Extract deprecated component info
const deprecatedLabels = deprecatedResult.labels;
const deprecatedInfo = deprecatedResult.deprecatedInfo;
// Combine all labels
const allLabels = new Set([
...branchLabels,
...componentLabels,
...newComponentLabels,
...newPlatformLabels,
...coreLabels,
...sizeLabels,
...dashboardLabels,
...actionsLabels,
...codeOwnerLabels,
...testLabels,
...checkboxLabels,
...deprecatedLabels
]);
// Detect requirements based on all other labels
const requirementLabels = await detectRequirements(allLabels, prFiles, context);
for (const label of requirementLabels) {
allLabels.add(label);
}
let finalLabels = Array.from(allLabels);
// For mega-PRs, exclude component labels if there are too many
if (isMegaPR) {
const componentLabels = finalLabels.filter(label => label.startsWith('component: '));
if (componentLabels.length > COMPONENT_LABEL_THRESHOLD) {
finalLabels = finalLabels.filter(label => !label.startsWith('component: '));
console.log(`Mega-PR detected - excluding ${componentLabels.length} component labels (threshold: ${COMPONENT_LABEL_THRESHOLD})`);
}
}
// Handle too many labels (only for non-mega PRs)
const tooManyLabels = finalLabels.length > MAX_LABELS;
const originalLabelCount = finalLabels.length;
if (tooManyLabels && !isMegaPR && !finalLabels.includes('too-big')) {
finalLabels = ['too-big'];
}
console.log('Computed labels:', finalLabels.join(', '));
// Handle reviews
await handleReviews(github, context, finalLabels, originalLabelCount, deprecatedInfo, prFiles, totalAdditions, totalDeletions, MAX_LABELS, TOO_BIG_THRESHOLD);
// Apply labels
await applyLabels(github, context, finalLabels);
// Remove old managed labels
await removeOldLabels(github, context, managedLabels, finalLabels);
};

41
.github/scripts/auto-label-pr/labels.js vendored Normal file
View File

@@ -0,0 +1,41 @@
// Apply labels to PR
async function applyLabels(github, context, finalLabels) {
const { owner, repo } = context.repo;
const pr_number = context.issue.number;
if (finalLabels.length > 0) {
console.log(`Adding labels: ${finalLabels.join(', ')}`);
await github.rest.issues.addLabels({
owner,
repo,
issue_number: pr_number,
labels: finalLabels
});
}
}
// Remove old managed labels
async function removeOldLabels(github, context, managedLabels, finalLabels) {
const { owner, repo } = context.repo;
const pr_number = context.issue.number;
const labelsToRemove = managedLabels.filter(label => !finalLabels.includes(label));
for (const label of labelsToRemove) {
console.log(`Removing label: ${label}`);
try {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: pr_number,
name: label
});
} catch (error) {
console.log(`Failed to remove label ${label}:`, error.message);
}
}
}
module.exports = {
applyLabels,
removeOldLabels
};

141
.github/scripts/auto-label-pr/reviews.js vendored Normal file
View File

@@ -0,0 +1,141 @@
const {
BOT_COMMENT_MARKER,
CODEOWNERS_MARKER,
TOO_BIG_MARKER,
DEPRECATED_COMPONENT_MARKER
} = require('./constants');
// Generate review messages
function generateReviewMessages(finalLabels, originalLabelCount, deprecatedInfo, prFiles, totalAdditions, totalDeletions, prAuthor, MAX_LABELS, TOO_BIG_THRESHOLD) {
const messages = [];
// Deprecated component message
if (finalLabels.includes('deprecated-component') && deprecatedInfo && deprecatedInfo.length > 0) {
let message = `${DEPRECATED_COMPONENT_MARKER}\n### ⚠️ Deprecated Component\n\n`;
message += `Hey there @${prAuthor},\n`;
message += `This PR modifies one or more deprecated components. Please be aware:\n\n`;
for (const info of deprecatedInfo) {
message += `#### Component: \`${info.component}\`\n`;
message += `${info.message}\n\n`;
}
message += `Consider migrating to the recommended alternative if applicable.`;
messages.push(message);
}
// Too big message
if (finalLabels.includes('too-big')) {
const testAdditions = prFiles
.filter(file => file.filename.startsWith('tests/'))
.reduce((sum, file) => sum + (file.additions || 0), 0);
const testDeletions = prFiles
.filter(file => file.filename.startsWith('tests/'))
.reduce((sum, file) => sum + (file.deletions || 0), 0);
const nonTestChanges = (totalAdditions - testAdditions) - (totalDeletions - testDeletions);
const tooManyLabels = originalLabelCount > MAX_LABELS;
const tooManyChanges = nonTestChanges > TOO_BIG_THRESHOLD;
let message = `${TOO_BIG_MARKER}\n### 📦 Pull Request Size\n\n`;
if (tooManyLabels && tooManyChanges) {
message += `This PR is too large with ${nonTestChanges} line changes (excluding tests) and affects ${originalLabelCount} different components/areas.`;
} else if (tooManyLabels) {
message += `This PR affects ${originalLabelCount} different components/areas.`;
} else {
message += `This PR is too large with ${nonTestChanges} line changes (excluding tests).`;
}
message += ` Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.\n\n`;
message += `For guidance on breaking down large PRs, see: https://developers.esphome.io/contributing/submitting-your-work/#how-to-approach-large-submissions`;
messages.push(message);
}
// CODEOWNERS message
if (finalLabels.includes('needs-codeowners')) {
const message = `${CODEOWNERS_MARKER}\n### 👥 Code Ownership\n\n` +
`Hey there @${prAuthor},\n` +
`Thanks for submitting this pull request! Can you add yourself as a codeowner for this integration? ` +
`This way we can notify you if a bug report for this integration is reported.\n\n` +
`In \`__init__.py\` of the integration, please add:\n\n` +
`\`\`\`python\nCODEOWNERS = ["@${prAuthor}"]\n\`\`\`\n\n` +
`And run \`script/build_codeowners.py\``;
messages.push(message);
}
return messages;
}
// Handle reviews
async function handleReviews(github, context, finalLabels, originalLabelCount, deprecatedInfo, prFiles, totalAdditions, totalDeletions, MAX_LABELS, TOO_BIG_THRESHOLD) {
const { owner, repo } = context.repo;
const pr_number = context.issue.number;
const prAuthor = context.payload.pull_request.user.login;
const reviewMessages = generateReviewMessages(finalLabels, originalLabelCount, deprecatedInfo, prFiles, totalAdditions, totalDeletions, prAuthor, MAX_LABELS, TOO_BIG_THRESHOLD);
const hasReviewableLabels = finalLabels.some(label =>
['too-big', 'needs-codeowners', 'deprecated-component'].includes(label)
);
const { data: reviews } = await github.rest.pulls.listReviews({
owner,
repo,
pull_number: pr_number
});
const botReviews = reviews.filter(review =>
review.user.type === 'Bot' &&
review.state === 'CHANGES_REQUESTED' &&
review.body && review.body.includes(BOT_COMMENT_MARKER)
);
if (hasReviewableLabels) {
const reviewBody = `${BOT_COMMENT_MARKER}\n\n${reviewMessages.join('\n\n---\n\n')}`;
if (botReviews.length > 0) {
// Update existing review
await github.rest.pulls.updateReview({
owner,
repo,
pull_number: pr_number,
review_id: botReviews[0].id,
body: reviewBody
});
console.log('Updated existing bot review');
} else {
// Create new review
await github.rest.pulls.createReview({
owner,
repo,
pull_number: pr_number,
body: reviewBody,
event: 'REQUEST_CHANGES'
});
console.log('Created new bot review');
}
} else if (botReviews.length > 0) {
// Dismiss existing reviews
for (const review of botReviews) {
try {
await github.rest.pulls.dismissReview({
owner,
repo,
pull_number: pr_number,
review_id: review.id,
message: 'Review dismissed: All requirements have been met'
});
console.log(`Dismissed bot review ${review.id}`);
} catch (error) {
console.log(`Failed to dismiss review ${review.id}:`, error.message);
}
}
}
}
module.exports = {
handleReviews
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components.esp32 import add_idf_component from esphome.components.esp32 import add_idf_component, include_builtin_idf_component
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_BITS_PER_SAMPLE, CONF_NUM_CHANNELS, CONF_SAMPLE_RATE from esphome.const import CONF_BITS_PER_SAMPLE, CONF_NUM_CHANNELS, CONF_SAMPLE_RATE
import esphome.final_validate as fv import esphome.final_validate as fv
@@ -166,6 +166,9 @@ def final_validate_audio_schema(
async def to_code(config): 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( add_idf_component(
name="esphome/esp-audio-libs", name="esphome/esp-audio-libs",
ref="2.0.3", ref="2.0.3",

View File

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

View File

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

View File

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

View File

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

View File

@@ -360,8 +360,7 @@ void Climate::add_on_control_callback(std::function<void(ClimateCall &)> &&callb
static const uint32_t RESTORE_STATE_VERSION = 0x848EA6ADUL; static const uint32_t RESTORE_STATE_VERSION = 0x848EA6ADUL;
optional<ClimateDeviceRestoreState> Climate::restore_state_() { optional<ClimateDeviceRestoreState> Climate::restore_state_() {
this->rtc_ = global_preferences->make_preference<ClimateDeviceRestoreState>(this->get_preference_hash() ^ this->rtc_ = this->make_entity_preference<ClimateDeviceRestoreState>(RESTORE_STATE_VERSION);
RESTORE_STATE_VERSION);
ClimateDeviceRestoreState recovered{}; ClimateDeviceRestoreState recovered{};
if (!this->rtc_.load(&recovered)) if (!this->rtc_.load(&recovered))
return {}; return {};

View File

@@ -187,7 +187,7 @@ void Cover::publish_state(bool save) {
} }
} }
optional<CoverRestoreState> Cover::restore_state_() { optional<CoverRestoreState> Cover::restore_state_() {
this->rtc_ = global_preferences->make_preference<CoverRestoreState>(this->get_preference_hash()); this->rtc_ = this->make_entity_preference<CoverRestoreState>();
CoverRestoreState recovered{}; CoverRestoreState recovered{};
if (!this->rtc_.load(&recovered)) if (!this->rtc_.load(&recovered))
return {}; return {};

View File

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

View File

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

View File

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

View File

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

View File

@@ -15,7 +15,7 @@ from esphome.const import (
CONF_UPDATE_INTERVAL, CONF_UPDATE_INTERVAL,
SCHEDULER_DONT_RUN, SCHEDULER_DONT_RUN,
) )
from esphome.core import CoroPriority, coroutine_with_priority from esphome.core import CORE, CoroPriority, coroutine_with_priority
IS_PLATFORM_COMPONENT = True IS_PLATFORM_COMPONENT = True
@@ -222,3 +222,8 @@ async def display_is_displaying_page_to_code(config, condition_id, template_arg,
async def to_code(config): async def to_code(config):
cg.add_global(display_ns.using) cg.add_global(display_ns.using)
cg.add_define("USE_DISPLAY") cg.add_define("USE_DISPLAY")
if CORE.is_esp32:
# Re-enable ESP-IDF's LCD driver (excluded by default to save compile time)
from esphome.components.esp32 import include_builtin_idf_component
include_builtin_idf_component("esp_lcd")

View File

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

View File

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

View File

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

View File

@@ -53,8 +53,10 @@ from .const import ( # noqa
KEY_BOARD, KEY_BOARD,
KEY_COMPONENTS, KEY_COMPONENTS,
KEY_ESP32, KEY_ESP32,
KEY_EXCLUDE_COMPONENTS,
KEY_EXTRA_BUILD_FILES, KEY_EXTRA_BUILD_FILES,
KEY_FLASH_SIZE, KEY_FLASH_SIZE,
KEY_FULL_CERT_BUNDLE,
KEY_PATH, KEY_PATH,
KEY_REF, KEY_REF,
KEY_REPO, KEY_REPO,
@@ -85,6 +87,7 @@ IS_TARGET_PLATFORM = True
CONF_ASSERTION_LEVEL = "assertion_level" CONF_ASSERTION_LEVEL = "assertion_level"
CONF_COMPILER_OPTIMIZATION = "compiler_optimization" CONF_COMPILER_OPTIMIZATION = "compiler_optimization"
CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES = "enable_idf_experimental_features" CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES = "enable_idf_experimental_features"
CONF_INCLUDE_BUILTIN_IDF_COMPONENTS = "include_builtin_idf_components"
CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert" CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert"
CONF_ENABLE_OTA_ROLLBACK = "enable_ota_rollback" CONF_ENABLE_OTA_ROLLBACK = "enable_ota_rollback"
CONF_EXECUTE_FROM_PSRAM = "execute_from_psram" CONF_EXECUTE_FROM_PSRAM = "execute_from_psram"
@@ -113,6 +116,36 @@ COMPILER_OPTIMIZATIONS = {
"SIZE": "CONFIG_COMPILER_OPTIMIZATION_SIZE", "SIZE": "CONFIG_COMPILER_OPTIMIZATION_SIZE",
} }
# ESP-IDF components excluded by default to reduce compile time.
# Components can be re-enabled by calling include_builtin_idf_component() in to_code().
#
# Cannot be excluded (dependencies of required components):
# - "console": espressif/mdns unconditionally depends on it
# - "sdmmc": driver -> esp_driver_sdmmc -> sdmmc dependency chain
DEFAULT_EXCLUDED_IDF_COMPONENTS = (
"cmock", # Unit testing mock framework - ESPHome doesn't use IDF's testing
"esp_adc", # ADC driver - only needed by adc component
"esp_driver_i2s", # I2S driver - only needed by i2s_audio component
"esp_driver_rmt", # RMT driver - only needed by remote_transmitter/receiver, neopixelbus
"esp_driver_touch_sens", # Touch sensor driver - only needed by esp32_touch
"esp_eth", # Ethernet driver - only needed by ethernet component
"esp_hid", # HID host/device support - ESPHome doesn't implement HID functionality
"esp_http_client", # HTTP client - only needed by http_request component
"esp_https_ota", # ESP-IDF HTTPS OTA - ESPHome has its own OTA implementation
"esp_https_server", # HTTPS server - ESPHome has its own web server
"esp_lcd", # LCD controller drivers - only needed by display component
"esp_local_ctrl", # Local control over HTTPS/BLE - ESPHome has native API
"espcoredump", # Core dump support - ESPHome has its own debug component
"fatfs", # FAT filesystem - ESPHome doesn't use filesystem storage
"mqtt", # ESP-IDF MQTT library - ESPHome has its own MQTT implementation
"perfmon", # Xtensa performance monitor - ESPHome has its own debug component
"protocomm", # Protocol communication for provisioning - unused by ESPHome
"spiffs", # SPIFFS filesystem - ESPHome doesn't use filesystem storage (IDF only)
"unity", # Unit testing framework - ESPHome doesn't use IDF's testing
"wear_levelling", # Flash wear levelling for fatfs - unused since fatfs unused
"wifi_provisioning", # WiFi provisioning - ESPHome uses its own improv implementation
)
# ESP32 (original) chip revision options # ESP32 (original) chip revision options
# Setting minimum revision to 3.0 or higher: # Setting minimum revision to 3.0 or higher:
# - Reduces flash size by excluding workaround code for older chip bugs # - Reduces flash size by excluding workaround code for older chip bugs
@@ -202,6 +235,9 @@ def set_core_data(config):
) )
CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] = {} CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] = {}
CORE.data[KEY_ESP32][KEY_COMPONENTS] = {} CORE.data[KEY_ESP32][KEY_COMPONENTS] = {}
# Initialize with default exclusions - components can call include_builtin_idf_component()
# to re-enable any they need
CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS] = set(DEFAULT_EXCLUDED_IDF_COMPONENTS)
CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse( CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse(
config[CONF_FRAMEWORK][CONF_VERSION] config[CONF_FRAMEWORK][CONF_VERSION]
) )
@@ -327,6 +363,28 @@ def add_idf_component(
} }
def exclude_builtin_idf_component(name: str) -> None:
"""Exclude an ESP-IDF component from the build.
This reduces compile time by skipping components that are not needed.
The component will be passed to ESP-IDF's EXCLUDE_COMPONENTS cmake variable.
Note: Components that are dependencies of other required components
cannot be excluded - ESP-IDF will still build them.
"""
CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS].add(name)
def include_builtin_idf_component(name: str) -> None:
"""Remove an ESP-IDF component from the exclusion list.
Call this from components that need an ESP-IDF component that is
excluded by default in DEFAULT_EXCLUDED_IDF_COMPONENTS. This ensures the
component will be built when needed.
"""
CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS].discard(name)
def add_extra_script(stage: str, filename: str, path: Path): def add_extra_script(stage: str, filename: str, path: Path):
"""Add an extra script to the project.""" """Add an extra script to the project."""
key = f"{stage}:{filename}" key = f"{stage}:{filename}"
@@ -670,11 +728,26 @@ CONF_FREERTOS_IN_IRAM = "freertos_in_iram"
CONF_RINGBUF_IN_IRAM = "ringbuf_in_iram" CONF_RINGBUF_IN_IRAM = "ringbuf_in_iram"
CONF_HEAP_IN_IRAM = "heap_in_iram" CONF_HEAP_IN_IRAM = "heap_in_iram"
CONF_LOOP_TASK_STACK_SIZE = "loop_task_stack_size" CONF_LOOP_TASK_STACK_SIZE = "loop_task_stack_size"
CONF_USE_FULL_CERTIFICATE_BUNDLE = "use_full_certificate_bundle"
CONF_DISABLE_DEBUG_STUBS = "disable_debug_stubs"
CONF_DISABLE_OCD_AWARE = "disable_ocd_aware"
CONF_DISABLE_USB_SERIAL_JTAG_SECONDARY = "disable_usb_serial_jtag_secondary"
CONF_DISABLE_DEV_NULL_VFS = "disable_dev_null_vfs"
CONF_DISABLE_MBEDTLS_PEER_CERT = "disable_mbedtls_peer_cert"
CONF_DISABLE_MBEDTLS_PKCS7 = "disable_mbedtls_pkcs7"
CONF_DISABLE_REGI2C_IN_IRAM = "disable_regi2c_in_iram"
CONF_DISABLE_FATFS = "disable_fatfs"
# VFS requirement tracking # VFS requirement tracking
# Components that need VFS features can call require_vfs_select() or require_vfs_dir() # Components that need VFS features can call require_vfs_select() or require_vfs_dir()
KEY_VFS_SELECT_REQUIRED = "vfs_select_required" KEY_VFS_SELECT_REQUIRED = "vfs_select_required"
KEY_VFS_DIR_REQUIRED = "vfs_dir_required" KEY_VFS_DIR_REQUIRED = "vfs_dir_required"
# Feature requirement tracking - components can call require_* functions to re-enable
# 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: def require_vfs_select() -> None:
@@ -695,6 +768,55 @@ def require_vfs_dir() -> None:
CORE.data[KEY_VFS_DIR_REQUIRED] = True CORE.data[KEY_VFS_DIR_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: def _parse_idf_component(value: str) -> ConfigType:
"""Parse IDF component shorthand syntax like 'owner/component^version'""" """Parse IDF component shorthand syntax like 'owner/component^version'"""
# Match operator followed by version-like string (digit or *) # Match operator followed by version-like string (digit or *)
@@ -776,6 +898,22 @@ FRAMEWORK_SCHEMA = cv.Schema(
min=8192, max=32768 min=8192, max=32768
), ),
cv.Optional(CONF_ENABLE_OTA_ROLLBACK, default=True): cv.boolean, cv.Optional(CONF_ENABLE_OTA_ROLLBACK, default=True): cv.boolean,
cv.Optional(
CONF_USE_FULL_CERTIFICATE_BUNDLE, default=False
): cv.boolean,
cv.Optional(
CONF_INCLUDE_BUILTIN_IDF_COMPONENTS, default=[]
): cv.ensure_list(cv.string_strict),
cv.Optional(CONF_DISABLE_DEBUG_STUBS, default=True): cv.boolean,
cv.Optional(CONF_DISABLE_OCD_AWARE, default=True): cv.boolean,
cv.Optional(
CONF_DISABLE_USB_SERIAL_JTAG_SECONDARY, default=True
): cv.boolean,
cv.Optional(CONF_DISABLE_DEV_NULL_VFS, default=True): cv.boolean,
cv.Optional(CONF_DISABLE_MBEDTLS_PEER_CERT, default=True): cv.boolean,
cv.Optional(CONF_DISABLE_MBEDTLS_PKCS7, default=True): cv.boolean,
cv.Optional(CONF_DISABLE_REGI2C_IN_IRAM, default=True): cv.boolean,
cv.Optional(CONF_DISABLE_FATFS, default=True): cv.boolean,
} }
), ),
cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list( cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list(
@@ -965,6 +1103,19 @@ def _configure_lwip_max_sockets(conf: dict) -> None:
add_idf_sdkconfig_option("CONFIG_LWIP_MAX_SOCKETS", max_sockets) add_idf_sdkconfig_option("CONFIG_LWIP_MAX_SOCKETS", max_sockets)
@coroutine_with_priority(CoroPriority.FINAL)
async def _write_exclude_components() -> None:
"""Write EXCLUDE_COMPONENTS cmake arg after all components have registered exclusions."""
if KEY_ESP32 not in CORE.data:
return
excluded = CORE.data[KEY_ESP32].get(KEY_EXCLUDE_COMPONENTS)
if excluded:
exclude_list = ";".join(sorted(excluded))
cg.add_platformio_option(
"board_build.cmake_extra_args", f"-DEXCLUDE_COMPONENTS={exclude_list}"
)
@coroutine_with_priority(CoroPriority.FINAL) @coroutine_with_priority(CoroPriority.FINAL)
async def _add_yaml_idf_components(components: list[ConfigType]): async def _add_yaml_idf_components(components: list[ConfigType]):
"""Add IDF components from YAML config with final priority to override code-added components.""" """Add IDF components from YAML config with final priority to override code-added components."""
@@ -1048,6 +1199,19 @@ async def to_code(config):
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF") cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF")
if use_platformio: if use_platformio:
cg.add_platformio_option("framework", "espidf") cg.add_platformio_option("framework", "espidf")
# Wrap std::__throw_* functions to abort immediately, eliminating ~3KB of
# exception class overhead. See throw_stubs.cpp for implementation.
# ESP-IDF already compiles with -fno-exceptions, so this code was dead anyway.
for mangled in [
"_ZSt20__throw_length_errorPKc",
"_ZSt19__throw_logic_errorPKc",
"_ZSt20__throw_out_of_rangePKc",
"_ZSt24__throw_out_of_range_fmtPKcz",
"_ZSt17__throw_bad_allocv",
"_ZSt25__throw_bad_function_callv",
]:
cg.add_build_flag(f"-Wl,--wrap={mangled}")
else: else:
cg.add_build_flag("-DUSE_ARDUINO") cg.add_build_flag("-DUSE_ARDUINO")
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO") cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO")
@@ -1080,6 +1244,18 @@ async def to_code(config):
cg.add_build_flag("-Wno-nonnull-compare") cg.add_build_flag("-Wno-nonnull-compare")
# Use CMN (common CAs) bundle by default to save ~51KB flash
# CMN covers CAs with >1% market share (~99% of websites)
# Components needing uncommon CAs can call require_full_certificate_bundle()
use_full_bundle = conf[CONF_ADVANCED].get(
CONF_USE_FULL_CERTIFICATE_BUNDLE, False
) or CORE.data[KEY_ESP32].get(KEY_FULL_CERT_BUNDLE, False)
add_idf_sdkconfig_option(
"CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL", use_full_bundle
)
if not use_full_bundle:
add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_CMN", True)
add_idf_sdkconfig_option(f"CONFIG_IDF_TARGET_{variant}", True) add_idf_sdkconfig_option(f"CONFIG_IDF_TARGET_{variant}", True)
add_idf_sdkconfig_option( add_idf_sdkconfig_option(
f"CONFIG_ESPTOOLPY_FLASHSIZE_{config[CONF_FLASH_SIZE]}", True f"CONFIG_ESPTOOLPY_FLASHSIZE_{config[CONF_FLASH_SIZE]}", True
@@ -1153,6 +1329,11 @@ async def to_code(config):
# Apply LWIP optimization settings # Apply LWIP optimization settings
advanced = conf[CONF_ADVANCED] advanced = conf[CONF_ADVANCED]
# Re-include any IDF components the user explicitly requested
for component_name in advanced.get(CONF_INCLUDE_BUILTIN_IDF_COMPONENTS, []):
include_builtin_idf_component(component_name)
# DHCP server: only disable if explicitly set to false # DHCP server: only disable if explicitly set to false
# WiFi component handles its own optimization when AP mode is not used # WiFi component handles its own optimization when AP mode is not used
# When using Arduino with Ethernet, DHCP server functions must be available # When using Arduino with Ethernet, DHCP server functions must be available
@@ -1274,6 +1455,61 @@ async def to_code(config):
add_idf_sdkconfig_option(f"CONFIG_LOG_DEFAULT_LEVEL_{conf[CONF_LOG_LEVEL]}", True) add_idf_sdkconfig_option(f"CONFIG_LOG_DEFAULT_LEVEL_{conf[CONF_LOG_LEVEL]}", True)
# Disable OpenOCD debug stubs to save code size
# These are used for on-chip debugging with OpenOCD/JTAG, rarely needed for ESPHome
if advanced[CONF_DISABLE_DEBUG_STUBS]:
add_idf_sdkconfig_option("CONFIG_ESP_DEBUG_STUBS_ENABLE", False)
# Disable OCD-aware exception handlers
# When enabled, the panic handler detects JTAG debugger and halts instead of resetting
# Most ESPHome users don't use JTAG debugging
if advanced[CONF_DISABLE_OCD_AWARE]:
add_idf_sdkconfig_option("CONFIG_ESP_DEBUG_OCDAWARE", False)
# Disable USB Serial/JTAG secondary console
# Components like logger can call require_usb_serial_jtag_secondary() to re-enable
if CORE.data[KEY_ESP32].get(KEY_USB_SERIAL_JTAG_SECONDARY_REQUIRED, False):
add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG", True)
elif advanced[CONF_DISABLE_USB_SERIAL_JTAG_SECONDARY]:
add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_SECONDARY_NONE", True)
# Disable /dev/null VFS initialization
# ESPHome doesn't typically need /dev/null
if advanced[CONF_DISABLE_DEV_NULL_VFS]:
add_idf_sdkconfig_option("CONFIG_VFS_INITIALIZE_DEV_NULL", False)
# Disable keeping peer certificate after TLS handshake
# Saves ~4KB heap per connection, but prevents certificate inspection after handshake
# Components that need it can call require_mbedtls_peer_cert()
if CORE.data[KEY_ESP32].get(KEY_MBEDTLS_PEER_CERT_REQUIRED, False):
add_idf_sdkconfig_option("CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE", True)
elif advanced[CONF_DISABLE_MBEDTLS_PEER_CERT]:
add_idf_sdkconfig_option("CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE", False)
# Disable PKCS#7 support in mbedTLS
# Only needed for specific certificate validation scenarios
# Components that need it can call require_mbedtls_pkcs7()
if CORE.data[KEY_ESP32].get(KEY_MBEDTLS_PKCS7_REQUIRED, False):
# Component called require_mbedtls_pkcs7() - enable regardless of user setting
add_idf_sdkconfig_option("CONFIG_MBEDTLS_PKCS7_C", True)
elif advanced[CONF_DISABLE_MBEDTLS_PKCS7]:
add_idf_sdkconfig_option("CONFIG_MBEDTLS_PKCS7_C", False)
# Disable regi2c control functions in IRAM
# Only needed if using analog peripherals (ADC, DAC, etc.) from ISRs while cache is disabled
if advanced[CONF_DISABLE_REGI2C_IN_IRAM]:
add_idf_sdkconfig_option("CONFIG_ESP_REGI2C_CTRL_FUNC_IN_IRAM", False)
# Disable FATFS support
# Components that need FATFS (SD card, etc.) can call require_fatfs()
if CORE.data[KEY_ESP32].get(KEY_FATFS_REQUIRED, False):
# Component called require_fatfs() - enable regardless of user setting
add_idf_sdkconfig_option("CONFIG_FATFS_LFN_NONE", False)
add_idf_sdkconfig_option("CONFIG_FATFS_VOLUME_COUNT", 2)
elif advanced[CONF_DISABLE_FATFS]:
add_idf_sdkconfig_option("CONFIG_FATFS_LFN_NONE", True)
add_idf_sdkconfig_option("CONFIG_FATFS_VOLUME_COUNT", 0)
for name, value in conf[CONF_SDKCONFIG_OPTIONS].items(): for name, value in conf[CONF_SDKCONFIG_OPTIONS].items():
add_idf_sdkconfig_option(name, RawSdkconfigValue(value)) add_idf_sdkconfig_option(name, RawSdkconfigValue(value))
@@ -1282,6 +1518,11 @@ async def to_code(config):
if conf[CONF_COMPONENTS]: if conf[CONF_COMPONENTS]:
CORE.add_job(_add_yaml_idf_components, conf[CONF_COMPONENTS]) CORE.add_job(_add_yaml_idf_components, conf[CONF_COMPONENTS])
# Write EXCLUDE_COMPONENTS at FINAL priority after all components have had
# a chance to call include_builtin_idf_component() to re-enable components they need.
# Default exclusions are added in set_core_data() during config validation.
CORE.add_job(_write_exclude_components)
APP_PARTITION_SIZES = { APP_PARTITION_SIZES = {
"2MB": 0x0C0000, # 768 KB "2MB": 0x0C0000, # 768 KB

View File

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

View File

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

View File

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

View File

@@ -5,6 +5,7 @@ from esphome import pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import esp32, light from esphome.components import esp32, light
from esphome.components.const import CONF_USE_PSRAM from esphome.components.const import CONF_USE_PSRAM
from esphome.components.esp32 import include_builtin_idf_component
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_CHIPSET, CONF_CHIPSET,
@@ -129,6 +130,9 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config): 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]) var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
await light.register_light(var, config) await light.register_light(var, config)
await cg.register_component(var, config) await cg.register_component(var, config)

View File

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

View File

@@ -14,6 +14,7 @@ from esphome.components.esp32 import (
add_idf_component, add_idf_component,
add_idf_sdkconfig_option, add_idf_sdkconfig_option,
get_esp32_variant, get_esp32_variant,
include_builtin_idf_component,
) )
from esphome.components.network import ip_address_literal from esphome.components.network import ip_address_literal
from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface
@@ -419,6 +420,9 @@ async def to_code(config):
# Also disable WiFi/BT coexistence since WiFi is disabled # Also disable WiFi/BT coexistence since WiFi is disabled
add_idf_sdkconfig_option("CONFIG_SW_COEXIST_ENABLE", False) 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": if config[CONF_TYPE] == "LAN8670":
# Add LAN867x 10BASE-T1S PHY support component # Add LAN867x 10BASE-T1S PHY support component
add_idf_component(name="espressif/lan867x", ref="2.0.0") add_idf_component(name="espressif/lan867x", ref="2.0.0")

View File

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

View File

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

View File

@@ -227,8 +227,7 @@ void Fan::publish_state() {
constexpr uint32_t RESTORE_STATE_VERSION = 0x71700ABA; constexpr uint32_t RESTORE_STATE_VERSION = 0x71700ABA;
optional<FanRestoreState> Fan::restore_state_() { optional<FanRestoreState> Fan::restore_state_() {
FanRestoreState recovered{}; FanRestoreState recovered{};
this->rtc_ = this->rtc_ = this->make_entity_preference<FanRestoreState>(RESTORE_STATE_VERSION);
global_preferences->make_preference<FanRestoreState>(this->get_preference_hash() ^ RESTORE_STATE_VERSION);
bool restored = this->rtc_.load(&recovered); bool restored = this->rtc_.load(&recovered);
switch (this->restore_mode_) { switch (this->restore_mode_) {

View File

@@ -9,30 +9,56 @@ from esphome.const import (
CONF_VALUE, CONF_VALUE,
) )
from esphome.core import CoroPriority, coroutine_with_priority from esphome.core import CoroPriority, coroutine_with_priority
from esphome.types import ConfigType
CODEOWNERS = ["@esphome/core"] CODEOWNERS = ["@esphome/core"]
globals_ns = cg.esphome_ns.namespace("globals") globals_ns = cg.esphome_ns.namespace("globals")
GlobalsComponent = globals_ns.class_("GlobalsComponent", cg.Component) GlobalsComponent = globals_ns.class_("GlobalsComponent", cg.Component)
RestoringGlobalsComponent = globals_ns.class_("RestoringGlobalsComponent", cg.Component) RestoringGlobalsComponent = globals_ns.class_(
"RestoringGlobalsComponent", cg.PollingComponent
)
RestoringGlobalStringComponent = globals_ns.class_( RestoringGlobalStringComponent = globals_ns.class_(
"RestoringGlobalStringComponent", cg.Component "RestoringGlobalStringComponent", cg.PollingComponent
) )
GlobalVarSetAction = globals_ns.class_("GlobalVarSetAction", automation.Action) GlobalVarSetAction = globals_ns.class_("GlobalVarSetAction", automation.Action)
CONF_MAX_RESTORE_DATA_LENGTH = "max_restore_data_length" 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),
}
MULTI_CONF = True # Non-restoring globals: regular Component (no polling needed)
CONFIG_SCHEMA = cv.Schema( _NON_RESTORING_SCHEMA = cv.Schema(
{ {
cv.Required(CONF_ID): cv.declare_id(GlobalsComponent), **_BASE_SCHEMA,
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_RESTORE_VALUE, default=False): cv.boolean,
cv.Optional(CONF_MAX_RESTORE_DATA_LENGTH): cv.int_range(0, 254),
} }
).extend(cv.COMPONENT_SCHEMA) ).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
# Run with low priority so that namespaces are registered first # Run with low priority so that namespaces are registered first
@coroutine_with_priority(CoroPriority.LATE) @coroutine_with_priority(CoroPriority.LATE)

View File

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

View File

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

View File

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

View File

@@ -155,6 +155,9 @@ async def to_code(config):
cg.add(var.set_watchdog_timeout(timeout_ms)) cg.add(var.set_watchdog_timeout(timeout_ms))
if CORE.is_esp32: 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_rx(config[CONF_BUFFER_SIZE_RX]))
cg.add(var.set_buffer_size_tx(config[CONF_BUFFER_SIZE_TX])) cg.add(var.set_buffer_size_tx(config[CONF_BUFFER_SIZE_TX]))
cg.add(var.set_verify_ssl(config[CONF_VERIFY_SSL])) cg.add(var.set_verify_ssl(config[CONF_VERIFY_SSL]))
@@ -165,6 +168,16 @@ async def to_code(config):
ca_cert_content = f.read() ca_cert_content = f.read()
cg.add(var.set_ca_certificate(ca_cert_content)) cg.add(var.set_ca_certificate(ca_cert_content))
else: 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( esp32.add_idf_sdkconfig_option(
"CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True "CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True
) )

View File

@@ -131,6 +131,10 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur
} }
} }
// HTTPClient::getSize() returns -1 for chunked transfer encoding (no Content-Length).
// When cast to size_t, -1 becomes SIZE_MAX (4294967295 on 32-bit).
// The read() method handles this: bytes_read_ can never reach SIZE_MAX, so the
// early return check (bytes_read_ >= content_length) will never trigger.
int content_length = container->client_.getSize(); int content_length = container->client_.getSize();
ESP_LOGD(TAG, "Content-Length: %d", content_length); ESP_LOGD(TAG, "Content-Length: %d", content_length);
container->content_length = (size_t) content_length; container->content_length = (size_t) content_length;
@@ -167,17 +171,23 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
} }
int available_data = stream_ptr->available(); int available_data = stream_ptr->available();
int bufsize = std::min(max_len, std::min(this->content_length - this->bytes_read_, (size_t) available_data)); // For chunked transfer encoding, HTTPClient::getSize() returns -1, which becomes SIZE_MAX when
// cast to size_t. SIZE_MAX - bytes_read_ is still huge, so it won't limit the read.
size_t remaining = (this->content_length > 0) ? (this->content_length - this->bytes_read_) : max_len;
int bufsize = std::min(max_len, std::min(remaining, (size_t) available_data));
if (bufsize == 0) { if (bufsize == 0) {
this->duration_ms += (millis() - start); this->duration_ms += (millis() - start);
// Check if we've read all expected content // Check if we've read all expected content (only valid when content_length is known and not SIZE_MAX)
if (this->bytes_read_ >= this->content_length) { // For chunked encoding (content_length == SIZE_MAX), we can't use this check
if (this->content_length > 0 && this->bytes_read_ >= this->content_length) {
return 0; // All content read successfully return 0; // All content read successfully
} }
// No data available - check if connection is still open // 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()) { if (!stream_ptr->connected()) {
return HTTP_ERROR_CONNECTION_CLOSED; // Connection closed prematurely return HTTP_ERROR_CONNECTION_CLOSED; // Connection closed or EOF for chunked
} }
return 0; // No data yet, caller should retry return 0; // No data yet, caller should retry
} }

View File

@@ -157,6 +157,8 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
} }
container->feed_wdt(); 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->content_length = esp_http_client_fetch_headers(client);
container->feed_wdt(); container->feed_wdt();
container->status_code = esp_http_client_get_status_code(client); container->status_code = esp_http_client_get_status_code(client);
@@ -225,14 +227,22 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
// //
// We normalize to HttpContainer::read() contract: // We normalize to HttpContainer::read() contract:
// > 0: bytes read // > 0: bytes read
// 0: no data yet / all content read (caller should check bytes_read vs content_length) // 0: all content read (only returned when content_length is known and fully read)
// < 0: error/connection closed // < 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) { int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
const uint32_t start = millis(); const uint32_t start = millis();
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout()); watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
// Check if we've already read all expected content // Check if we've already read all expected content
if (this->bytes_read_ >= this->content_length) { // Skip this check when content_length is 0 (chunked transfer encoding or unknown length)
// For chunked responses, esp_http_client_read() will return 0 when all data is received
if (this->content_length > 0 && this->bytes_read_ >= this->content_length) {
return 0; // All content read successfully return 0; // All content read successfully
} }
@@ -247,7 +257,13 @@ int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
return read_len_or_error; return read_len_or_error;
} }
// Connection closed by server before all content received // esp_http_client_read() returns 0 in two cases:
// 1. Known content_length: connection closed before all data received (error)
// 2. Chunked encoding (content_length == 0): end of stream reached (EOF)
// For case 1, returning HTTP_ERROR_CONNECTION_CLOSED is correct.
// For case 2, 0 indicates that all chunked data has already been delivered
// in previous successful read() calls, so treating this as a closed
// connection does not cause any loss of response data.
if (read_len_or_error == 0) { if (read_len_or_error == 0) {
return HTTP_ERROR_CONNECTION_CLOSED; return HTTP_ERROR_CONNECTION_CLOSED;
} }

View File

@@ -2,6 +2,25 @@
from . import BoardConfig 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 # Huidu HD-WF2
BoardConfig( BoardConfig(
"huidu-hd-wf2", "huidu-hd-wf2",

View File

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

View File

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

View File

@@ -10,7 +10,7 @@ static const char *const TAG = "integration";
void IntegrationSensor::setup() { void IntegrationSensor::setup() {
if (this->restore_) { if (this->restore_) {
this->pref_ = global_preferences->make_preference<float>(this->get_preference_hash()); this->pref_ = this->make_entity_preference<float>();
float preference_value = 0; float preference_value = 0;
this->pref_.load(&preference_value); this->pref_.load(&preference_value);
this->result_ = 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() { void LD2450Component::setup() {
#ifdef USE_NUMBER #ifdef USE_NUMBER
if (this->presence_timeout_number_ != nullptr) { if (this->presence_timeout_number_ != nullptr) {
this->pref_ = global_preferences->make_preference<float>(this->presence_timeout_number_->get_preference_hash()); this->pref_ = this->presence_timeout_number_->make_entity_preference<float>();
this->set_presence_timeout(); this->set_presence_timeout();
} }
#endif #endif
@@ -451,7 +451,7 @@ void LD2450Component::handle_periodic_data_() {
int16_t ty = 0; int16_t ty = 0;
int16_t td = 0; int16_t td = 0;
int16_t ts = 0; int16_t ts = 0;
int16_t angle = 0; float angle = 0;
uint8_t index = 0; uint8_t index = 0;
Direction direction{DIRECTION_UNDEFINED}; Direction direction{DIRECTION_UNDEFINED};
bool is_moving = false; bool is_moving = false;

View File

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

View File

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

View File

@@ -14,9 +14,7 @@ class Lock;
#define LOG_LOCK(prefix, type, obj) \ #define LOG_LOCK(prefix, type, obj) \
if ((obj) != nullptr) { \ if ((obj) != nullptr) { \
ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \
if (!(obj)->get_icon_ref().empty()) { \ LOG_ENTITY_ICON(TAG, prefix, *(obj)); \
ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \
} \
if ((obj)->traits.get_assumed_state()) { \ if ((obj)->traits.get_assumed_state()) { \
ESP_LOGCONFIG(TAG, "%s Assumed State: YES", prefix); \ ESP_LOGCONFIG(TAG, "%s Assumed State: YES", prefix); \
} \ } \

View File

@@ -21,7 +21,7 @@ class LVGLNumber : public number::Number, public Component {
void setup() override { void setup() override {
float value = this->value_lambda_(); float value = this->value_lambda_();
if (this->restore_) { if (this->restore_) {
this->pref_ = global_preferences->make_preference<float>(this->get_preference_hash()); this->pref_ = this->make_entity_preference<float>();
if (this->pref_.load(&value)) { if (this->pref_.load(&value)) {
this->control_lambda_(value); this->control_lambda_(value);
} }

View File

@@ -20,7 +20,7 @@ class LVGLSelect : public select::Select, public Component {
this->set_options_(); this->set_options_();
if (this->restore_) { if (this->restore_) {
size_t index; size_t index;
this->pref_ = global_preferences->make_preference<size_t>(this->get_preference_hash()); this->pref_ = this->make_entity_preference<size_t>();
if (this->pref_.load(&index)) if (this->pref_.load(&index))
this->widget_->set_selected_index(index, LV_ANIM_OFF); this->widget_->set_selected_index(index, LV_ANIM_OFF);
} }

View File

@@ -12,6 +12,10 @@ namespace esphome::mdns {
static const char *const TAG = "mdns"; static const char *const TAG = "mdns";
static void register_esp32(MDNSComponent *comp, StaticVector<MDNSService, MDNS_SERVICE_COUNT> &services) { static void register_esp32(MDNSComponent *comp, StaticVector<MDNSService, MDNS_SERVICE_COUNT> &services) {
#ifdef USE_OPENTHREAD
// OpenThread handles service registration via SRP client
// Services are compiled by MDNSComponent::compile_records_() and consumed by OpenThreadSrpComponent
#else
esp_err_t err = mdns_init(); esp_err_t err = mdns_init();
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGW(TAG, "Init failed: %s", esp_err_to_name(err)); ESP_LOGW(TAG, "Init failed: %s", esp_err_to_name(err));
@@ -41,13 +45,16 @@ static void register_esp32(MDNSComponent *comp, StaticVector<MDNSService, MDNS_S
ESP_LOGW(TAG, "Failed to register service %s: %s", MDNS_STR_ARG(service.service_type), esp_err_to_name(err)); ESP_LOGW(TAG, "Failed to register service %s: %s", MDNS_STR_ARG(service.service_type), esp_err_to_name(err));
} }
} }
#endif
} }
void MDNSComponent::setup() { this->setup_buffers_and_register_(register_esp32); } void MDNSComponent::setup() { this->setup_buffers_and_register_(register_esp32); }
void MDNSComponent::on_shutdown() { void MDNSComponent::on_shutdown() {
#ifndef USE_OPENTHREAD
mdns_free(); mdns_free();
delay(40); // Allow the mdns packets announcing service removal to be sent delay(40); // Allow the mdns packets announcing service removal to be sent
#endif
} }
} // namespace esphome::mdns } // namespace esphome::mdns

View File

@@ -4,7 +4,10 @@ from esphome import automation
from esphome.automation import Condition from esphome.automation import Condition
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import logger, socket from esphome.components import logger, socket
from esphome.components.esp32 import add_idf_sdkconfig_option from esphome.components.esp32 import (
add_idf_sdkconfig_option,
include_builtin_idf_component,
)
from esphome.config_helpers import filter_source_files_from_platform from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
@@ -360,6 +363,8 @@ async def to_code(config):
# This enables low-latency MQTT event processing instead of waiting for select() timeout # This enables low-latency MQTT event processing instead of waiting for select() timeout
if CORE.is_esp32: if CORE.is_esp32:
socket.require_wake_loop_threadsafe() socket.require_wake_loop_threadsafe()
# Re-enable ESP-IDF's mqtt component (excluded by default to save compile time)
include_builtin_idf_component("mqtt")
cg.add_define("USE_MQTT") cg.add_define("USE_MQTT")
cg.add_global(mqtt_ns.using) cg.add_global(mqtt_ns.using)

View File

@@ -1,5 +1,6 @@
#include "mqtt_alarm_control_panel.h" #include "mqtt_alarm_control_panel.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include "mqtt_const.h" #include "mqtt_const.h"
@@ -18,21 +19,21 @@ void MQTTAlarmControlPanelComponent::setup() {
this->alarm_control_panel_->add_on_state_callback([this]() { this->publish_state(); }); this->alarm_control_panel_->add_on_state_callback([this]() { this->publish_state(); });
this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) { this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) {
auto call = this->alarm_control_panel_->make_call(); auto call = this->alarm_control_panel_->make_call();
if (strcasecmp(payload.c_str(), "ARM_AWAY") == 0) { if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("ARM_AWAY")) == 0) {
call.arm_away(); call.arm_away();
} else if (strcasecmp(payload.c_str(), "ARM_HOME") == 0) { } else if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("ARM_HOME")) == 0) {
call.arm_home(); call.arm_home();
} else if (strcasecmp(payload.c_str(), "ARM_NIGHT") == 0) { } else if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("ARM_NIGHT")) == 0) {
call.arm_night(); call.arm_night();
} else if (strcasecmp(payload.c_str(), "ARM_VACATION") == 0) { } else if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("ARM_VACATION")) == 0) {
call.arm_vacation(); call.arm_vacation();
} else if (strcasecmp(payload.c_str(), "ARM_CUSTOM_BYPASS") == 0) { } else if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("ARM_CUSTOM_BYPASS")) == 0) {
call.arm_custom_bypass(); call.arm_custom_bypass();
} else if (strcasecmp(payload.c_str(), "DISARM") == 0) { } else if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("DISARM")) == 0) {
call.disarm(); call.disarm();
} else if (strcasecmp(payload.c_str(), "PENDING") == 0) { } else if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("PENDING")) == 0) {
call.pending(); call.pending();
} else if (strcasecmp(payload.c_str(), "TRIGGERED") == 0) { } else if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("TRIGGERED")) == 0) {
call.triggered(); call.triggered();
} else { } else {
ESP_LOGW(TAG, "'%s': Received unknown command payload %s", this->friendly_name_().c_str(), payload.c_str()); ESP_LOGW(TAG, "'%s': Received unknown command payload %s", this->friendly_name_().c_str(), payload.c_str());

View File

@@ -1,5 +1,6 @@
#include "mqtt_lock.h" #include "mqtt_lock.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include "mqtt_const.h" #include "mqtt_const.h"
@@ -16,11 +17,11 @@ MQTTLockComponent::MQTTLockComponent(lock::Lock *a_lock) : lock_(a_lock) {}
void MQTTLockComponent::setup() { void MQTTLockComponent::setup() {
this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) { this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) {
if (strcasecmp(payload.c_str(), "LOCK") == 0) { if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("LOCK")) == 0) {
this->lock_->lock(); this->lock_->lock();
} else if (strcasecmp(payload.c_str(), "UNLOCK") == 0) { } else if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("UNLOCK")) == 0) {
this->lock_->unlock(); this->lock_->unlock();
} else if (strcasecmp(payload.c_str(), "OPEN") == 0) { } else if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("OPEN")) == 0) {
this->lock_->open(); this->lock_->open();
} else { } else {
ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name_().c_str(), payload.c_str()); ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name_().c_str(), payload.c_str());

View File

@@ -1,7 +1,12 @@
from esphome import pins from esphome import pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import light from esphome.components import light
from esphome.components.esp32 import VARIANT_ESP32C3, VARIANT_ESP32S3, get_esp32_variant from esphome.components.esp32 import (
VARIANT_ESP32C3,
VARIANT_ESP32S3,
get_esp32_variant,
include_builtin_idf_component,
)
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_CHANNEL, CONF_CHANNEL,
@@ -205,6 +210,10 @@ async def to_code(config):
has_white = "W" in config[CONF_TYPE] has_white = "W" in config[CONF_TYPE]
method = config[CONF_METHOD] method = config[CONF_METHOD]
# Re-enable ESP-IDF's RMT driver if using RMT method (excluded by default)
if CORE.is_esp32 and method[CONF_TYPE] == METHOD_ESP32_RMT:
include_builtin_idf_component("esp_driver_rmt")
method_template = METHODS[method[CONF_TYPE]].to_code( method_template = METHODS[method[CONF_TYPE]].to_code(
method, config[CONF_VARIANT], config[CONF_INVERT] method, config[CONF_VARIANT], config[CONF_INVERT]
) )

View File

@@ -177,6 +177,8 @@ async def to_code(config):
cg.add_define("USE_NEXTION_TFT_UPLOAD") cg.add_define("USE_NEXTION_TFT_UPLOAD")
cg.add(var.set_tft_url(config[CONF_TFT_URL])) cg.add(var.set_tft_url(config[CONF_TFT_URL]))
if CORE.is_esp32: 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")
esp32.add_idf_sdkconfig_option("CONFIG_ESP_TLS_INSECURE", True) esp32.add_idf_sdkconfig_option("CONFIG_ESP_TLS_INSECURE", True)
esp32.add_idf_sdkconfig_option( esp32.add_idf_sdkconfig_option(
"CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY", True "CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY", True

View File

@@ -150,27 +150,24 @@ void Nextion::dump_config() {
#ifdef USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE #ifdef USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE
ESP_LOGCONFIG(TAG, " Skip handshake: YES"); ESP_LOGCONFIG(TAG, " Skip handshake: YES");
#else // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE #else // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE
ESP_LOGCONFIG(TAG,
#ifdef USE_NEXTION_CONFIG_DUMP_DEVICE_INFO #ifdef USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
ESP_LOGCONFIG(TAG,
" Device Model: %s\n" " Device Model: %s\n"
" FW Version: %s\n" " FW Version: %s\n"
" Serial Number: %s\n" " Serial Number: %s\n"
" Flash Size: %s\n" " Flash Size: %s\n"
" Max queue age: %u ms\n" " Max queue age: %u ms\n"
" Startup override: %u ms\n", " Startup override: %u ms\n",
this->device_model_.c_str(), this->firmware_version_.c_str(), this->serial_number_.c_str(),
this->flash_size_.c_str(), this->max_q_age_ms_, this->startup_override_ms_);
#endif // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO #endif // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
#ifdef USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START #ifdef USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START
" Exit reparse: YES\n" ESP_LOGCONFIG(TAG, " Exit reparse: YES\n");
#endif // USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START #endif // USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START
ESP_LOGCONFIG(TAG,
" Wake On Touch: %s\n" " Wake On Touch: %s\n"
" Touch Timeout: %" PRIu16, " Touch Timeout: %" PRIu16,
#ifdef USE_NEXTION_CONFIG_DUMP_DEVICE_INFO YESNO(this->connection_state_.auto_wake_on_touch_), this->touch_sleep_timeout_);
this->device_model_.c_str(), this->firmware_version_.c_str(), this->serial_number_.c_str(),
this->flash_size_.c_str(), this->max_q_age_ms_,
this->startup_override_ms_
#endif // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
YESNO(this->connection_state_.auto_wake_on_touch_),
this->touch_sleep_timeout_);
#endif // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE #endif // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE
#ifdef USE_NEXTION_MAX_COMMANDS_PER_LOOP #ifdef USE_NEXTION_MAX_COMMANDS_PER_LOOP

View File

@@ -40,7 +40,7 @@ void NfcTagBinarySensor::set_tag_name(const std::string &str) {
this->match_tag_name_ = true; this->match_tag_name_ = true;
} }
void NfcTagBinarySensor::set_uid(const std::vector<uint8_t> &uid) { this->uid_ = uid; } void NfcTagBinarySensor::set_uid(const NfcTagUid &uid) { this->uid_ = uid; }
bool NfcTagBinarySensor::tag_match_ndef_string(const std::shared_ptr<NdefMessage> &msg) { bool NfcTagBinarySensor::tag_match_ndef_string(const std::shared_ptr<NdefMessage> &msg) {
for (const auto &record : msg->get_records()) { for (const auto &record : msg->get_records()) {
@@ -63,7 +63,7 @@ bool NfcTagBinarySensor::tag_match_tag_name(const std::shared_ptr<NdefMessage> &
return false; return false;
} }
bool NfcTagBinarySensor::tag_match_uid(const std::vector<uint8_t> &data) { bool NfcTagBinarySensor::tag_match_uid(const NfcTagUid &data) {
if (data.size() != this->uid_.size()) { if (data.size() != this->uid_.size()) {
return false; return false;
} }

View File

@@ -19,11 +19,11 @@ class NfcTagBinarySensor : public binary_sensor::BinarySensor,
void set_ndef_match_string(const std::string &str); void set_ndef_match_string(const std::string &str);
void set_tag_name(const std::string &str); void set_tag_name(const std::string &str);
void set_uid(const std::vector<uint8_t> &uid); void set_uid(const NfcTagUid &uid);
bool tag_match_ndef_string(const std::shared_ptr<NdefMessage> &msg); bool tag_match_ndef_string(const std::shared_ptr<NdefMessage> &msg);
bool tag_match_tag_name(const std::shared_ptr<NdefMessage> &msg); bool tag_match_tag_name(const std::shared_ptr<NdefMessage> &msg);
bool tag_match_uid(const std::vector<uint8_t> &data); bool tag_match_uid(const NfcTagUid &data);
void tag_off(NfcTag &tag) override; void tag_off(NfcTag &tag) override;
void tag_on(NfcTag &tag) override; void tag_on(NfcTag &tag) override;
@@ -31,7 +31,7 @@ class NfcTagBinarySensor : public binary_sensor::BinarySensor,
protected: protected:
bool match_tag_name_{false}; bool match_tag_name_{false};
std::string match_string_; std::string match_string_;
std::vector<uint8_t> uid_; NfcTagUid uid_;
}; };
} // namespace nfc } // namespace nfc

View File

@@ -8,19 +8,23 @@ namespace nfc {
static const char *const TAG = "nfc"; static const char *const TAG = "nfc";
char *format_uid_to(char *buffer, const std::vector<uint8_t> &uid) { char *format_uid_to(char *buffer, std::span<const uint8_t> uid) {
return format_hex_pretty_to(buffer, FORMAT_UID_BUFFER_SIZE, uid.data(), uid.size(), '-'); return format_hex_pretty_to(buffer, FORMAT_UID_BUFFER_SIZE, uid.data(), uid.size(), '-');
} }
char *format_bytes_to(char *buffer, const std::vector<uint8_t> &bytes) { char *format_bytes_to(char *buffer, std::span<const uint8_t> bytes) {
return format_hex_pretty_to(buffer, FORMAT_BYTES_BUFFER_SIZE, bytes.data(), bytes.size(), ' '); return format_hex_pretty_to(buffer, FORMAT_BYTES_BUFFER_SIZE, bytes.data(), bytes.size(), ' ');
} }
#pragma GCC diagnostic push #pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations" #pragma GCC diagnostic ignored "-Wdeprecated-declarations"
// Deprecated wrappers intentionally use heap-allocating version for backward compatibility // Deprecated wrappers intentionally use heap-allocating version for backward compatibility
std::string format_uid(const std::vector<uint8_t> &uid) { return format_hex_pretty(uid, '-', false); } // NOLINT std::string format_uid(std::span<const uint8_t> uid) {
std::string format_bytes(const std::vector<uint8_t> &bytes) { return format_hex_pretty(bytes, ' ', false); } // NOLINT return format_hex_pretty(uid.data(), uid.size(), '-', false); // NOLINT
}
std::string format_bytes(std::span<const uint8_t> bytes) {
return format_hex_pretty(bytes.data(), bytes.size(), ' ', false); // NOLINT
}
#pragma GCC diagnostic pop #pragma GCC diagnostic pop
uint8_t guess_tag_type(uint8_t uid_length) { uint8_t guess_tag_type(uint8_t uid_length) {

View File

@@ -6,6 +6,7 @@
#include "ndef_record.h" #include "ndef_record.h"
#include "nfc_tag.h" #include "nfc_tag.h"
#include <span>
#include <vector> #include <vector>
namespace esphome { namespace esphome {
@@ -56,19 +57,19 @@ static const uint8_t MAD_KEY[6] = {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5};
/// Max UID size is 10 bytes, formatted as "XX-XX-XX-XX-XX-XX-XX-XX-XX-XX\0" = 30 chars /// Max UID size is 10 bytes, formatted as "XX-XX-XX-XX-XX-XX-XX-XX-XX-XX\0" = 30 chars
static constexpr size_t FORMAT_UID_BUFFER_SIZE = 30; static constexpr size_t FORMAT_UID_BUFFER_SIZE = 30;
/// Format UID to buffer with '-' separator (e.g., "04-11-22-33"). Returns buffer for inline use. /// Format UID to buffer with '-' separator (e.g., "04-11-22-33"). Returns buffer for inline use.
char *format_uid_to(char *buffer, const std::vector<uint8_t> &uid); char *format_uid_to(char *buffer, std::span<const uint8_t> uid);
/// Buffer size for format_bytes_to (64 bytes max = 192 chars with space separator) /// Buffer size for format_bytes_to (64 bytes max = 192 chars with space separator)
static constexpr size_t FORMAT_BYTES_BUFFER_SIZE = 192; static constexpr size_t FORMAT_BYTES_BUFFER_SIZE = 192;
/// Format bytes to buffer with ' ' separator (e.g., "04 11 22 33"). Returns buffer for inline use. /// Format bytes to buffer with ' ' separator (e.g., "04 11 22 33"). Returns buffer for inline use.
char *format_bytes_to(char *buffer, const std::vector<uint8_t> &bytes); char *format_bytes_to(char *buffer, std::span<const uint8_t> bytes);
// Remove before 2026.6.0 // Remove before 2026.6.0
ESPDEPRECATED("Use format_uid_to() with stack buffer instead. Removed in 2026.6.0", "2025.12.0") ESPDEPRECATED("Use format_uid_to() with stack buffer instead. Removed in 2026.6.0", "2025.12.0")
std::string format_uid(const std::vector<uint8_t> &uid); std::string format_uid(std::span<const uint8_t> uid);
// Remove before 2026.6.0 // Remove before 2026.6.0
ESPDEPRECATED("Use format_bytes_to() with stack buffer instead. Removed in 2026.6.0", "2025.12.0") ESPDEPRECATED("Use format_bytes_to() with stack buffer instead. Removed in 2026.6.0", "2025.12.0")
std::string format_bytes(const std::vector<uint8_t> &bytes); std::string format_bytes(std::span<const uint8_t> bytes);
uint8_t guess_tag_type(uint8_t uid_length); uint8_t guess_tag_type(uint8_t uid_length);
uint8_t get_mifare_classic_ndef_start_index(std::vector<uint8_t> &data); uint8_t get_mifare_classic_ndef_start_index(std::vector<uint8_t> &data);

View File

@@ -10,26 +10,27 @@
namespace esphome { namespace esphome {
namespace nfc { namespace nfc {
// NFC UIDs are 4, 7, or 10 bytes depending on tag type
static constexpr size_t NFC_UID_MAX_LENGTH = 10;
using NfcTagUid = StaticVector<uint8_t, NFC_UID_MAX_LENGTH>;
class NfcTag { class NfcTag {
public: public:
NfcTag() { NfcTag() { this->tag_type_ = "Unknown"; };
this->uid_ = {}; NfcTag(const NfcTagUid &uid) {
this->tag_type_ = "Unknown";
};
NfcTag(std::vector<uint8_t> &uid) {
this->uid_ = uid; this->uid_ = uid;
this->tag_type_ = "Unknown"; this->tag_type_ = "Unknown";
}; };
NfcTag(std::vector<uint8_t> &uid, const std::string &tag_type) { NfcTag(const NfcTagUid &uid, const std::string &tag_type) {
this->uid_ = uid; this->uid_ = uid;
this->tag_type_ = tag_type; this->tag_type_ = tag_type;
}; };
NfcTag(std::vector<uint8_t> &uid, const std::string &tag_type, std::unique_ptr<nfc::NdefMessage> ndef_message) { NfcTag(const NfcTagUid &uid, const std::string &tag_type, std::unique_ptr<nfc::NdefMessage> ndef_message) {
this->uid_ = uid; this->uid_ = uid;
this->tag_type_ = tag_type; this->tag_type_ = tag_type;
this->ndef_message_ = std::move(ndef_message); this->ndef_message_ = std::move(ndef_message);
}; };
NfcTag(std::vector<uint8_t> &uid, const std::string &tag_type, std::vector<uint8_t> &ndef_data) { NfcTag(const NfcTagUid &uid, const std::string &tag_type, std::vector<uint8_t> &ndef_data) {
this->uid_ = uid; this->uid_ = uid;
this->tag_type_ = tag_type; this->tag_type_ = tag_type;
this->ndef_message_ = make_unique<NdefMessage>(ndef_data); this->ndef_message_ = make_unique<NdefMessage>(ndef_data);
@@ -41,14 +42,14 @@ class NfcTag {
ndef_message_ = make_unique<NdefMessage>(*rhs.ndef_message_); ndef_message_ = make_unique<NdefMessage>(*rhs.ndef_message_);
} }
std::vector<uint8_t> &get_uid() { return this->uid_; }; NfcTagUid &get_uid() { return this->uid_; };
const std::string &get_tag_type() { return this->tag_type_; }; const std::string &get_tag_type() { return this->tag_type_; };
bool has_ndef_message() { return this->ndef_message_ != nullptr; }; bool has_ndef_message() { return this->ndef_message_ != nullptr; };
const std::shared_ptr<NdefMessage> &get_ndef_message() { return this->ndef_message_; }; const std::shared_ptr<NdefMessage> &get_ndef_message() { return this->ndef_message_; };
void set_ndef_message(std::unique_ptr<NdefMessage> ndef_message) { this->ndef_message_ = std::move(ndef_message); }; void set_ndef_message(std::unique_ptr<NdefMessage> ndef_message) { this->ndef_message_ = std::move(ndef_message); };
protected: protected:
std::vector<uint8_t> uid_; NfcTagUid uid_;
std::string tag_type_; std::string tag_type_;
std::shared_ptr<NdefMessage> ndef_message_; std::shared_ptr<NdefMessage> ndef_message_;
}; };

View File

@@ -69,9 +69,21 @@ def set_core_data(config: ConfigType) -> ConfigType:
def set_framework(config: ConfigType) -> ConfigType: def set_framework(config: ConfigType) -> ConfigType:
version = cv.Version.parse(cv.version_number(config[CONF_FRAMEWORK][CONF_VERSION])) framework_ver = cv.Version.parse(
CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = version cv.version_number(config[CONF_FRAMEWORK][CONF_VERSION])
return config )
CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = framework_ver
if framework_ver < cv.Version(2, 9, 2):
return cv.require_framework_version(
nrf52_zephyr=cv.Version(2, 6, 1, "a"),
)(config)
if framework_ver < cv.Version(3, 2, 0):
return cv.require_framework_version(
nrf52_zephyr=cv.Version(2, 9, 2, "2"),
)(config)
return cv.require_framework_version(
nrf52_zephyr=cv.Version(3, 2, 0, "1"),
)(config)
BOOTLOADERS = [ BOOTLOADERS = [
@@ -140,7 +152,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_UICR_ERASE, default=False): cv.boolean, cv.Optional(CONF_UICR_ERASE, default=False): cv.boolean,
} }
), ),
cv.Optional(CONF_FRAMEWORK, default={CONF_VERSION: "2.6.1-7"}): cv.Schema( cv.Optional(CONF_FRAMEWORK, default={CONF_VERSION: "2.6.1-a"}): cv.Schema(
{ {
cv.Required(CONF_VERSION): cv.string_strict, cv.Required(CONF_VERSION): cv.string_strict,
} }
@@ -181,13 +193,12 @@ async def to_code(config: ConfigType) -> None:
cg.add_platformio_option(CONF_FRAMEWORK, CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK]) cg.add_platformio_option(CONF_FRAMEWORK, CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK])
cg.add_platformio_option( cg.add_platformio_option(
"platform", "platform",
"https://github.com/tomaszduda23/platform-nordicnrf52/archive/refs/tags/v10.3.0-1.zip", "https://github.com/tomaszduda23/platform-nordicnrf52/archive/refs/tags/v10.3.0-5.zip",
) )
cg.add_platformio_option( cg.add_platformio_option(
"platform_packages", "platform_packages",
[ [
f"platformio/framework-zephyr@https://github.com/tomaszduda23/framework-sdk-nrf/archive/refs/tags/v{CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]}.zip", f"platformio/framework-zephyr@https://github.com/tomaszduda23/framework-sdk-nrf/archive/refs/tags/v{CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]}.zip",
"platformio/toolchain-gccarmnoneeabi@https://github.com/tomaszduda23/toolchain-sdk-ng/archive/refs/tags/v0.17.4-0.zip",
], ],
) )

View File

@@ -1,6 +1,6 @@
from esphome import automation from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import mqtt, web_server from esphome.components import mqtt, web_server, zigbee
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_ABOVE, CONF_ABOVE,
@@ -189,6 +189,7 @@ validate_unit_of_measurement = cv.string_strict
_NUMBER_SCHEMA = ( _NUMBER_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
.extend(zigbee.NUMBER_SCHEMA)
.extend( .extend(
{ {
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTNumberComponent), cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTNumberComponent),
@@ -214,6 +215,7 @@ _NUMBER_SCHEMA = (
_NUMBER_SCHEMA.add_extra(entity_duplicate_validator("number")) _NUMBER_SCHEMA.add_extra(entity_duplicate_validator("number"))
_NUMBER_SCHEMA.add_extra(zigbee.validate_number)
def number_schema( def number_schema(
@@ -277,6 +279,8 @@ async def setup_number_core_(
if web_server_config := config.get(CONF_WEB_SERVER): if web_server_config := config.get(CONF_WEB_SERVER):
await web_server.add_entity_config(var, web_server_config) await web_server.add_entity_config(var, web_server_config)
await zigbee.setup_number(var, config, min_value, max_value, step)
async def register_number( async def register_number(
var, config, *, min_value: float, max_value: float, step: float var, config, *, min_value: float, max_value: float, step: float

View File

@@ -14,8 +14,7 @@ void ValueRangeTrigger::setup() {
float local_min = this->min_.value(0.0); float local_min = this->min_.value(0.0);
float local_max = this->max_.value(0.0); float local_max = this->max_.value(0.0);
convert hash = {.from = (local_max - local_min)}; convert hash = {.from = (local_max - local_min)};
uint32_t myhash = hash.to ^ this->parent_->get_preference_hash(); this->rtc_ = this->parent_->make_entity_preference<bool>(hash.to);
this->rtc_ = global_preferences->make_preference<bool>(myhash);
bool initial_state; bool initial_state;
if (this->rtc_.load(&initial_state)) { if (this->rtc_.load(&initial_state)) {
this->previous_in_range_ = initial_state; this->previous_in_range_ = initial_state;

View File

@@ -14,18 +14,9 @@ void log_number(const char *tag, const char *prefix, const char *type, Number *o
} }
ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str()); ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str());
LOG_ENTITY_ICON(tag, prefix, *obj);
if (!obj->get_icon_ref().empty()) { LOG_ENTITY_UNIT_OF_MEASUREMENT(tag, prefix, obj->traits);
ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj->get_icon_ref().c_str()); LOG_ENTITY_DEVICE_CLASS(tag, prefix, obj->traits);
}
if (!obj->traits.get_unit_of_measurement_ref().empty()) {
ESP_LOGCONFIG(tag, "%s Unit of Measurement: '%s'", prefix, obj->traits.get_unit_of_measurement_ref().c_str());
}
if (!obj->traits.get_device_class_ref().empty()) {
ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj->traits.get_device_class_ref().c_str());
}
} }
void Number::publish_state(float state) { void Number::publish_state(float state) {

View File

@@ -17,7 +17,7 @@ void OpenthermNumber::setup() {
if (!this->restore_value_) { if (!this->restore_value_) {
value = this->initial_value_; value = this->initial_value_;
} else { } else {
this->pref_ = global_preferences->make_preference<float>(this->get_preference_hash()); this->pref_ = this->make_entity_preference<float>();
if (!this->pref_.load(&value)) { if (!this->pref_.load(&value)) {
if (!std::isnan(this->initial_value_)) { if (!std::isnan(this->initial_value_)) {
value = this->initial_value_; value = this->initial_value_;

View File

@@ -168,11 +168,11 @@ void PN532::loop() {
} }
uint8_t nfcid_length = read[5]; uint8_t nfcid_length = read[5];
std::vector<uint8_t> nfcid(read.begin() + 6, read.begin() + 6 + nfcid_length); if (nfcid_length > nfc::NFC_UID_MAX_LENGTH || read.size() < 6U + nfcid_length) {
if (read.size() < 6U + nfcid_length) {
// oops, pn532 returned invalid data // oops, pn532 returned invalid data
return; return;
} }
nfc::NfcTagUid nfcid(read.begin() + 6, read.begin() + 6 + nfcid_length);
bool report = true; bool report = true;
for (auto *bin_sens : this->binary_sensors_) { for (auto *bin_sens : this->binary_sensors_) {
@@ -358,7 +358,7 @@ void PN532::turn_off_rf_() {
}); });
} }
std::unique_ptr<nfc::NfcTag> PN532::read_tag_(std::vector<uint8_t> &uid) { std::unique_ptr<nfc::NfcTag> PN532::read_tag_(nfc::NfcTagUid &uid) {
uint8_t type = nfc::guess_tag_type(uid.size()); uint8_t type = nfc::guess_tag_type(uid.size());
if (type == nfc::TAG_TYPE_MIFARE_CLASSIC) { if (type == nfc::TAG_TYPE_MIFARE_CLASSIC) {
@@ -393,7 +393,7 @@ void PN532::write_mode(nfc::NdefMessage *message) {
ESP_LOGD(TAG, "Waiting to write next tag"); ESP_LOGD(TAG, "Waiting to write next tag");
} }
bool PN532::clean_tag_(std::vector<uint8_t> &uid) { bool PN532::clean_tag_(nfc::NfcTagUid &uid) {
uint8_t type = nfc::guess_tag_type(uid.size()); uint8_t type = nfc::guess_tag_type(uid.size());
if (type == nfc::TAG_TYPE_MIFARE_CLASSIC) { if (type == nfc::TAG_TYPE_MIFARE_CLASSIC) {
return this->format_mifare_classic_mifare_(uid); return this->format_mifare_classic_mifare_(uid);
@@ -404,7 +404,7 @@ bool PN532::clean_tag_(std::vector<uint8_t> &uid) {
return false; return false;
} }
bool PN532::format_tag_(std::vector<uint8_t> &uid) { bool PN532::format_tag_(nfc::NfcTagUid &uid) {
uint8_t type = nfc::guess_tag_type(uid.size()); uint8_t type = nfc::guess_tag_type(uid.size());
if (type == nfc::TAG_TYPE_MIFARE_CLASSIC) { if (type == nfc::TAG_TYPE_MIFARE_CLASSIC) {
return this->format_mifare_classic_ndef_(uid); return this->format_mifare_classic_ndef_(uid);
@@ -415,7 +415,7 @@ bool PN532::format_tag_(std::vector<uint8_t> &uid) {
return false; return false;
} }
bool PN532::write_tag_(std::vector<uint8_t> &uid, nfc::NdefMessage *message) { bool PN532::write_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message) {
uint8_t type = nfc::guess_tag_type(uid.size()); uint8_t type = nfc::guess_tag_type(uid.size());
if (type == nfc::TAG_TYPE_MIFARE_CLASSIC) { if (type == nfc::TAG_TYPE_MIFARE_CLASSIC) {
return this->write_mifare_classic_tag_(uid, message); return this->write_mifare_classic_tag_(uid, message);
@@ -448,7 +448,7 @@ void PN532::dump_config() {
} }
} }
bool PN532BinarySensor::process(std::vector<uint8_t> &data) { bool PN532BinarySensor::process(const nfc::NfcTagUid &data) {
if (data.size() != this->uid_.size()) if (data.size() != this->uid_.size())
return false; return false;

View File

@@ -69,28 +69,28 @@ class PN532 : public PollingComponent {
virtual bool read_data(std::vector<uint8_t> &data, uint8_t len) = 0; virtual bool read_data(std::vector<uint8_t> &data, uint8_t len) = 0;
virtual bool read_response(uint8_t command, std::vector<uint8_t> &data) = 0; virtual bool read_response(uint8_t command, std::vector<uint8_t> &data) = 0;
std::unique_ptr<nfc::NfcTag> read_tag_(std::vector<uint8_t> &uid); std::unique_ptr<nfc::NfcTag> read_tag_(nfc::NfcTagUid &uid);
bool format_tag_(std::vector<uint8_t> &uid); bool format_tag_(nfc::NfcTagUid &uid);
bool clean_tag_(std::vector<uint8_t> &uid); bool clean_tag_(nfc::NfcTagUid &uid);
bool write_tag_(std::vector<uint8_t> &uid, nfc::NdefMessage *message); bool write_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message);
std::unique_ptr<nfc::NfcTag> read_mifare_classic_tag_(std::vector<uint8_t> &uid); std::unique_ptr<nfc::NfcTag> read_mifare_classic_tag_(nfc::NfcTagUid &uid);
bool read_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &data); bool read_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &data);
bool write_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &data); bool write_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &data);
bool auth_mifare_classic_block_(std::vector<uint8_t> &uid, uint8_t block_num, uint8_t key_num, const uint8_t *key); bool auth_mifare_classic_block_(nfc::NfcTagUid &uid, uint8_t block_num, uint8_t key_num, const uint8_t *key);
bool format_mifare_classic_mifare_(std::vector<uint8_t> &uid); bool format_mifare_classic_mifare_(nfc::NfcTagUid &uid);
bool format_mifare_classic_ndef_(std::vector<uint8_t> &uid); bool format_mifare_classic_ndef_(nfc::NfcTagUid &uid);
bool write_mifare_classic_tag_(std::vector<uint8_t> &uid, nfc::NdefMessage *message); bool write_mifare_classic_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message);
std::unique_ptr<nfc::NfcTag> read_mifare_ultralight_tag_(std::vector<uint8_t> &uid); std::unique_ptr<nfc::NfcTag> read_mifare_ultralight_tag_(nfc::NfcTagUid &uid);
bool read_mifare_ultralight_bytes_(uint8_t start_page, uint16_t num_bytes, std::vector<uint8_t> &data); bool read_mifare_ultralight_bytes_(uint8_t start_page, uint16_t num_bytes, std::vector<uint8_t> &data);
bool is_mifare_ultralight_formatted_(const std::vector<uint8_t> &page_3_to_6); bool is_mifare_ultralight_formatted_(const std::vector<uint8_t> &page_3_to_6);
uint16_t read_mifare_ultralight_capacity_(); uint16_t read_mifare_ultralight_capacity_();
bool find_mifare_ultralight_ndef_(const std::vector<uint8_t> &page_3_to_6, uint8_t &message_length, bool find_mifare_ultralight_ndef_(const std::vector<uint8_t> &page_3_to_6, uint8_t &message_length,
uint8_t &message_start_index); uint8_t &message_start_index);
bool write_mifare_ultralight_page_(uint8_t page_num, std::vector<uint8_t> &write_data); bool write_mifare_ultralight_page_(uint8_t page_num, std::vector<uint8_t> &write_data);
bool write_mifare_ultralight_tag_(std::vector<uint8_t> &uid, nfc::NdefMessage *message); bool write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message);
bool clean_mifare_ultralight_(); bool clean_mifare_ultralight_();
bool updates_enabled_{true}; bool updates_enabled_{true};
@@ -98,7 +98,7 @@ class PN532 : public PollingComponent {
std::vector<PN532BinarySensor *> binary_sensors_; std::vector<PN532BinarySensor *> binary_sensors_;
std::vector<nfc::NfcOnTagTrigger *> triggers_ontag_; std::vector<nfc::NfcOnTagTrigger *> triggers_ontag_;
std::vector<nfc::NfcOnTagTrigger *> triggers_ontagremoved_; std::vector<nfc::NfcOnTagTrigger *> triggers_ontagremoved_;
std::vector<uint8_t> current_uid_; nfc::NfcTagUid current_uid_;
nfc::NdefMessage *next_task_message_to_write_; nfc::NdefMessage *next_task_message_to_write_;
uint32_t rd_start_time_{0}; uint32_t rd_start_time_{0};
enum PN532ReadReady rd_ready_ { WOULDBLOCK }; enum PN532ReadReady rd_ready_ { WOULDBLOCK };
@@ -118,9 +118,9 @@ class PN532 : public PollingComponent {
class PN532BinarySensor : public binary_sensor::BinarySensor { class PN532BinarySensor : public binary_sensor::BinarySensor {
public: public:
void set_uid(const std::vector<uint8_t> &uid) { uid_ = uid; } void set_uid(const nfc::NfcTagUid &uid) { uid_ = uid; }
bool process(std::vector<uint8_t> &data); bool process(const nfc::NfcTagUid &data);
void on_scan_end() { void on_scan_end() {
if (!this->found_) { if (!this->found_) {
@@ -130,7 +130,7 @@ class PN532BinarySensor : public binary_sensor::BinarySensor {
} }
protected: protected:
std::vector<uint8_t> uid_; nfc::NfcTagUid uid_;
bool found_{false}; bool found_{false};
}; };

View File

@@ -8,7 +8,7 @@ namespace pn532 {
static const char *const TAG = "pn532.mifare_classic"; static const char *const TAG = "pn532.mifare_classic";
std::unique_ptr<nfc::NfcTag> PN532::read_mifare_classic_tag_(std::vector<uint8_t> &uid) { std::unique_ptr<nfc::NfcTag> PN532::read_mifare_classic_tag_(nfc::NfcTagUid &uid) {
uint8_t current_block = 4; uint8_t current_block = 4;
uint8_t message_start_index = 0; uint8_t message_start_index = 0;
uint32_t message_length = 0; uint32_t message_length = 0;
@@ -82,8 +82,7 @@ bool PN532::read_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &
return true; return true;
} }
bool PN532::auth_mifare_classic_block_(std::vector<uint8_t> &uid, uint8_t block_num, uint8_t key_num, bool PN532::auth_mifare_classic_block_(nfc::NfcTagUid &uid, uint8_t block_num, uint8_t key_num, const uint8_t *key) {
const uint8_t *key) {
std::vector<uint8_t> data({ std::vector<uint8_t> data({
PN532_COMMAND_INDATAEXCHANGE, PN532_COMMAND_INDATAEXCHANGE,
0x01, // One card 0x01, // One card
@@ -106,7 +105,7 @@ bool PN532::auth_mifare_classic_block_(std::vector<uint8_t> &uid, uint8_t block_
return true; return true;
} }
bool PN532::format_mifare_classic_mifare_(std::vector<uint8_t> &uid) { bool PN532::format_mifare_classic_mifare_(nfc::NfcTagUid &uid) {
std::vector<uint8_t> blank_buffer( std::vector<uint8_t> blank_buffer(
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00});
std::vector<uint8_t> trailer_buffer( std::vector<uint8_t> trailer_buffer(
@@ -141,7 +140,7 @@ bool PN532::format_mifare_classic_mifare_(std::vector<uint8_t> &uid) {
return !error; return !error;
} }
bool PN532::format_mifare_classic_ndef_(std::vector<uint8_t> &uid) { bool PN532::format_mifare_classic_ndef_(nfc::NfcTagUid &uid) {
std::vector<uint8_t> empty_ndef_message( std::vector<uint8_t> empty_ndef_message(
{0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); {0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00});
std::vector<uint8_t> blank_block( std::vector<uint8_t> blank_block(
@@ -216,7 +215,7 @@ bool PN532::write_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t>
return true; return true;
} }
bool PN532::write_mifare_classic_tag_(std::vector<uint8_t> &uid, nfc::NdefMessage *message) { bool PN532::write_mifare_classic_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message) {
auto encoded = message->encode(); auto encoded = message->encode();
uint32_t message_length = encoded.size(); uint32_t message_length = encoded.size();

View File

@@ -8,7 +8,7 @@ namespace pn532 {
static const char *const TAG = "pn532.mifare_ultralight"; static const char *const TAG = "pn532.mifare_ultralight";
std::unique_ptr<nfc::NfcTag> PN532::read_mifare_ultralight_tag_(std::vector<uint8_t> &uid) { std::unique_ptr<nfc::NfcTag> PN532::read_mifare_ultralight_tag_(nfc::NfcTagUid &uid) {
std::vector<uint8_t> data; std::vector<uint8_t> data;
// pages 3 to 6 contain various info we are interested in -- do one read to grab it all // pages 3 to 6 contain various info we are interested in -- do one read to grab it all
if (!this->read_mifare_ultralight_bytes_(3, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE * nfc::MIFARE_ULTRALIGHT_READ_SIZE, if (!this->read_mifare_ultralight_bytes_(3, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE * nfc::MIFARE_ULTRALIGHT_READ_SIZE,
@@ -114,7 +114,7 @@ bool PN532::find_mifare_ultralight_ndef_(const std::vector<uint8_t> &page_3_to_6
return false; return false;
} }
bool PN532::write_mifare_ultralight_tag_(std::vector<uint8_t> &uid, nfc::NdefMessage *message) { bool PN532::write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message) {
uint32_t capacity = this->read_mifare_ultralight_capacity_(); uint32_t capacity = this->read_mifare_ultralight_capacity_();
auto encoded = message->encode(); auto encoded = message->encode();

View File

@@ -478,7 +478,7 @@ uint8_t PN7150::read_endpoint_data_(nfc::NfcTag &tag) {
return nfc::STATUS_FAILED; return nfc::STATUS_FAILED;
} }
uint8_t PN7150::clean_endpoint_(std::vector<uint8_t> &uid) { uint8_t PN7150::clean_endpoint_(nfc::NfcTagUid &uid) {
uint8_t type = nfc::guess_tag_type(uid.size()); uint8_t type = nfc::guess_tag_type(uid.size());
switch (type) { switch (type) {
case nfc::TAG_TYPE_MIFARE_CLASSIC: case nfc::TAG_TYPE_MIFARE_CLASSIC:
@@ -494,7 +494,7 @@ uint8_t PN7150::clean_endpoint_(std::vector<uint8_t> &uid) {
return nfc::STATUS_FAILED; return nfc::STATUS_FAILED;
} }
uint8_t PN7150::format_endpoint_(std::vector<uint8_t> &uid) { uint8_t PN7150::format_endpoint_(nfc::NfcTagUid &uid) {
uint8_t type = nfc::guess_tag_type(uid.size()); uint8_t type = nfc::guess_tag_type(uid.size());
switch (type) { switch (type) {
case nfc::TAG_TYPE_MIFARE_CLASSIC: case nfc::TAG_TYPE_MIFARE_CLASSIC:
@@ -510,7 +510,7 @@ uint8_t PN7150::format_endpoint_(std::vector<uint8_t> &uid) {
return nfc::STATUS_FAILED; return nfc::STATUS_FAILED;
} }
uint8_t PN7150::write_endpoint_(std::vector<uint8_t> &uid, std::shared_ptr<nfc::NdefMessage> &message) { uint8_t PN7150::write_endpoint_(nfc::NfcTagUid &uid, std::shared_ptr<nfc::NdefMessage> &message) {
uint8_t type = nfc::guess_tag_type(uid.size()); uint8_t type = nfc::guess_tag_type(uid.size());
switch (type) { switch (type) {
case nfc::TAG_TYPE_MIFARE_CLASSIC: case nfc::TAG_TYPE_MIFARE_CLASSIC:
@@ -534,7 +534,7 @@ std::unique_ptr<nfc::NfcTag> PN7150::build_tag_(const uint8_t mode_tech, const s
ESP_LOGE(TAG, "UID length cannot be zero"); ESP_LOGE(TAG, "UID length cannot be zero");
return nullptr; return nullptr;
} }
std::vector<uint8_t> uid(data.begin() + 3, data.begin() + 3 + uid_length); nfc::NfcTagUid uid(data.begin() + 3, data.begin() + 3 + uid_length);
const auto *tag_type_str = const auto *tag_type_str =
nfc::guess_tag_type(uid_length) == nfc::TAG_TYPE_MIFARE_CLASSIC ? nfc::MIFARE_CLASSIC : nfc::NFC_FORUM_TYPE_2; nfc::guess_tag_type(uid_length) == nfc::TAG_TYPE_MIFARE_CLASSIC ? nfc::MIFARE_CLASSIC : nfc::NFC_FORUM_TYPE_2;
return make_unique<nfc::NfcTag>(uid, tag_type_str); return make_unique<nfc::NfcTag>(uid, tag_type_str);
@@ -543,7 +543,7 @@ std::unique_ptr<nfc::NfcTag> PN7150::build_tag_(const uint8_t mode_tech, const s
return nullptr; return nullptr;
} }
optional<size_t> PN7150::find_tag_uid_(const std::vector<uint8_t> &uid) { optional<size_t> PN7150::find_tag_uid_(const nfc::NfcTagUid &uid) {
if (!this->discovered_endpoint_.empty()) { if (!this->discovered_endpoint_.empty()) {
for (size_t i = 0; i < this->discovered_endpoint_.size(); i++) { for (size_t i = 0; i < this->discovered_endpoint_.size(); i++) {
auto existing_tag_uid = this->discovered_endpoint_[i].tag->get_uid(); auto existing_tag_uid = this->discovered_endpoint_[i].tag->get_uid();

View File

@@ -203,12 +203,12 @@ class PN7150 : public nfc::Nfcc, public Component {
void select_endpoint_(); void select_endpoint_();
uint8_t read_endpoint_data_(nfc::NfcTag &tag); uint8_t read_endpoint_data_(nfc::NfcTag &tag);
uint8_t clean_endpoint_(std::vector<uint8_t> &uid); uint8_t clean_endpoint_(nfc::NfcTagUid &uid);
uint8_t format_endpoint_(std::vector<uint8_t> &uid); uint8_t format_endpoint_(nfc::NfcTagUid &uid);
uint8_t write_endpoint_(std::vector<uint8_t> &uid, std::shared_ptr<nfc::NdefMessage> &message); uint8_t write_endpoint_(nfc::NfcTagUid &uid, std::shared_ptr<nfc::NdefMessage> &message);
std::unique_ptr<nfc::NfcTag> build_tag_(uint8_t mode_tech, const std::vector<uint8_t> &data); std::unique_ptr<nfc::NfcTag> build_tag_(uint8_t mode_tech, const std::vector<uint8_t> &data);
optional<size_t> find_tag_uid_(const std::vector<uint8_t> &uid); optional<size_t> find_tag_uid_(const nfc::NfcTagUid &uid);
void purge_old_tags_(); void purge_old_tags_();
void erase_tag_(uint8_t tag_index); void erase_tag_(uint8_t tag_index);
@@ -251,7 +251,7 @@ class PN7150 : public nfc::Nfcc, public Component {
uint8_t find_mifare_ultralight_ndef_(const std::vector<uint8_t> &page_3_to_6, uint8_t &message_length, uint8_t find_mifare_ultralight_ndef_(const std::vector<uint8_t> &page_3_to_6, uint8_t &message_length,
uint8_t &message_start_index); uint8_t &message_start_index);
uint8_t write_mifare_ultralight_page_(uint8_t page_num, std::vector<uint8_t> &write_data); uint8_t write_mifare_ultralight_page_(uint8_t page_num, std::vector<uint8_t> &write_data);
uint8_t write_mifare_ultralight_tag_(std::vector<uint8_t> &uid, const std::shared_ptr<nfc::NdefMessage> &message); uint8_t write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::shared_ptr<nfc::NdefMessage> &message);
uint8_t clean_mifare_ultralight_(); uint8_t clean_mifare_ultralight_();
enum NfcTask : uint8_t { enum NfcTask : uint8_t {

View File

@@ -115,8 +115,7 @@ uint8_t PN7150::find_mifare_ultralight_ndef_(const std::vector<uint8_t> &page_3_
return nfc::STATUS_FAILED; return nfc::STATUS_FAILED;
} }
uint8_t PN7150::write_mifare_ultralight_tag_(std::vector<uint8_t> &uid, uint8_t PN7150::write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::shared_ptr<nfc::NdefMessage> &message) {
const std::shared_ptr<nfc::NdefMessage> &message) {
uint32_t capacity = this->read_mifare_ultralight_capacity_(); uint32_t capacity = this->read_mifare_ultralight_capacity_();
auto encoded = message->encode(); auto encoded = message->encode();

View File

@@ -506,7 +506,7 @@ uint8_t PN7160::read_endpoint_data_(nfc::NfcTag &tag) {
return nfc::STATUS_FAILED; return nfc::STATUS_FAILED;
} }
uint8_t PN7160::clean_endpoint_(std::vector<uint8_t> &uid) { uint8_t PN7160::clean_endpoint_(nfc::NfcTagUid &uid) {
uint8_t type = nfc::guess_tag_type(uid.size()); uint8_t type = nfc::guess_tag_type(uid.size());
switch (type) { switch (type) {
case nfc::TAG_TYPE_MIFARE_CLASSIC: case nfc::TAG_TYPE_MIFARE_CLASSIC:
@@ -522,7 +522,7 @@ uint8_t PN7160::clean_endpoint_(std::vector<uint8_t> &uid) {
return nfc::STATUS_FAILED; return nfc::STATUS_FAILED;
} }
uint8_t PN7160::format_endpoint_(std::vector<uint8_t> &uid) { uint8_t PN7160::format_endpoint_(nfc::NfcTagUid &uid) {
uint8_t type = nfc::guess_tag_type(uid.size()); uint8_t type = nfc::guess_tag_type(uid.size());
switch (type) { switch (type) {
case nfc::TAG_TYPE_MIFARE_CLASSIC: case nfc::TAG_TYPE_MIFARE_CLASSIC:
@@ -538,7 +538,7 @@ uint8_t PN7160::format_endpoint_(std::vector<uint8_t> &uid) {
return nfc::STATUS_FAILED; return nfc::STATUS_FAILED;
} }
uint8_t PN7160::write_endpoint_(std::vector<uint8_t> &uid, std::shared_ptr<nfc::NdefMessage> &message) { uint8_t PN7160::write_endpoint_(nfc::NfcTagUid &uid, std::shared_ptr<nfc::NdefMessage> &message) {
uint8_t type = nfc::guess_tag_type(uid.size()); uint8_t type = nfc::guess_tag_type(uid.size());
switch (type) { switch (type) {
case nfc::TAG_TYPE_MIFARE_CLASSIC: case nfc::TAG_TYPE_MIFARE_CLASSIC:
@@ -562,7 +562,7 @@ std::unique_ptr<nfc::NfcTag> PN7160::build_tag_(const uint8_t mode_tech, const s
ESP_LOGE(TAG, "UID length cannot be zero"); ESP_LOGE(TAG, "UID length cannot be zero");
return nullptr; return nullptr;
} }
std::vector<uint8_t> uid(data.begin() + 3, data.begin() + 3 + uid_length); nfc::NfcTagUid uid(data.begin() + 3, data.begin() + 3 + uid_length);
const auto *tag_type_str = const auto *tag_type_str =
nfc::guess_tag_type(uid_length) == nfc::TAG_TYPE_MIFARE_CLASSIC ? nfc::MIFARE_CLASSIC : nfc::NFC_FORUM_TYPE_2; nfc::guess_tag_type(uid_length) == nfc::TAG_TYPE_MIFARE_CLASSIC ? nfc::MIFARE_CLASSIC : nfc::NFC_FORUM_TYPE_2;
return make_unique<nfc::NfcTag>(uid, tag_type_str); return make_unique<nfc::NfcTag>(uid, tag_type_str);
@@ -571,7 +571,7 @@ std::unique_ptr<nfc::NfcTag> PN7160::build_tag_(const uint8_t mode_tech, const s
return nullptr; return nullptr;
} }
optional<size_t> PN7160::find_tag_uid_(const std::vector<uint8_t> &uid) { optional<size_t> PN7160::find_tag_uid_(const nfc::NfcTagUid &uid) {
if (!this->discovered_endpoint_.empty()) { if (!this->discovered_endpoint_.empty()) {
for (size_t i = 0; i < this->discovered_endpoint_.size(); i++) { for (size_t i = 0; i < this->discovered_endpoint_.size(); i++) {
auto existing_tag_uid = this->discovered_endpoint_[i].tag->get_uid(); auto existing_tag_uid = this->discovered_endpoint_[i].tag->get_uid();

View File

@@ -220,12 +220,12 @@ class PN7160 : public nfc::Nfcc, public Component {
void select_endpoint_(); void select_endpoint_();
uint8_t read_endpoint_data_(nfc::NfcTag &tag); uint8_t read_endpoint_data_(nfc::NfcTag &tag);
uint8_t clean_endpoint_(std::vector<uint8_t> &uid); uint8_t clean_endpoint_(nfc::NfcTagUid &uid);
uint8_t format_endpoint_(std::vector<uint8_t> &uid); uint8_t format_endpoint_(nfc::NfcTagUid &uid);
uint8_t write_endpoint_(std::vector<uint8_t> &uid, std::shared_ptr<nfc::NdefMessage> &message); uint8_t write_endpoint_(nfc::NfcTagUid &uid, std::shared_ptr<nfc::NdefMessage> &message);
std::unique_ptr<nfc::NfcTag> build_tag_(uint8_t mode_tech, const std::vector<uint8_t> &data); std::unique_ptr<nfc::NfcTag> build_tag_(uint8_t mode_tech, const std::vector<uint8_t> &data);
optional<size_t> find_tag_uid_(const std::vector<uint8_t> &uid); optional<size_t> find_tag_uid_(const nfc::NfcTagUid &uid);
void purge_old_tags_(); void purge_old_tags_();
void erase_tag_(uint8_t tag_index); void erase_tag_(uint8_t tag_index);
@@ -268,7 +268,7 @@ class PN7160 : public nfc::Nfcc, public Component {
uint8_t find_mifare_ultralight_ndef_(const std::vector<uint8_t> &page_3_to_6, uint8_t &message_length, uint8_t find_mifare_ultralight_ndef_(const std::vector<uint8_t> &page_3_to_6, uint8_t &message_length,
uint8_t &message_start_index); uint8_t &message_start_index);
uint8_t write_mifare_ultralight_page_(uint8_t page_num, std::vector<uint8_t> &write_data); uint8_t write_mifare_ultralight_page_(uint8_t page_num, std::vector<uint8_t> &write_data);
uint8_t write_mifare_ultralight_tag_(std::vector<uint8_t> &uid, const std::shared_ptr<nfc::NdefMessage> &message); uint8_t write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::shared_ptr<nfc::NdefMessage> &message);
uint8_t clean_mifare_ultralight_(); uint8_t clean_mifare_ultralight_();
enum NfcTask : uint8_t { enum NfcTask : uint8_t {

View File

@@ -115,8 +115,7 @@ uint8_t PN7160::find_mifare_ultralight_ndef_(const std::vector<uint8_t> &page_3_
return nfc::STATUS_FAILED; return nfc::STATUS_FAILED;
} }
uint8_t PN7160::write_mifare_ultralight_tag_(std::vector<uint8_t> &uid, uint8_t PN7160::write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::shared_ptr<nfc::NdefMessage> &message) {
const std::shared_ptr<nfc::NdefMessage> &message) {
uint32_t capacity = this->read_mifare_ultralight_capacity_(); uint32_t capacity = this->read_mifare_ultralight_capacity_();
auto encoded = message->encode(); auto encoded = message->encode();

View File

@@ -41,12 +41,14 @@ class PrometheusHandler : public AsyncWebHandler, public Component {
void add_label_name(EntityBase *obj, const std::string &value) { relabel_map_name_.insert({obj, value}); } void add_label_name(EntityBase *obj, const std::string &value) { relabel_map_name_.insert({obj, value}); }
bool canHandle(AsyncWebServerRequest *request) const override { bool canHandle(AsyncWebServerRequest *request) const override {
if (request->method() == HTTP_GET) { if (request->method() != HTTP_GET)
if (request->url() == "/metrics") return false;
return true; #ifdef USE_ESP32
} char url_buf[AsyncWebServerRequest::URL_BUF_SIZE];
return request->url_to(url_buf) == "/metrics";
return false; #else
return request->url() == ESPHOME_F("/metrics");
#endif
} }
void handleRequest(AsyncWebServerRequest *req) override; void handleRequest(AsyncWebServerRequest *req) override;

View File

@@ -170,6 +170,9 @@ CONFIG_SCHEMA = remote_base.validate_triggers(
async def to_code(config): async def to_code(config):
pin = await cg.gpio_pin_expression(config[CONF_PIN]) pin = await cg.gpio_pin_expression(config[CONF_PIN])
if CORE.is_esp32: if CORE.is_esp32:
# Re-enable ESP-IDF's RMT driver (excluded by default to save compile time)
esp32.include_builtin_idf_component("esp_driver_rmt")
var = cg.new_Pvariable(config[CONF_ID], pin) var = cg.new_Pvariable(config[CONF_ID], pin)
cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS])) cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS]))
cg.add(var.set_receive_symbols(config[CONF_RECEIVE_SYMBOLS])) cg.add(var.set_receive_symbols(config[CONF_RECEIVE_SYMBOLS]))

View File

@@ -112,6 +112,9 @@ async def digital_write_action_to_code(config, action_id, template_arg, args):
async def to_code(config): async def to_code(config):
pin = await cg.gpio_pin_expression(config[CONF_PIN]) pin = await cg.gpio_pin_expression(config[CONF_PIN])
if CORE.is_esp32: if CORE.is_esp32:
# Re-enable ESP-IDF's RMT driver (excluded by default to save compile time)
esp32.include_builtin_idf_component("esp_driver_rmt")
var = cg.new_Pvariable(config[CONF_ID], pin) var = cg.new_Pvariable(config[CONF_ID], pin)
cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS])) cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS]))
cg.add(var.set_non_blocking(config[CONF_NON_BLOCKING])) cg.add(var.set_non_blocking(config[CONF_NON_BLOCKING]))

View File

@@ -132,7 +132,7 @@ void RotaryEncoderSensor::setup() {
int32_t initial_value = 0; int32_t initial_value = 0;
switch (this->restore_mode_) { switch (this->restore_mode_) {
case ROTARY_ENCODER_RESTORE_DEFAULT_ZERO: case ROTARY_ENCODER_RESTORE_DEFAULT_ZERO:
this->rtc_ = global_preferences->make_preference<int32_t>(this->get_preference_hash()); this->rtc_ = this->make_entity_preference<int32_t>();
if (!this->rtc_.load(&initial_value)) { if (!this->rtc_.load(&initial_value)) {
initial_value = 0; initial_value = 0;
} }

View File

@@ -8,7 +8,6 @@
#include "preferences.h" #include "preferences.h"
#include <cstring> #include <cstring>
#include <vector>
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
@@ -25,6 +24,9 @@ static bool s_flash_dirty = false; // NOLINT(cppcoreguidelines-avoid-no
static const uint32_t RP2040_FLASH_STORAGE_SIZE = 512; static const uint32_t RP2040_FLASH_STORAGE_SIZE = 512;
// Stack buffer size for preferences - covers virtually all real-world preferences without heap allocation
static constexpr size_t PREF_BUFFER_SIZE = 64;
extern "C" uint8_t _EEPROM_start; extern "C" uint8_t _EEPROM_start;
template<class It> uint8_t calculate_crc(It first, It last, uint32_t type) { template<class It> uint8_t calculate_crc(It first, It last, uint32_t type) {
@@ -42,12 +44,14 @@ class RP2040PreferenceBackend : public ESPPreferenceBackend {
uint32_t type = 0; uint32_t type = 0;
bool save(const uint8_t *data, size_t len) override { bool save(const uint8_t *data, size_t len) override {
std::vector<uint8_t> buffer; const size_t buffer_size = len + 1;
buffer.resize(len + 1); SmallBufferWithHeapFallback<PREF_BUFFER_SIZE> buffer_alloc(buffer_size);
memcpy(buffer.data(), data, len); uint8_t *buffer = buffer_alloc.get();
buffer[buffer.size() - 1] = calculate_crc(buffer.begin(), buffer.end() - 1, type);
for (uint32_t i = 0; i < len + 1; i++) { memcpy(buffer, data, len);
buffer[len] = calculate_crc(buffer, buffer + len, type);
for (size_t i = 0; i < buffer_size; i++) {
uint32_t j = offset + i; uint32_t j = offset + i;
if (j >= RP2040_FLASH_STORAGE_SIZE) if (j >= RP2040_FLASH_STORAGE_SIZE)
return false; return false;
@@ -60,22 +64,23 @@ class RP2040PreferenceBackend : public ESPPreferenceBackend {
return true; return true;
} }
bool load(uint8_t *data, size_t len) override { bool load(uint8_t *data, size_t len) override {
std::vector<uint8_t> buffer; const size_t buffer_size = len + 1;
buffer.resize(len + 1); SmallBufferWithHeapFallback<PREF_BUFFER_SIZE> buffer_alloc(buffer_size);
uint8_t *buffer = buffer_alloc.get();
for (size_t i = 0; i < len + 1; i++) { for (size_t i = 0; i < buffer_size; i++) {
uint32_t j = offset + i; uint32_t j = offset + i;
if (j >= RP2040_FLASH_STORAGE_SIZE) if (j >= RP2040_FLASH_STORAGE_SIZE)
return false; return false;
buffer[i] = s_flash_storage[j]; buffer[i] = s_flash_storage[j];
} }
uint8_t crc = calculate_crc(buffer.begin(), buffer.end() - 1, type); uint8_t crc = calculate_crc(buffer, buffer + len, type);
if (buffer[buffer.size() - 1] != crc) { if (buffer[len] != crc) {
return false; return false;
} }
memcpy(data, buffer.data(), len); memcpy(data, buffer, len);
return true; return true;
} }
}; };

View File

@@ -23,6 +23,10 @@ RTL87XX_BOARDS = {
"name": "WR2 Wi-Fi Module", "name": "WR2 Wi-Fi Module",
"family": FAMILY_RTL8710B, "family": FAMILY_RTL8710B,
}, },
"wbr3": {
"name": "WBR3 Wi-Fi Module",
"family": FAMILY_RTL8720C,
},
"generic-rtl8710bn-2mb-468k": { "generic-rtl8710bn-2mb-468k": {
"name": "Generic - RTL8710BN (2M/468k)", "name": "Generic - RTL8710BN (2M/468k)",
"family": FAMILY_RTL8710B, "family": FAMILY_RTL8710B,
@@ -79,6 +83,10 @@ RTL87XX_BOARDS = {
"name": "T103_V1.0", "name": "T103_V1.0",
"family": FAMILY_RTL8710B, "family": FAMILY_RTL8710B,
}, },
"generic-rtl8720cf-2mb-896k": {
"name": "Generic - RTL8720CF (2M/896k)",
"family": FAMILY_RTL8720C,
},
"generic-rtl8720cf-2mb-992k": { "generic-rtl8720cf-2mb-992k": {
"name": "Generic - RTL8720CF (2M/992k)", "name": "Generic - RTL8720CF (2M/992k)",
"family": FAMILY_RTL8720C, "family": FAMILY_RTL8720C,
@@ -221,6 +229,71 @@ RTL87XX_BOARD_PINS = {
"D9": 29, "D9": 29,
"A1": 41, "A1": 41,
}, },
"wbr3": {
"WIRE0_SCL_0": 11,
"WIRE0_SCL_1": 2,
"WIRE0_SCL_2": 19,
"WIRE0_SCL_3": 15,
"WIRE0_SDA_0": 3,
"WIRE0_SDA_1": 12,
"WIRE0_SDA_2": 16,
"SERIAL0_RX_0": 12,
"SERIAL0_RX_1": 13,
"SERIAL0_TX_0": 11,
"SERIAL0_TX_1": 14,
"SERIAL1_CTS": 4,
"SERIAL1_RX_0": 2,
"SERIAL1_RX_1": 0,
"SERIAL1_TX_0": 3,
"SERIAL1_TX_1": 1,
"SERIAL2_CTS": 19,
"SERIAL2_RX": 15,
"SERIAL2_TX": 16,
"CS0": 15,
"CTS1": 4,
"CTS2": 19,
"PA00": 0,
"PA0": 0,
"PA01": 1,
"PA1": 1,
"PA02": 2,
"PA2": 2,
"PA03": 3,
"PA3": 3,
"PA04": 4,
"PA4": 4,
"PA07": 7,
"PA7": 7,
"PA11": 11,
"PA12": 12,
"PA13": 13,
"PA14": 14,
"PA15": 15,
"PA16": 16,
"PA17": 17,
"PA18": 18,
"PA19": 19,
"PWM5": 17,
"PWM6": 18,
"RX2": 15,
"SDA0": 16,
"TX2": 16,
"D0": 7,
"D1": 11,
"D2": 2,
"D3": 3,
"D4": 4,
"D5": 12,
"D6": 16,
"D7": 17,
"D8": 18,
"D9": 19,
"D10": 13,
"D11": 14,
"D12": 15,
"D13": 0,
"D14": 1,
},
"generic-rtl8710bn-2mb-468k": { "generic-rtl8710bn-2mb-468k": {
"SPI0_CS": 19, "SPI0_CS": 19,
"SPI0_MISO": 22, "SPI0_MISO": 22,
@@ -1178,6 +1251,104 @@ RTL87XX_BOARD_PINS = {
"A0": 19, "A0": 19,
"A1": 41, "A1": 41,
}, },
"generic-rtl8720cf-2mb-896k": {
"SPI0_CS_0": 2,
"SPI0_CS_1": 7,
"SPI0_CS_2": 15,
"SPI0_MISO_0": 10,
"SPI0_MISO_1": 20,
"SPI0_MOSI_0": 4,
"SPI0_MOSI_1": 9,
"SPI0_MOSI_2": 19,
"SPI0_SCK_0": 3,
"SPI0_SCK_1": 8,
"SPI0_SCK_2": 16,
"WIRE0_SCL_0": 2,
"WIRE0_SCL_1": 11,
"WIRE0_SCL_2": 15,
"WIRE0_SCL_3": 19,
"WIRE0_SDA_0": 3,
"WIRE0_SDA_1": 12,
"WIRE0_SDA_2": 16,
"WIRE0_SDA_3": 20,
"SERIAL0_CTS": 10,
"SERIAL0_RTS": 9,
"SERIAL0_RX_0": 12,
"SERIAL0_RX_1": 13,
"SERIAL0_TX_0": 11,
"SERIAL0_TX_1": 14,
"SERIAL1_CTS": 4,
"SERIAL1_RX_0": 0,
"SERIAL1_RX_1": 2,
"SERIAL1_TX_0": 1,
"SERIAL1_TX_1": 3,
"SERIAL2_CTS": 19,
"SERIAL2_RTS": 20,
"SERIAL2_RX": 15,
"SERIAL2_TX": 16,
"CS0": 15,
"CTS0": 10,
"CTS1": 4,
"CTS2": 19,
"MOSI0": 19,
"PA00": 0,
"PA0": 0,
"PA01": 1,
"PA1": 1,
"PA02": 2,
"PA2": 2,
"PA03": 3,
"PA3": 3,
"PA04": 4,
"PA4": 4,
"PA07": 7,
"PA7": 7,
"PA08": 8,
"PA8": 8,
"PA09": 9,
"PA9": 9,
"PA10": 10,
"PA11": 11,
"PA12": 12,
"PA13": 13,
"PA14": 14,
"PA15": 15,
"PA16": 16,
"PA17": 17,
"PA18": 18,
"PA19": 19,
"PA20": 20,
"PA23": 23,
"PWM0": 20,
"PWM5": 17,
"PWM6": 18,
"PWM7": 23,
"RTS0": 9,
"RTS2": 20,
"RX2": 15,
"SCK0": 16,
"TX2": 16,
"D0": 0,
"D1": 1,
"D2": 2,
"D3": 3,
"D4": 4,
"D5": 7,
"D6": 8,
"D7": 9,
"D8": 10,
"D9": 11,
"D10": 12,
"D11": 13,
"D12": 14,
"D13": 15,
"D14": 16,
"D15": 17,
"D16": 18,
"D17": 19,
"D18": 20,
"D19": 23,
},
"generic-rtl8720cf-2mb-992k": { "generic-rtl8720cf-2mb-992k": {
"SPI0_CS_0": 2, "SPI0_CS_0": 2,
"SPI0_CS_1": 7, "SPI0_CS_1": 7,

View File

@@ -27,46 +27,61 @@ void RuntimeStatsCollector::record_component_time(Component *component, uint32_t
} }
void RuntimeStatsCollector::log_stats_() { void RuntimeStatsCollector::log_stats_() {
// First pass: count active components
size_t count = 0;
for (const auto &it : this->component_stats_) {
if (it.second.get_period_count() > 0) {
count++;
}
}
ESP_LOGI(TAG, ESP_LOGI(TAG,
"Component Runtime Statistics\n" "Component Runtime Statistics\n"
" Period stats (last %" PRIu32 "ms):", " Period stats (last %" PRIu32 "ms): %zu active components",
this->log_interval_); this->log_interval_, count);
// First collect stats we want to display if (count == 0) {
std::vector<ComponentStatPair> stats_to_display; return;
}
// Stack buffer sized to actual active count (up to 256 components), heap fallback for larger
SmallBufferWithHeapFallback<256, Component *> buffer(count);
Component **sorted = buffer.get();
// Second pass: fill buffer with active components
size_t idx = 0;
for (const auto &it : this->component_stats_) { for (const auto &it : this->component_stats_) {
Component *component = it.first; if (it.second.get_period_count() > 0) {
const ComponentRuntimeStats &stats = it.second; sorted[idx++] = it.first;
if (stats.get_period_count() > 0) {
ComponentStatPair pair = {component, &stats};
stats_to_display.push_back(pair);
} }
} }
// Sort by period runtime (descending) // Sort by period runtime (descending)
std::sort(stats_to_display.begin(), stats_to_display.end(), std::greater<ComponentStatPair>()); std::sort(sorted, sorted + count, [this](Component *a, Component *b) {
return this->component_stats_[a].get_period_time_ms() > this->component_stats_[b].get_period_time_ms();
});
// Log top components by period runtime // Log top components by period runtime
for (const auto &it : stats_to_display) { for (size_t i = 0; i < count; i++) {
const auto &stats = this->component_stats_[sorted[i]];
ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms",
LOG_STR_ARG(it.component->get_component_log_str()), it.stats->get_period_count(), LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.get_period_count(), stats.get_period_avg_time_ms(),
it.stats->get_period_avg_time_ms(), it.stats->get_period_max_time_ms(), it.stats->get_period_time_ms()); stats.get_period_max_time_ms(), stats.get_period_time_ms());
} }
// Log total stats since boot // Log total stats since boot (only for active components - idle ones haven't changed)
ESP_LOGI(TAG, " Total stats (since boot):"); ESP_LOGI(TAG, " Total stats (since boot): %zu active components", count);
// Re-sort by total runtime for all-time stats // Re-sort by total runtime for all-time stats
std::sort(stats_to_display.begin(), stats_to_display.end(), std::sort(sorted, sorted + count, [this](Component *a, Component *b) {
[](const ComponentStatPair &a, const ComponentStatPair &b) { return this->component_stats_[a].get_total_time_ms() > this->component_stats_[b].get_total_time_ms();
return a.stats->get_total_time_ms() > b.stats->get_total_time_ms(); });
});
for (const auto &it : stats_to_display) { for (size_t i = 0; i < count; i++) {
const auto &stats = this->component_stats_[sorted[i]];
ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms",
LOG_STR_ARG(it.component->get_component_log_str()), it.stats->get_total_count(), LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.get_total_count(), stats.get_total_avg_time_ms(),
it.stats->get_total_avg_time_ms(), it.stats->get_total_max_time_ms(), it.stats->get_total_time_ms()); stats.get_total_max_time_ms(), stats.get_total_time_ms());
} }
} }

View File

@@ -5,7 +5,6 @@
#ifdef USE_RUNTIME_STATS #ifdef USE_RUNTIME_STATS
#include <map> #include <map>
#include <vector>
#include <cstdint> #include <cstdint>
#include <cstring> #include <cstring>
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
@@ -77,17 +76,6 @@ class ComponentRuntimeStats {
uint32_t total_max_time_ms_; uint32_t total_max_time_ms_;
}; };
// For sorting components by run time
struct ComponentStatPair {
Component *component;
const ComponentRuntimeStats *stats;
bool operator>(const ComponentStatPair &other) const {
// Sort by period time as that's what we're displaying in the logs
return stats->get_period_time_ms() > other.stats->get_period_time_ms();
}
};
class RuntimeStatsCollector { class RuntimeStatsCollector {
public: public:
RuntimeStatsCollector(); RuntimeStatsCollector();

View File

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

View File

@@ -39,7 +39,7 @@ class ValueRangeTrigger : public Trigger<float>, public Component {
template<typename V> void set_max(V max) { this->max_ = max; } template<typename V> void set_max(V max) { this->max_ = max; }
void setup() override { void setup() override {
this->rtc_ = global_preferences->make_preference<bool>(this->parent_->get_preference_hash()); this->rtc_ = this->parent_->make_entity_preference<bool>();
bool initial_state; bool initial_state;
if (this->rtc_.load(&initial_state)) { if (this->rtc_.load(&initial_state)) {
this->previous_in_range_ = initial_state; this->previous_in_range_ = initial_state;

View File

@@ -22,13 +22,8 @@ void log_sensor(const char *tag, const char *prefix, const char *type, Sensor *o
LOG_STR_ARG(state_class_to_string(obj->get_state_class())), prefix, LOG_STR_ARG(state_class_to_string(obj->get_state_class())), prefix,
obj->get_unit_of_measurement_ref().c_str(), prefix, obj->get_accuracy_decimals()); obj->get_unit_of_measurement_ref().c_str(), prefix, obj->get_accuracy_decimals());
if (!obj->get_device_class_ref().empty()) { LOG_ENTITY_DEVICE_CLASS(tag, prefix, *obj);
ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj->get_device_class_ref().c_str()); 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->get_force_update()) { if (obj->get_force_update()) {
ESP_LOGV(tag, "%s Force Update: YES", prefix); ESP_LOGV(tag, "%s Force Update: YES", prefix);

View File

@@ -29,6 +29,14 @@ void socket_delay(uint32_t ms) {
// Use esp_delay with a callback that checks if socket data arrived. // Use esp_delay with a callback that checks if socket data arrived.
// This allows the delay to exit early when socket_wake() is called by // This allows the delay to exit early when socket_wake() is called by
// lwip recv_fn/accept_fn callbacks, reducing socket latency. // lwip recv_fn/accept_fn callbacks, reducing socket latency.
//
// When ms is 0, we must use delay(0) because esp_delay(0, callback)
// exits immediately without yielding, which can cause watchdog timeouts
// when the main loop runs in high-frequency mode (e.g., during light effects).
if (ms == 0) {
delay(0);
return;
}
s_socket_woke = false; s_socket_woke = false;
esp_delay(ms, []() { return !s_socket_woke; }); esp_delay(ms, []() { return !s_socket_woke; });
} }

View File

@@ -2,7 +2,7 @@ from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import audio, audio_dac from esphome.components import audio, audio_dac
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_DATA, CONF_ID, CONF_VOLUME from esphome.const import CONF_AUDIO_DAC, CONF_DATA, CONF_ID, CONF_VOLUME
from esphome.core import CORE, ID from esphome.core import CORE, ID
from esphome.coroutine import CoroPriority, coroutine_with_priority from esphome.coroutine import CoroPriority, coroutine_with_priority
@@ -11,8 +11,6 @@ CODEOWNERS = ["@jesserockz", "@kahrendt"]
IS_PLATFORM_COMPONENT = True IS_PLATFORM_COMPONENT = True
CONF_AUDIO_DAC = "audio_dac"
speaker_ns = cg.esphome_ns.namespace("speaker") speaker_ns = cg.esphome_ns.namespace("speaker")
Speaker = speaker_ns.class_("Speaker") Speaker = speaker_ns.class_("Speaker")

View File

@@ -55,7 +55,7 @@ void SpeakerMediaPlayer::setup() {
this->media_control_command_queue_ = xQueueCreate(MEDIA_CONTROLS_QUEUE_LENGTH, sizeof(MediaCallCommand)); this->media_control_command_queue_ = xQueueCreate(MEDIA_CONTROLS_QUEUE_LENGTH, sizeof(MediaCallCommand));
this->pref_ = global_preferences->make_preference<VolumeRestoreState>(this->get_preference_hash()); this->pref_ = this->make_entity_preference<VolumeRestoreState>();
VolumeRestoreState volume_restore_state; VolumeRestoreState volume_restore_state;
if (this->pref_.load(&volume_restore_state)) { if (this->pref_.load(&volume_restore_state)) {

View File

@@ -16,7 +16,7 @@ void SprinklerControllerNumber::setup() {
if (!this->restore_value_) { if (!this->restore_value_) {
value = this->initial_value_; value = this->initial_value_;
} else { } else {
this->pref_ = global_preferences->make_preference<float>(this->get_preference_hash()); this->pref_ = this->make_entity_preference<float>();
if (!this->pref_.load(&value)) { if (!this->pref_.load(&value)) {
if (!std::isnan(this->initial_value_)) { if (!std::isnan(this->initial_value_)) {
value = this->initial_value_; value = this->initial_value_;

View File

@@ -34,7 +34,7 @@ optional<bool> Switch::get_initial_state() {
if (!(restore_mode & RESTORE_MODE_PERSISTENT_MASK)) if (!(restore_mode & RESTORE_MODE_PERSISTENT_MASK))
return {}; return {};
this->rtc_ = global_preferences->make_preference<bool>(this->get_preference_hash()); this->rtc_ = this->make_entity_preference<bool>();
bool initial_state; bool initial_state;
if (!this->rtc_.load(&initial_state)) if (!this->rtc_.load(&initial_state))
return {}; return {};
@@ -96,18 +96,14 @@ void log_switch(const char *tag, const char *prefix, const char *type, Switch *o
LOG_STR_ARG(onoff)); LOG_STR_ARG(onoff));
// Add optional fields separately // Add optional fields separately
if (!obj->get_icon_ref().empty()) { LOG_ENTITY_ICON(tag, prefix, *obj);
ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj->get_icon_ref().c_str());
}
if (obj->assumed_state()) { if (obj->assumed_state()) {
ESP_LOGCONFIG(tag, "%s Assumed State: YES", prefix); ESP_LOGCONFIG(tag, "%s Assumed State: YES", prefix);
} }
if (obj->is_inverted()) { if (obj->is_inverted()) {
ESP_LOGCONFIG(tag, "%s Inverted: YES", prefix); ESP_LOGCONFIG(tag, "%s Inverted: YES", prefix);
} }
if (!obj->get_device_class_ref().empty()) { LOG_ENTITY_DEVICE_CLASS(tag, prefix, *obj);
ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj->get_device_class_ref().c_str());
}
} }
} }

View File

@@ -82,7 +82,7 @@ void TemplateAlarmControlPanel::setup() {
this->current_state_ = ACP_STATE_DISARMED; this->current_state_ = ACP_STATE_DISARMED;
if (this->restore_mode_ == ALARM_CONTROL_PANEL_RESTORE_DEFAULT_DISARMED) { if (this->restore_mode_ == ALARM_CONTROL_PANEL_RESTORE_DEFAULT_DISARMED) {
uint8_t value; uint8_t value;
this->pref_ = global_preferences->make_preference<uint8_t>(this->get_preference_hash()); this->pref_ = this->make_entity_preference<uint8_t>();
if (this->pref_.load(&value)) { if (this->pref_.load(&value)) {
this->current_state_ = static_cast<alarm_control_panel::AlarmControlPanelState>(value); this->current_state_ = static_cast<alarm_control_panel::AlarmControlPanelState>(value);
} }

View File

@@ -18,8 +18,7 @@ void TemplateDate::setup() {
state = this->initial_value_; state = this->initial_value_;
} else { } else {
datetime::DateEntityRestoreState temp; datetime::DateEntityRestoreState temp;
this->pref_ = this->pref_ = this->make_entity_preference<datetime::DateEntityRestoreState>(194434030U);
global_preferences->make_preference<datetime::DateEntityRestoreState>(194434030U ^ this->get_preference_hash());
if (this->pref_.load(&temp)) { if (this->pref_.load(&temp)) {
temp.apply(this); temp.apply(this);
return; return;

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