Merge remote-tracking branch 'upstream/dev' into memory

This commit is contained in:
J. Nick Koston 2025-07-25 09:45:46 -10:00
commit bbea462502
No known key found for this signature in database
238 changed files with 7336 additions and 8410 deletions

View File

@ -1 +1 @@
0c2acbc16bfb7d63571dbe7042f94f683be25e4ca8a0f158a960a94adac4b931 32b0db73b3ae01ba18c9cbb1dabbd8156bc14dded500471919bd0a3dc33916e0

View File

@ -11,51 +11,10 @@ permissions:
contents: read contents: read
env: env:
TARGET_PLATFORMS: |
esp32
esp8266
rp2040
libretiny
bk72xx
rtl87xx
ln882x
nrf52
host
PLATFORM_COMPONENTS: |
alarm_control_panel
audio_adc
audio_dac
binary_sensor
button
canbus
climate
cover
datetime
display
event
fan
light
lock
media_player
microphone
number
one_wire
ota
output
packet_transport
select
sensor
speaker
stepper
switch
text
text_sensor
time
touchscreen
update
valve
SMALL_PR_THRESHOLD: 30 SMALL_PR_THRESHOLD: 30
MAX_LABELS: 15 MAX_LABELS: 15
TOO_BIG_THRESHOLD: 1000
COMPONENT_LABEL_THRESHOLD: 10
jobs: jobs:
label: label:
@ -65,24 +24,6 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
- name: Get changes
id: changes
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Get PR number
pr_number="${{ github.event.pull_request.number }}"
# Get list of changed files using gh CLI
files=$(gh pr diff $pr_number --name-only)
echo "files<<EOF" >> $GITHUB_OUTPUT
echo "$files" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
# Get file stats (additions + deletions) using gh CLI
stats=$(gh pr view $pr_number --json files --jq '.files | map(.additions + .deletions) | add')
echo "total_changes=${stats:-0}" >> $GITHUB_OUTPUT
- name: Generate a token - name: Generate a token
id: generate-token id: generate-token
uses: actions/create-github-app-token@v2 uses: actions/create-github-app-token@v2
@ -97,73 +38,466 @@ jobs:
script: | script: |
const fs = require('fs'); const fs = require('fs');
// Constants
const SMALL_PR_THRESHOLD = parseInt('${{ env.SMALL_PR_THRESHOLD }}');
const MAX_LABELS = parseInt('${{ env.MAX_LABELS }}');
const TOO_BIG_THRESHOLD = parseInt('${{ env.TOO_BIG_THRESHOLD }}');
const COMPONENT_LABEL_THRESHOLD = parseInt('${{ env.COMPONENT_LABEL_THRESHOLD }}');
const BOT_COMMENT_MARKER = '<!-- auto-label-pr-bot -->';
const CODEOWNERS_MARKER = '<!-- codeowners-request -->';
const TOO_BIG_MARKER = '<!-- too-big-request -->';
const MANAGED_LABELS = [
'new-component',
'new-platform',
'new-target-platform',
'merging-to-release',
'merging-to-beta',
'core',
'small-pr',
'dashboard',
'github-actions',
'by-code-owner',
'has-tests',
'needs-tests',
'needs-docs',
'needs-codeowners',
'too-big',
'labeller-recheck'
];
const DOCS_PR_PATTERNS = [
/https:\/\/github\.com\/esphome\/esphome-docs\/pull\/\d+/,
/esphome\/esphome-docs#\d+/
];
// Global state
const { owner, repo } = context.repo; const { owner, repo } = context.repo;
const pr_number = context.issue.number; const pr_number = context.issue.number;
// Get current labels // Get current labels and PR data
const { data: currentLabelsData } = await github.rest.issues.listLabelsOnIssue({ const { data: currentLabelsData } = await github.rest.issues.listLabelsOnIssue({
owner, owner,
repo, repo,
issue_number: pr_number issue_number: pr_number
}); });
const currentLabels = currentLabelsData.map(label => label.name); const currentLabels = currentLabelsData.map(label => label.name);
// Define managed labels that this workflow controls
const managedLabels = currentLabels.filter(label => const managedLabels = currentLabels.filter(label =>
label.startsWith('component: ') || label.startsWith('component: ') || MANAGED_LABELS.includes(label)
[
'new-component',
'new-platform',
'new-target-platform',
'merging-to-release',
'merging-to-beta',
'core',
'small-pr',
'dashboard',
'github-actions',
'by-code-owner',
'has-tests',
'needs-tests',
'needs-docs',
'too-big',
'labeller-recheck'
].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 totalChanges = prFiles.reduce((sum, file) => sum + (file.additions || 0) + (file.deletions || 0), 0);
console.log('Current labels:', currentLabels.join(', ')); console.log('Current labels:', currentLabels.join(', '));
console.log('Managed labels:', managedLabels.join(', '));
// Get changed files
const changedFiles = `${{ steps.changes.outputs.files }}`.split('\n').filter(f => f.length > 0);
const totalChanges = parseInt('${{ steps.changes.outputs.total_changes }}') || 0;
console.log('Changed files:', changedFiles.length); console.log('Changed files:', changedFiles.length);
console.log('Total changes:', totalChanges); console.log('Total changes:', totalChanges);
if (isMegaPR) {
console.log('Mega-PR detected - applying limited labeling logic');
}
const labels = new Set(); // 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: [] };
}
}
// Get environment variables // Strategy: Merge branch detection
const targetPlatforms = `${{ env.TARGET_PLATFORMS }}`.split('\n').filter(p => p.trim().length > 0).map(p => p.trim()); async function detectMergeBranch() {
const platformComponents = `${{ env.PLATFORM_COMPONENTS }}`.split('\n').filter(p => p.trim().length > 0).map(p => p.trim()); const labels = new Set();
const smallPrThreshold = parseInt('${{ env.SMALL_PR_THRESHOLD }}'); const baseRef = context.payload.pull_request.base.ref;
const maxLabels = parseInt('${{ env.MAX_LABELS }}');
// Strategy: Merge to release or beta branch
const baseRef = context.payload.pull_request.base.ref;
if (baseRef !== 'dev') {
if (baseRef === 'release') { if (baseRef === 'release') {
labels.add('merging-to-release'); labels.add('merging-to-release');
} else if (baseRef === 'beta') { } else if (baseRef === 'beta') {
labels.add('merging-to-beta'); labels.add('merging-to-beta');
} }
// When targeting non-dev branches, only use merge warning labels return labels;
const finalLabels = Array.from(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();
const testChanges = prFiles
.filter(file => file.filename.startsWith('tests/'))
.reduce((sum, file) => sum + (file.additions || 0) + (file.deletions || 0), 0);
const nonTestChanges = totalChanges - testChanges;
if (totalChanges <= SMALL_PR_THRESHOLD) {
labels.add('small-pr');
}
// 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: 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('has-tests')) {
labels.add('needs-tests');
}
// Check for missing docs
if (allLabels.has('new-component') || allLabels.has('new-platform')) {
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) {
const messages = [];
const prAuthor = context.payload.pull_request.user.login;
// Too big message
if (finalLabels.includes('too-big')) {
const testChanges = prFiles
.filter(file => file.filename.startsWith('tests/'))
.reduce((sum, file) => sum + (file.additions || 0) + (file.deletions || 0), 0);
const nonTestChanges = totalChanges - testChanges;
const tooManyLabels = finalLabels.length > 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 ${finalLabels.length} different components/areas.`;
} else if (tooManyLabels) {
message += `This PR affects ${finalLabels.length} 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) {
const reviewMessages = generateReviewMessages(finalLabels);
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 non-dev branches
if (baseRef !== 'dev') {
const branchLabels = await detectMergeBranch();
const finalLabels = Array.from(branchLabels);
console.log('Computed labels (merge branch only):', finalLabels.join(', ')); console.log('Computed labels (merge branch only):', finalLabels.join(', '));
// Add new labels // Apply labels
if (finalLabels.length > 0) { if (finalLabels.length > 0) {
console.log(`Adding labels: ${finalLabels.join(', ')}`);
await github.rest.issues.addLabels({ await github.rest.issues.addLabels({
owner, owner,
repo, repo,
@ -172,13 +506,9 @@ jobs:
}); });
} }
// Remove old managed labels that are no longer needed // Remove old managed labels
const labelsToRemove = managedLabels.filter(label => const labelsToRemove = managedLabels.filter(label => !finalLabels.includes(label));
!finalLabels.includes(label)
);
for (const label of labelsToRemove) { for (const label of labelsToRemove) {
console.log(`Removing label: ${label}`);
try { try {
await github.rest.issues.removeLabel({ await github.rest.issues.removeLabel({
owner, owner,
@ -191,234 +521,78 @@ jobs:
} }
} }
return; // Exit early, don't process other strategies return;
} }
// Strategy: Component and Platform labeling // Run all strategies
const componentRegex = /^esphome\/components\/([^\/]+)\//; const [
const targetPlatformRegex = new RegExp(`^esphome\/components\/(${targetPlatforms.join('|')})/`); branchLabels,
componentLabels,
newComponentLabels,
newPlatformLabels,
coreLabels,
sizeLabels,
dashboardLabels,
actionsLabels,
codeOwnerLabels,
testLabels
] = await Promise.all([
detectMergeBranch(),
detectComponentPlatforms(apiData),
detectNewComponents(),
detectNewPlatforms(apiData),
detectCoreChanges(),
detectPRSize(),
detectDashboardChanges(),
detectGitHubActionsChanges(),
detectCodeOwner(),
detectTests()
]);
for (const file of changedFiles) { // Combine all labels
// Check for component changes const allLabels = new Set([
const componentMatch = file.match(componentRegex); ...branchLabels,
if (componentMatch) { ...componentLabels,
const component = componentMatch[1]; ...newComponentLabels,
labels.add(`component: ${component}`); ...newPlatformLabels,
} ...coreLabels,
...sizeLabels,
...dashboardLabels,
...actionsLabels,
...codeOwnerLabels,
...testLabels
]);
// Check for target platform changes // Detect requirements based on all other labels
const platformMatch = file.match(targetPlatformRegex); const requirementLabels = await detectRequirements(allLabels);
if (platformMatch) { for (const label of requirementLabels) {
const targetPlatform = platformMatch[1]; allLabels.add(label);
labels.add(`platform: ${targetPlatform}`); }
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})`);
} }
} }
// Get PR files for new component/platform detection // Handle too many labels (only for non-mega PRs)
const { data: prFiles } = await github.rest.pulls.listFiles({ const tooManyLabels = finalLabels.length > MAX_LABELS;
owner,
repo,
pull_number: pr_number
});
const addedFiles = prFiles.filter(file => file.status === 'added').map(file => file.filename); if (tooManyLabels && !isMegaPR && !finalLabels.includes('too-big')) {
finalLabels = ['too-big'];
// Strategy: New Component detection
for (const file of addedFiles) {
// Check for new component files: esphome/components/{component}/__init__.py
const componentMatch = file.match(/^esphome\/components\/([^\/]+)\/__init__\.py$/);
if (componentMatch) {
try {
// Read the content directly from the filesystem since we have it checked out
const content = fs.readFileSync(file, 'utf8');
// Strategy: New Target Platform detection
if (content.includes('IS_TARGET_PLATFORM = True')) {
labels.add('new-target-platform');
}
labels.add('new-component');
} catch (error) {
console.log(`Failed to read content of ${file}:`, error.message);
// Fallback: assume it's a new component if we can't read the content
labels.add('new-component');
}
}
} }
// Strategy: New Platform detection
for (const file of addedFiles) {
// Check for new platform files: esphome/components/{component}/{platform}.py
const platformFileMatch = file.match(/^esphome\/components\/([^\/]+)\/([^\/]+)\.py$/);
if (platformFileMatch) {
const [, component, platform] = platformFileMatch;
if (platformComponents.includes(platform)) {
labels.add('new-platform');
}
}
// Check for new platform files: esphome/components/{component}/{platform}/__init__.py
const platformDirMatch = file.match(/^esphome\/components\/([^\/]+)\/([^\/]+)\/__init__\.py$/);
if (platformDirMatch) {
const [, component, platform] = platformDirMatch;
if (platformComponents.includes(platform)) {
labels.add('new-platform');
}
}
}
const coreFiles = changedFiles.filter(file =>
file.startsWith('esphome/core/') ||
(file.startsWith('esphome/') && file.split('/').length === 2)
);
if (coreFiles.length > 0) {
labels.add('core');
}
// Strategy: Small PR detection
if (totalChanges <= smallPrThreshold) {
labels.add('small-pr');
}
// Strategy: Dashboard changes
const dashboardFiles = changedFiles.filter(file =>
file.startsWith('esphome/dashboard/') ||
file.startsWith('esphome/components/dashboard_import/')
);
if (dashboardFiles.length > 0) {
labels.add('dashboard');
}
// Strategy: GitHub Actions changes
const githubActionsFiles = changedFiles.filter(file =>
file.startsWith('.github/workflows/')
);
if (githubActionsFiles.length > 0) {
labels.add('github-actions');
}
// Strategy: Code Owner detection
try {
// Fetch CODEOWNERS file from the repository (in case it was changed in this PR)
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;
// Parse CODEOWNERS file
const codeownersLines = codeownersContent.split('\n')
.map(line => line.trim())
.filter(line => line && !line.startsWith('#'));
let isCodeOwner = false;
// Precompile CODEOWNERS patterns into regex objects
const codeownersRegexes = codeownersLines.map(line => {
const parts = line.split(/\s+/);
const pattern = parts[0];
const owners = parts.slice(1);
let regex;
if (pattern.endsWith('*')) {
// Directory pattern like "esphome/components/api/*"
const dir = pattern.slice(0, -1);
regex = new RegExp(`^${dir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`);
} else if (pattern.includes('*')) {
// Glob pattern
const regexPattern = pattern
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
.replace(/\\*/g, '.*');
regex = new RegExp(`^${regexPattern}$`);
} else {
// Exact match
regex = new RegExp(`^${pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`);
}
return { regex, owners };
});
for (const file of changedFiles) {
for (const { regex, owners } of codeownersRegexes) {
if (regex.test(file)) {
// Check if PR author is in the owners list
if (owners.some(owner => owner === `@${prAuthor}`)) {
isCodeOwner = true;
break;
}
}
}
if (isCodeOwner) break;
}
if (isCodeOwner) {
labels.add('by-code-owner');
}
} catch (error) {
console.log('Failed to read or parse CODEOWNERS file:', error.message);
}
// Strategy: Test detection
const testFiles = changedFiles.filter(file =>
file.startsWith('tests/')
);
if (testFiles.length > 0) {
labels.add('has-tests');
} else {
// Only check for needs-tests if this is a new component or new platform
if (labels.has('new-component') || labels.has('new-platform')) {
labels.add('needs-tests');
}
}
// Strategy: Documentation check for new components/platforms
if (labels.has('new-component') || labels.has('new-platform')) {
const prBody = context.payload.pull_request.body || '';
// Look for documentation PR links
// Patterns to match:
// - https://github.com/esphome/esphome-docs/pull/1234
// - esphome/esphome-docs#1234
const docsPrPatterns = [
/https:\/\/github\.com\/esphome\/esphome-docs\/pull\/\d+/,
/esphome\/esphome-docs#\d+/
];
const hasDocsLink = docsPrPatterns.some(pattern => pattern.test(prBody));
if (!hasDocsLink) {
labels.add('needs-docs');
}
}
// Convert Set to Array
let finalLabels = Array.from(labels);
console.log('Computed labels:', finalLabels.join(', ')); console.log('Computed labels:', finalLabels.join(', '));
// Don't set more than max labels // Handle reviews
if (finalLabels.length > maxLabels) { await handleReviews(finalLabels);
const originalLength = finalLabels.length;
console.log(`Not setting ${originalLength} labels because out of range`);
finalLabels = ['too-big'];
// Request changes on the PR // Apply labels
await github.rest.pulls.createReview({
owner,
repo,
pull_number: pr_number,
body: `This PR is too large and affects ${originalLength} different components/areas. Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.`,
event: 'REQUEST_CHANGES'
});
}
// Add new labels
if (finalLabels.length > 0) { if (finalLabels.length > 0) {
console.log(`Adding labels: ${finalLabels.join(', ')}`); console.log(`Adding labels: ${finalLabels.join(', ')}`);
await github.rest.issues.addLabels({ await github.rest.issues.addLabels({
@ -429,11 +603,8 @@ jobs:
}); });
} }
// Remove old managed labels that are no longer needed // Remove old managed labels
const labelsToRemove = managedLabels.filter(label => const labelsToRemove = managedLabels.filter(label => !finalLabels.includes(label));
!finalLabels.includes(label)
);
for (const label of labelsToRemove) { for (const label of labelsToRemove) {
console.log(`Removing label: ${label}`); console.log(`Removing label: ${label}`);
try { try {

View File

@ -34,6 +34,9 @@ jobs:
console.log(`Processing PR #${pr_number} for codeowner review requests`); console.log(`Processing PR #${pr_number} for codeowner review requests`);
// Hidden marker to identify bot comments from this workflow
const BOT_COMMENT_MARKER = '<!-- codeowner-review-request-bot -->';
try { try {
// Get the list of changed files in this PR // Get the list of changed files in this PR
const { data: files } = await github.rest.pulls.listFiles({ const { data: files } = await github.rest.pulls.listFiles({
@ -84,9 +87,9 @@ jobs:
const allMentions = [...reviewerMentions, ...teamMentions].join(', '); const allMentions = [...reviewerMentions, ...teamMentions].join(', ');
if (isSuccessful) { if (isSuccessful) {
return `👋 Hi there! I've automatically requested reviews from codeowners based on the files changed in this PR.\n\n${allMentions} - You've been requested to review this PR as codeowner(s) of ${matchedFileCount} file(s) that were modified. Thanks for your time! 🙏`; return `${BOT_COMMENT_MARKER}\n👋 Hi there! I've automatically requested reviews from codeowners based on the files changed in this PR.\n\n${allMentions} - You've been requested to review this PR as codeowner(s) of ${matchedFileCount} file(s) that were modified. Thanks for your time! 🙏`;
} else { } else {
return `👋 Hi there! This PR modifies ${matchedFileCount} file(s) with codeowners.\n\n${allMentions} - As codeowner(s) of the affected files, your review would be appreciated! 🙏\n\n_Note: Automatic review request may have failed, but you're still welcome to review._`; return `${BOT_COMMENT_MARKER}\n👋 Hi there! This PR modifies ${matchedFileCount} file(s) with codeowners.\n\n${allMentions} - As codeowner(s) of the affected files, your review would be appreciated! 🙏\n\n_Note: Automatic review request may have failed, but you're still welcome to review._`;
} }
} }
@ -178,6 +181,53 @@ jobs:
reviewedUsers.add(review.user.login); reviewedUsers.add(review.user.login);
}); });
// Check for previous comments from this workflow to avoid duplicate pings
const comments = await github.paginate(
github.rest.issues.listComments,
{
owner,
repo,
issue_number: pr_number
}
);
const previouslyPingedUsers = new Set();
const previouslyPingedTeams = new Set();
// Look for comments from github-actions bot that contain our bot marker
const workflowComments = comments.filter(comment =>
comment.user.type === 'Bot' &&
comment.body.includes(BOT_COMMENT_MARKER)
);
// Extract previously mentioned users and teams from workflow comments
for (const comment of workflowComments) {
// Match @username patterns (not team mentions)
const userMentions = comment.body.match(/@([a-zA-Z0-9_.-]+)(?![/])/g) || [];
userMentions.forEach(mention => {
const username = mention.slice(1); // remove @
previouslyPingedUsers.add(username);
});
// Match @org/team patterns
const teamMentions = comment.body.match(/@[a-zA-Z0-9_.-]+\/([a-zA-Z0-9_.-]+)/g) || [];
teamMentions.forEach(mention => {
const teamName = mention.split('/')[1];
previouslyPingedTeams.add(teamName);
});
}
console.log(`Found ${previouslyPingedUsers.size} previously pinged users and ${previouslyPingedTeams.size} previously pinged teams`);
// Remove users who have already been pinged in previous workflow comments
previouslyPingedUsers.forEach(user => {
matchedOwners.delete(user);
});
previouslyPingedTeams.forEach(team => {
matchedTeams.delete(team);
});
// Remove only users who have already submitted reviews (not just requested reviewers) // Remove only users who have already submitted reviews (not just requested reviewers)
reviewedUsers.forEach(reviewer => { reviewedUsers.forEach(reviewer => {
matchedOwners.delete(reviewer); matchedOwners.delete(reviewer);
@ -192,7 +242,7 @@ jobs:
const teamsList = Array.from(matchedTeams); const teamsList = Array.from(matchedTeams);
if (reviewersList.length === 0 && teamsList.length === 0) { if (reviewersList.length === 0 && teamsList.length === 0) {
console.log('No eligible reviewers found (all may already be requested or reviewed)'); console.log('No eligible reviewers found (all may already be requested, reviewed, or previously pinged)');
return; return;
} }
@ -227,31 +277,41 @@ jobs:
console.log('All codeowners are already requested reviewers or have reviewed'); console.log('All codeowners are already requested reviewers or have reviewed');
} }
// Add a comment to the PR mentioning what happened (include all matched codeowners) // Only add a comment if there are new codeowners to mention (not previously pinged)
const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, true); if (reviewersList.length > 0 || teamsList.length > 0) {
const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, true);
await github.rest.issues.createComment({ await github.rest.issues.createComment({
owner, owner,
repo, repo,
issue_number: pr_number, issue_number: pr_number,
body: commentBody body: commentBody
}); });
console.log(`Added comment mentioning ${reviewersList.length} users and ${teamsList.length} teams`);
} else {
console.log('No new codeowners to mention in comment (all previously pinged)');
}
} catch (error) { } catch (error) {
if (error.status === 422) { if (error.status === 422) {
console.log('Some reviewers may already be requested or unavailable:', error.message); console.log('Some reviewers may already be requested or unavailable:', error.message);
// Try to add a comment even if review request failed // Only try to add a comment if there are new codeowners to mention
const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, false); if (reviewersList.length > 0 || teamsList.length > 0) {
const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, false);
try { try {
await github.rest.issues.createComment({ await github.rest.issues.createComment({
owner, owner,
repo, repo,
issue_number: pr_number, issue_number: pr_number,
body: commentBody body: commentBody
}); });
} catch (commentError) { console.log(`Added fallback comment mentioning ${reviewersList.length} users and ${teamsList.length} teams`);
console.log('Failed to add comment:', commentError.message); } catch (commentError) {
console.log('Failed to add comment:', commentError.message);
}
} else {
console.log('No new codeowners to mention in fallback comment');
} }
} else { } else {
throw error; throw error;

View File

@ -61,7 +61,8 @@ jobs:
} }
async function createComment(octokit, owner, repo, prNumber, esphomeChanges, componentChanges) { async function createComment(octokit, owner, repo, prNumber, esphomeChanges, componentChanges) {
const commentMarker = "<!-- This comment was generated automatically by a GitHub workflow. -->"; const commentMarker = "<!-- This comment was generated automatically by the external-component-bot workflow. -->";
const legacyCommentMarker = "<!-- This comment was generated automatically by a GitHub workflow. -->";
let commentBody; let commentBody;
if (esphomeChanges.length === 1) { if (esphomeChanges.length === 1) {
commentBody = generateExternalComponentInstructions(prNumber, componentChanges, owner, repo); commentBody = generateExternalComponentInstructions(prNumber, componentChanges, owner, repo);
@ -71,14 +72,23 @@ jobs:
commentBody += `\n\n---\n(Added by the PR bot)\n\n${commentMarker}`; commentBody += `\n\n---\n(Added by the PR bot)\n\n${commentMarker}`;
// Check for existing bot comment // Check for existing bot comment
const comments = await github.rest.issues.listComments({ const comments = await github.paginate(
owner: owner, github.rest.issues.listComments,
repo: repo, {
issue_number: prNumber, owner: owner,
}); repo: repo,
issue_number: prNumber,
per_page: 100,
}
);
const botComment = comments.data.find(comment => const sorted = comments.sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at));
comment.body.includes(commentMarker)
const botComment = sorted.find(comment =>
(
comment.body.includes(commentMarker) ||
comment.body.includes(legacyCommentMarker)
) && comment.user.type === "Bot"
); );
if (botComment && botComment.body === commentBody) { if (botComment && botComment.body === commentBody) {

View File

@ -29,6 +29,9 @@ jobs:
console.log(`Processing issue #${issue_number} with label: ${labelName}`); console.log(`Processing issue #${issue_number} with label: ${labelName}`);
// Hidden marker to identify bot comments from this workflow
const BOT_COMMENT_MARKER = '<!-- issue-codeowner-notify-bot -->';
// Extract component name from label // Extract component name from label
const componentName = labelName.replace('component: ', ''); const componentName = labelName.replace('component: ', '');
console.log(`Component: ${componentName}`); console.log(`Component: ${componentName}`);
@ -92,16 +95,57 @@ jobs:
mention !== `@${issueAuthor}` mention !== `@${issueAuthor}`
); );
const allMentions = [...filteredUserOwners, ...teamOwners]; // Check for previous comments from this workflow to avoid duplicate pings
const comments = await github.paginate(
github.rest.issues.listComments,
{
owner,
repo,
issue_number: issue_number
}
);
const previouslyPingedUsers = new Set();
const previouslyPingedTeams = new Set();
// Look for comments from github-actions bot that contain codeowner pings for this component
const workflowComments = comments.filter(comment =>
comment.user.type === 'Bot' &&
comment.body.includes(BOT_COMMENT_MARKER) &&
comment.body.includes(`component: ${componentName}`)
);
// Extract previously mentioned users and teams from workflow comments
for (const comment of workflowComments) {
// Match @username patterns (not team mentions)
const userMentions = comment.body.match(/@([a-zA-Z0-9_.-]+)(?![/])/g) || [];
userMentions.forEach(mention => {
previouslyPingedUsers.add(mention); // Keep @ prefix for easy comparison
});
// Match @org/team patterns
const teamMentions = comment.body.match(/@[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+/g) || [];
teamMentions.forEach(mention => {
previouslyPingedTeams.add(mention);
});
}
console.log(`Found ${previouslyPingedUsers.size} previously pinged users and ${previouslyPingedTeams.size} previously pinged teams for component ${componentName}`);
// Remove previously pinged users and teams
const newUserOwners = filteredUserOwners.filter(mention => !previouslyPingedUsers.has(mention));
const newTeamOwners = teamOwners.filter(mention => !previouslyPingedTeams.has(mention));
const allMentions = [...newUserOwners, ...newTeamOwners];
if (allMentions.length === 0) { if (allMentions.length === 0) {
console.log('No codeowners to notify (issue author is the only codeowner)'); console.log('No new codeowners to notify (all previously pinged or issue author is the only codeowner)');
return; return;
} }
// Create comment body // Create comment body
const mentionString = allMentions.join(', '); const mentionString = allMentions.join(', ');
const commentBody = `👋 Hey ${mentionString}!\n\nThis issue has been labeled with \`component: ${componentName}\` and you've been identified as a codeowner of this component. Please take a look when you have a chance!\n\nThanks for maintaining this component! 🙏`; const commentBody = `${BOT_COMMENT_MARKER}\n👋 Hey ${mentionString}!\n\nThis issue has been labeled with \`component: ${componentName}\` and you've been identified as a codeowner of this component. Please take a look when you have a chance!\n\nThanks for maintaining this component! 🙏`;
// Post comment // Post comment
await github.rest.issues.createComment({ await github.rest.issues.createComment({
@ -111,7 +155,7 @@ jobs:
body: commentBody body: commentBody
}); });
console.log(`Successfully notified codeowners: ${mentionString}`); console.log(`Successfully notified new codeowners: ${mentionString}`);
} catch (error) { } catch (error) {
console.log('Failed to process codeowner notifications:', error.message); console.log('Failed to process codeowner notifications:', error.message);

View File

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

View File

@ -9,6 +9,7 @@
pyproject.toml @esphome/core pyproject.toml @esphome/core
esphome/*.py @esphome/core esphome/*.py @esphome/core
esphome/core/* @esphome/core esphome/core/* @esphome/core
.github/** @esphome/core
# Integrations # Integrations
esphome/components/a01nyub/* @MrSuicideParrot esphome/components/a01nyub/* @MrSuicideParrot
@ -245,6 +246,7 @@ esphome/components/lcd_menu/* @numo68
esphome/components/ld2410/* @regevbr @sebcaps esphome/components/ld2410/* @regevbr @sebcaps
esphome/components/ld2420/* @descipher esphome/components/ld2420/* @descipher
esphome/components/ld2450/* @hareeshmu esphome/components/ld2450/* @hareeshmu
esphome/components/ld24xx/* @kbx81
esphome/components/ledc/* @OttoWinter esphome/components/ledc/* @OttoWinter
esphome/components/libretiny/* @kuba2k2 esphome/components/libretiny/* @kuba2k2
esphome/components/libretiny_pwm/* @kuba2k2 esphome/components/libretiny_pwm/* @kuba2k2

View File

@ -34,6 +34,7 @@ from esphome.const import (
CONF_PORT, CONF_PORT,
CONF_SUBSTITUTIONS, CONF_SUBSTITUTIONS,
CONF_TOPIC, CONF_TOPIC,
ENV_NOGITIGNORE,
PLATFORM_ESP32, PLATFORM_ESP32,
PLATFORM_ESP8266, PLATFORM_ESP8266,
PLATFORM_RP2040, PLATFORM_RP2040,
@ -88,9 +89,9 @@ def choose_prompt(options, purpose: str = None):
def choose_upload_log_host( def choose_upload_log_host(
default, check_default, show_ota, show_mqtt, show_api, purpose: str = None default, check_default, show_ota, show_mqtt, show_api, purpose: str = None
): ):
options = [] options = [
for port in get_serial_ports(): (f"{port.path} ({port.description})", port.path) for port in get_serial_ports()
options.append((f"{port.path} ({port.description})", port.path)) ]
if default == "SERIAL": if default == "SERIAL":
return choose_prompt(options, purpose=purpose) return choose_prompt(options, purpose=purpose)
if (show_ota and "ota" in CORE.config) or (show_api and "api" in CORE.config): if (show_ota and "ota" in CORE.config) or (show_api and "api" in CORE.config):
@ -118,9 +119,7 @@ def mqtt_logging_enabled(mqtt_config):
return False return False
if CONF_TOPIC not in log_topic: if CONF_TOPIC not in log_topic:
return False return False
if log_topic.get(CONF_LEVEL, None) == "NONE": return log_topic.get(CONF_LEVEL, None) != "NONE"
return False
return True
def get_port_type(port): def get_port_type(port):
@ -209,6 +208,9 @@ def wrap_to_code(name, comp):
def write_cpp(config): def write_cpp(config):
if not get_bool_env(ENV_NOGITIGNORE):
writer.write_gitignore()
generate_cpp_contents(config) generate_cpp_contents(config)
return write_cpp_file() return write_cpp_file()
@ -225,10 +227,13 @@ def generate_cpp_contents(config):
def write_cpp_file(): def write_cpp_file():
writer.write_platformio_project()
code_s = indent(CORE.cpp_main_section) code_s = indent(CORE.cpp_main_section)
writer.write_cpp(code_s) writer.write_cpp(code_s)
from esphome.build_gen import platformio
platformio.write_project()
return 0 return 0

View File

View File

@ -0,0 +1,102 @@
import os
from esphome.const import __version__
from esphome.core import CORE
from esphome.helpers import mkdir_p, read_file, write_file_if_changed
from esphome.writer import find_begin_end, update_storage_json
INI_AUTO_GENERATE_BEGIN = "; ========== AUTO GENERATED CODE BEGIN ==========="
INI_AUTO_GENERATE_END = "; =========== AUTO GENERATED CODE END ============"
INI_BASE_FORMAT = (
"""; Auto generated code by esphome
[common]
lib_deps =
build_flags =
upload_flags =
""",
"""
""",
)
def format_ini(data: dict[str, str | list[str]]) -> str:
content = ""
for key, value in sorted(data.items()):
if isinstance(value, list):
content += f"{key} =\n"
for x in value:
content += f" {x}\n"
else:
content += f"{key} = {value}\n"
return content
def get_ini_content():
CORE.add_platformio_option(
"lib_deps",
[x.as_lib_dep for x in CORE.platformio_libraries.values()]
+ ["${common.lib_deps}"],
)
# Sort to avoid changing build flags order
CORE.add_platformio_option("build_flags", sorted(CORE.build_flags))
# Sort to avoid changing build unflags order
CORE.add_platformio_option("build_unflags", sorted(CORE.build_unflags))
# Add extra script for C++ flags
CORE.add_platformio_option("extra_scripts", [f"pre:{CXX_FLAGS_FILE_NAME}"])
content = "[platformio]\n"
content += f"description = ESPHome {__version__}\n"
content += f"[env:{CORE.name}]\n"
content += format_ini(CORE.platformio_options)
return content
def write_ini(content):
update_storage_json()
path = CORE.relative_build_path("platformio.ini")
if os.path.isfile(path):
text = read_file(path)
content_format = find_begin_end(
text, INI_AUTO_GENERATE_BEGIN, INI_AUTO_GENERATE_END
)
else:
content_format = INI_BASE_FORMAT
full_file = f"{content_format[0] + INI_AUTO_GENERATE_BEGIN}\n{content}"
full_file += INI_AUTO_GENERATE_END + content_format[1]
write_file_if_changed(path, full_file)
def write_project():
mkdir_p(CORE.build_path)
content = get_ini_content()
write_ini(content)
# Write extra script for C++ specific flags
write_cxx_flags_script()
CXX_FLAGS_FILE_NAME = "cxx_flags.py"
CXX_FLAGS_FILE_CONTENTS = """# Auto-generated ESPHome script for C++ specific compiler flags
Import("env")
# Add C++ specific flags
"""
def write_cxx_flags_script() -> None:
path = CORE.relative_build_path(CXX_FLAGS_FILE_NAME)
contents = CXX_FLAGS_FILE_CONTENTS
if not CORE.is_host:
contents += 'env.Append(CXXFLAGS=["-Wno-volatile"])'
contents += "\n"
write_file_if_changed(path, contents)

View File

@ -323,9 +323,10 @@ async def api_connected_to_code(config, condition_id, template_arg, args):
def FILTER_SOURCE_FILES() -> list[str]: def FILTER_SOURCE_FILES() -> list[str]:
"""Filter out api_pb2_dump.cpp when proto message dumping is not enabled """Filter out api_pb2_dump.cpp when proto message dumping is not enabled,
and user_services.cpp when no services are defined.""" user_services.cpp when no services are defined, and protocol-specific
files_to_filter = [] implementations based on encryption configuration."""
files_to_filter: list[str] = []
# api_pb2_dump.cpp is only needed when HAS_PROTO_MESSAGE_DUMP is defined # api_pb2_dump.cpp is only needed when HAS_PROTO_MESSAGE_DUMP is defined
# This is a particularly large file that still needs to be opened and read # This is a particularly large file that still needs to be opened and read
@ -341,4 +342,16 @@ def FILTER_SOURCE_FILES() -> list[str]:
if config and not config.get(CONF_ACTIONS) and not config[CONF_CUSTOM_SERVICES]: if config and not config.get(CONF_ACTIONS) and not config[CONF_CUSTOM_SERVICES]:
files_to_filter.append("user_services.cpp") files_to_filter.append("user_services.cpp")
# Filter protocol-specific implementations based on encryption configuration
encryption_config = config.get(CONF_ENCRYPTION) if config else None
# If encryption is not configured at all, we only need plaintext
if encryption_config is None:
files_to_filter.append("api_frame_helper_noise.cpp")
# If encryption is configured with a key, we only need noise
elif encryption_config.get(CONF_KEY):
files_to_filter.append("api_frame_helper_plaintext.cpp")
# If encryption is configured but no key is provided, we need both
# (this allows a plaintext client to provide a noise key)
return files_to_filter return files_to_filter

View File

@ -203,7 +203,7 @@ message DeviceInfoResponse {
option (id) = 10; option (id) = 10;
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
bool uses_password = 1; bool uses_password = 1 [(field_ifdef) = "USE_API_PASSWORD"];
// The name of the node, given by "App.set_name()" // The name of the node, given by "App.set_name()"
string name = 2; string name = 2;
@ -230,14 +230,16 @@ message DeviceInfoResponse {
uint32 webserver_port = 10 [(field_ifdef) = "USE_WEBSERVER"]; uint32 webserver_port = 10 [(field_ifdef) = "USE_WEBSERVER"];
uint32 legacy_bluetooth_proxy_version = 11 [(field_ifdef) = "USE_BLUETOOTH_PROXY"]; // Deprecated in API version 1.9
uint32 legacy_bluetooth_proxy_version = 11 [deprecated=true, (field_ifdef) = "USE_BLUETOOTH_PROXY"];
uint32 bluetooth_proxy_feature_flags = 15 [(field_ifdef) = "USE_BLUETOOTH_PROXY"]; uint32 bluetooth_proxy_feature_flags = 15 [(field_ifdef) = "USE_BLUETOOTH_PROXY"];
string manufacturer = 12; string manufacturer = 12;
string friendly_name = 13; string friendly_name = 13;
uint32 legacy_voice_assistant_version = 14 [(field_ifdef) = "USE_VOICE_ASSISTANT"]; // Deprecated in API version 1.10
uint32 legacy_voice_assistant_version = 14 [deprecated=true, (field_ifdef) = "USE_VOICE_ASSISTANT"];
uint32 voice_assistant_feature_flags = 17 [(field_ifdef) = "USE_VOICE_ASSISTANT"]; uint32 voice_assistant_feature_flags = 17 [(field_ifdef) = "USE_VOICE_ASSISTANT"];
string suggested_area = 16 [(field_ifdef) = "USE_AREAS"]; string suggested_area = 16 [(field_ifdef) = "USE_AREAS"];
@ -337,7 +339,9 @@ message ListEntitiesCoverResponse {
uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"]; uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"];
} }
// Deprecated in API version 1.1
enum LegacyCoverState { enum LegacyCoverState {
option deprecated = true;
LEGACY_COVER_STATE_OPEN = 0; LEGACY_COVER_STATE_OPEN = 0;
LEGACY_COVER_STATE_CLOSED = 1; LEGACY_COVER_STATE_CLOSED = 1;
} }
@ -356,7 +360,8 @@ message CoverStateResponse {
fixed32 key = 1; fixed32 key = 1;
// legacy: state has been removed in 1.13 // legacy: state has been removed in 1.13
// clients/servers must still send/accept it until the next protocol change // clients/servers must still send/accept it until the next protocol change
LegacyCoverState legacy_state = 2; // Deprecated in API version 1.1
LegacyCoverState legacy_state = 2 [deprecated=true];
float position = 3; float position = 3;
float tilt = 4; float tilt = 4;
@ -364,7 +369,9 @@ message CoverStateResponse {
uint32 device_id = 6 [(field_ifdef) = "USE_DEVICES"]; uint32 device_id = 6 [(field_ifdef) = "USE_DEVICES"];
} }
// Deprecated in API version 1.1
enum LegacyCoverCommand { enum LegacyCoverCommand {
option deprecated = true;
LEGACY_COVER_COMMAND_OPEN = 0; LEGACY_COVER_COMMAND_OPEN = 0;
LEGACY_COVER_COMMAND_CLOSE = 1; LEGACY_COVER_COMMAND_CLOSE = 1;
LEGACY_COVER_COMMAND_STOP = 2; LEGACY_COVER_COMMAND_STOP = 2;
@ -380,8 +387,10 @@ message CoverCommandRequest {
// legacy: command has been removed in 1.13 // legacy: command has been removed in 1.13
// clients/servers must still send/accept it until the next protocol change // clients/servers must still send/accept it until the next protocol change
bool has_legacy_command = 2; // Deprecated in API version 1.1
LegacyCoverCommand legacy_command = 3; bool has_legacy_command = 2 [deprecated=true];
// Deprecated in API version 1.1
LegacyCoverCommand legacy_command = 3 [deprecated=true];
bool has_position = 4; bool has_position = 4;
float position = 5; float position = 5;
@ -413,7 +422,9 @@ message ListEntitiesFanResponse {
repeated string supported_preset_modes = 12; repeated string supported_preset_modes = 12;
uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"]; uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"];
} }
// Deprecated in API version 1.6 - only used in deprecated fields
enum FanSpeed { enum FanSpeed {
option deprecated = true;
FAN_SPEED_LOW = 0; FAN_SPEED_LOW = 0;
FAN_SPEED_MEDIUM = 1; FAN_SPEED_MEDIUM = 1;
FAN_SPEED_HIGH = 2; FAN_SPEED_HIGH = 2;
@ -432,7 +443,8 @@ message FanStateResponse {
fixed32 key = 1; fixed32 key = 1;
bool state = 2; bool state = 2;
bool oscillating = 3; bool oscillating = 3;
FanSpeed speed = 4 [deprecated = true]; // Deprecated in API version 1.6
FanSpeed speed = 4 [deprecated=true];
FanDirection direction = 5; FanDirection direction = 5;
int32 speed_level = 6; int32 speed_level = 6;
string preset_mode = 7; string preset_mode = 7;
@ -448,8 +460,10 @@ message FanCommandRequest {
fixed32 key = 1; fixed32 key = 1;
bool has_state = 2; bool has_state = 2;
bool state = 3; bool state = 3;
bool has_speed = 4 [deprecated = true]; // Deprecated in API version 1.6
FanSpeed speed = 5 [deprecated = true]; bool has_speed = 4 [deprecated=true];
// Deprecated in API version 1.6
FanSpeed speed = 5 [deprecated=true];
bool has_oscillating = 6; bool has_oscillating = 6;
bool oscillating = 7; bool oscillating = 7;
bool has_direction = 8; bool has_direction = 8;
@ -488,9 +502,13 @@ message ListEntitiesLightResponse {
repeated ColorMode supported_color_modes = 12; repeated ColorMode supported_color_modes = 12;
// next four supports_* are for legacy clients, newer clients should use color modes // next four supports_* are for legacy clients, newer clients should use color modes
// Deprecated in API version 1.6
bool legacy_supports_brightness = 5 [deprecated=true]; bool legacy_supports_brightness = 5 [deprecated=true];
// Deprecated in API version 1.6
bool legacy_supports_rgb = 6 [deprecated=true]; bool legacy_supports_rgb = 6 [deprecated=true];
// Deprecated in API version 1.6
bool legacy_supports_white_value = 7 [deprecated=true]; bool legacy_supports_white_value = 7 [deprecated=true];
// Deprecated in API version 1.6
bool legacy_supports_color_temperature = 8 [deprecated=true]; bool legacy_supports_color_temperature = 8 [deprecated=true];
float min_mireds = 9; float min_mireds = 9;
float max_mireds = 10; float max_mireds = 10;
@ -567,7 +585,9 @@ enum SensorStateClass {
STATE_CLASS_TOTAL = 3; STATE_CLASS_TOTAL = 3;
} }
// Deprecated in API version 1.5
enum SensorLastResetType { enum SensorLastResetType {
option deprecated = true;
LAST_RESET_NONE = 0; LAST_RESET_NONE = 0;
LAST_RESET_NEVER = 1; LAST_RESET_NEVER = 1;
LAST_RESET_AUTO = 2; LAST_RESET_AUTO = 2;
@ -591,7 +611,8 @@ message ListEntitiesSensorResponse {
string device_class = 9; string device_class = 9;
SensorStateClass state_class = 10; SensorStateClass state_class = 10;
// Last reset type removed in 2021.9.0 // Last reset type removed in 2021.9.0
SensorLastResetType legacy_last_reset_type = 11; // Deprecated in API version 1.5
SensorLastResetType legacy_last_reset_type = 11 [deprecated=true];
bool disabled_by_default = 12; bool disabled_by_default = 12;
EntityCategory entity_category = 13; EntityCategory entity_category = 13;
uint32 device_id = 14 [(field_ifdef) = "USE_DEVICES"]; uint32 device_id = 14 [(field_ifdef) = "USE_DEVICES"];
@ -711,7 +732,6 @@ message SubscribeLogsResponse {
LogLevel level = 1; LogLevel level = 1;
bytes message = 3; bytes message = 3;
bool send_failed = 4;
} }
// ==================== NOISE ENCRYPTION ==================== // ==================== NOISE ENCRYPTION ====================
@ -947,7 +967,8 @@ message ListEntitiesClimateResponse {
float visual_target_temperature_step = 10; float visual_target_temperature_step = 10;
// for older peer versions - in new system this // for older peer versions - in new system this
// is if CLIMATE_PRESET_AWAY exists is supported_presets // is if CLIMATE_PRESET_AWAY exists is supported_presets
bool legacy_supports_away = 11; // Deprecated in API version 1.5
bool legacy_supports_away = 11 [deprecated=true];
bool supports_action = 12; bool supports_action = 12;
repeated ClimateFanMode supported_fan_modes = 13; repeated ClimateFanMode supported_fan_modes = 13;
repeated ClimateSwingMode supported_swing_modes = 14; repeated ClimateSwingMode supported_swing_modes = 14;
@ -978,7 +999,8 @@ message ClimateStateResponse {
float target_temperature_low = 5; float target_temperature_low = 5;
float target_temperature_high = 6; float target_temperature_high = 6;
// For older peers, equal to preset == CLIMATE_PRESET_AWAY // For older peers, equal to preset == CLIMATE_PRESET_AWAY
bool unused_legacy_away = 7; // Deprecated in API version 1.5
bool unused_legacy_away = 7 [deprecated=true];
ClimateAction action = 8; ClimateAction action = 8;
ClimateFanMode fan_mode = 9; ClimateFanMode fan_mode = 9;
ClimateSwingMode swing_mode = 10; ClimateSwingMode swing_mode = 10;
@ -1006,8 +1028,10 @@ message ClimateCommandRequest {
bool has_target_temperature_high = 8; bool has_target_temperature_high = 8;
float target_temperature_high = 9; float target_temperature_high = 9;
// legacy, for older peers, newer ones should use CLIMATE_PRESET_AWAY in preset // legacy, for older peers, newer ones should use CLIMATE_PRESET_AWAY in preset
bool unused_has_legacy_away = 10; // Deprecated in API version 1.5
bool unused_legacy_away = 11; bool unused_has_legacy_away = 10 [deprecated=true];
// Deprecated in API version 1.5
bool unused_legacy_away = 11 [deprecated=true];
bool has_fan_mode = 12; bool has_fan_mode = 12;
ClimateFanMode fan_mode = 13; ClimateFanMode fan_mode = 13;
bool has_swing_mode = 14; bool has_swing_mode = 14;
@ -1354,12 +1378,17 @@ message SubscribeBluetoothLEAdvertisementsRequest {
uint32 flags = 1; uint32 flags = 1;
} }
// Deprecated - only used by deprecated BluetoothLEAdvertisementResponse
message BluetoothServiceData { message BluetoothServiceData {
option deprecated = true;
string uuid = 1; string uuid = 1;
repeated uint32 legacy_data = 2 [deprecated = true]; // Removed in api version 1.7 // Deprecated in API version 1.7
repeated uint32 legacy_data = 2 [deprecated=true]; // Removed in api version 1.7
bytes data = 3; // Added in api version 1.7 bytes data = 3; // Added in api version 1.7
} }
// Removed in ESPHome 2025.8.0 - use BluetoothLERawAdvertisementsResponse instead
message BluetoothLEAdvertisementResponse { message BluetoothLEAdvertisementResponse {
option deprecated = true;
option (id) = 67; option (id) = 67;
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (ifdef) = "USE_BLUETOOTH_PROXY"; option (ifdef) = "USE_BLUETOOTH_PROXY";
@ -1434,19 +1463,19 @@ message BluetoothGATTGetServicesRequest {
} }
message BluetoothGATTDescriptor { message BluetoothGATTDescriptor {
repeated uint64 uuid = 1; repeated uint64 uuid = 1 [(fixed_array_size) = 2];
uint32 handle = 2; uint32 handle = 2;
} }
message BluetoothGATTCharacteristic { message BluetoothGATTCharacteristic {
repeated uint64 uuid = 1; repeated uint64 uuid = 1 [(fixed_array_size) = 2];
uint32 handle = 2; uint32 handle = 2;
uint32 properties = 3; uint32 properties = 3;
repeated BluetoothGATTDescriptor descriptors = 4; repeated BluetoothGATTDescriptor descriptors = 4;
} }
message BluetoothGATTService { message BluetoothGATTService {
repeated uint64 uuid = 1; repeated uint64 uuid = 1 [(fixed_array_size) = 2];
uint32 handle = 2; uint32 handle = 2;
repeated BluetoothGATTCharacteristic characteristics = 3; repeated BluetoothGATTCharacteristic characteristics = 3;
} }
@ -1457,7 +1486,7 @@ message BluetoothGATTGetServicesResponse {
option (ifdef) = "USE_BLUETOOTH_PROXY"; option (ifdef) = "USE_BLUETOOTH_PROXY";
uint64 address = 1; uint64 address = 1;
repeated BluetoothGATTService services = 2; repeated BluetoothGATTService services = 2 [(fixed_array_size) = 1];
} }
message BluetoothGATTGetServicesDoneResponse { message BluetoothGATTGetServicesDoneResponse {

View File

@ -1,5 +1,11 @@
#include "api_connection.h" #include "api_connection.h"
#ifdef USE_API #ifdef USE_API
#ifdef USE_API_NOISE
#include "api_frame_helper_noise.h"
#endif
#ifdef USE_API_PLAINTEXT
#include "api_frame_helper_plaintext.h"
#endif
#include <cerrno> #include <cerrno>
#include <cinttypes> #include <cinttypes>
#include <utility> #include <utility>
@ -25,8 +31,7 @@
#include "esphome/components/voice_assistant/voice_assistant.h" #include "esphome/components/voice_assistant/voice_assistant.h"
#endif #endif
namespace esphome { namespace esphome::api {
namespace api {
// Read a maximum of 5 messages per loop iteration to prevent starving other components. // Read a maximum of 5 messages per loop iteration to prevent starving other components.
// This is a balance between API responsiveness and allowing other components to run. // This is a balance between API responsiveness and allowing other components to run.
@ -79,14 +84,16 @@ APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *pa
#if defined(USE_API_PLAINTEXT) && defined(USE_API_NOISE) #if defined(USE_API_PLAINTEXT) && defined(USE_API_NOISE)
auto noise_ctx = parent->get_noise_ctx(); auto noise_ctx = parent->get_noise_ctx();
if (noise_ctx->has_psk()) { if (noise_ctx->has_psk()) {
this->helper_ = std::unique_ptr<APIFrameHelper>{new APINoiseFrameHelper(std::move(sock), noise_ctx)}; this->helper_ =
std::unique_ptr<APIFrameHelper>{new APINoiseFrameHelper(std::move(sock), noise_ctx, &this->client_info_)};
} else { } else {
this->helper_ = std::unique_ptr<APIFrameHelper>{new APIPlaintextFrameHelper(std::move(sock))}; this->helper_ = std::unique_ptr<APIFrameHelper>{new APIPlaintextFrameHelper(std::move(sock), &this->client_info_)};
} }
#elif defined(USE_API_PLAINTEXT) #elif defined(USE_API_PLAINTEXT)
this->helper_ = std::unique_ptr<APIFrameHelper>{new APIPlaintextFrameHelper(std::move(sock))}; this->helper_ = std::unique_ptr<APIFrameHelper>{new APIPlaintextFrameHelper(std::move(sock), &this->client_info_)};
#elif defined(USE_API_NOISE) #elif defined(USE_API_NOISE)
this->helper_ = std::unique_ptr<APIFrameHelper>{new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx())}; this->helper_ = std::unique_ptr<APIFrameHelper>{
new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx(), &this->client_info_)};
#else #else
#error "No frame helper defined" #error "No frame helper defined"
#endif #endif
@ -109,9 +116,8 @@ void APIConnection::start() {
errno); errno);
return; return;
} }
this->client_info_ = helper_->getpeername(); this->client_info_.peername = helper_->getpeername();
this->client_peername_ = this->client_info_; this->client_info_.name = this->client_info_.peername;
this->helper_->set_log_info(this->client_info_);
} }
APIConnection::~APIConnection() { APIConnection::~APIConnection() {
@ -218,24 +224,16 @@ void APIConnection::loop() {
if (this->image_reader_ && this->image_reader_->available() && this->helper_->can_write_without_blocking()) { if (this->image_reader_ && this->image_reader_->available() && this->helper_->can_write_without_blocking()) {
uint32_t to_send = std::min((size_t) MAX_BATCH_PACKET_SIZE, this->image_reader_->available()); uint32_t to_send = std::min((size_t) MAX_BATCH_PACKET_SIZE, this->image_reader_->available());
bool done = this->image_reader_->available() == to_send; bool done = this->image_reader_->available() == to_send;
uint32_t msg_size = 0;
ProtoSize::add_fixed_field<4>(msg_size, 1, true);
// partial message size calculated manually since its a special case
// 1 for the data field, varint for the data size, and the data itself
msg_size += 1 + ProtoSize::varint(to_send) + to_send;
ProtoSize::add_bool_field(msg_size, 1, done);
auto buffer = this->create_buffer(msg_size); CameraImageResponse msg;
// fixed32 key = 1; msg.key = camera::Camera::instance()->get_object_id_hash();
buffer.encode_fixed32(1, camera::Camera::instance()->get_object_id_hash()); msg.set_data(this->image_reader_->peek_data_buffer(), to_send);
// bytes data = 2; msg.done = done;
buffer.encode_bytes(2, this->image_reader_->peek_data_buffer(), to_send); #ifdef USE_DEVICES
// bool done = 3; msg.device_id = camera::Camera::instance()->get_device_id();
buffer.encode_bool(3, done); #endif
bool success = this->send_buffer(buffer, CameraImageResponse::MESSAGE_TYPE); if (this->send_message_(msg, CameraImageResponse::MESSAGE_TYPE)) {
if (success) {
this->image_reader_->consume_data(to_send); this->image_reader_->consume_data(to_send);
if (done) { if (done) {
this->image_reader_->return_image(); this->image_reader_->return_image();
@ -249,8 +247,10 @@ void APIConnection::loop() {
if (state_subs_at_ < static_cast<int>(subs.size())) { if (state_subs_at_ < static_cast<int>(subs.size())) {
auto &it = subs[state_subs_at_]; auto &it = subs[state_subs_at_];
SubscribeHomeAssistantStateResponse resp; SubscribeHomeAssistantStateResponse resp;
resp.entity_id = it.entity_id; resp.set_entity_id(StringRef(it.entity_id));
resp.attribute = it.attribute.value(); // attribute.value() returns temporary - must store it
std::string attribute_value = it.attribute.value();
resp.set_attribute(StringRef(attribute_value));
resp.once = it.once; resp.once = it.once;
if (this->send_message(resp, SubscribeHomeAssistantStateResponse::MESSAGE_TYPE)) { if (this->send_message(resp, SubscribeHomeAssistantStateResponse::MESSAGE_TYPE)) {
state_subs_at_++; state_subs_at_++;
@ -261,14 +261,14 @@ void APIConnection::loop() {
} }
} }
DisconnectResponse APIConnection::disconnect(const DisconnectRequest &msg) { bool APIConnection::send_disconnect_response(const DisconnectRequest &msg) {
// remote initiated disconnect_client // remote initiated disconnect_client
// don't close yet, we still need to send the disconnect response // don't close yet, we still need to send the disconnect response
// close will happen on next loop // close will happen on next loop
ESP_LOGD(TAG, "%s disconnected", this->get_client_combined_info().c_str()); ESP_LOGD(TAG, "%s disconnected", this->get_client_combined_info().c_str());
this->flags_.next_close = true; this->flags_.next_close = true;
DisconnectResponse resp; DisconnectResponse resp;
return resp; return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE);
} }
void APIConnection::on_disconnect_response(const DisconnectResponse &value) { void APIConnection::on_disconnect_response(const DisconnectResponse &value) {
this->helper_->close(); this->helper_->close();
@ -345,7 +345,7 @@ uint16_t APIConnection::try_send_binary_sensor_info(EntityBase *entity, APIConne
bool is_single) { bool is_single) {
auto *binary_sensor = static_cast<binary_sensor::BinarySensor *>(entity); auto *binary_sensor = static_cast<binary_sensor::BinarySensor *>(entity);
ListEntitiesBinarySensorResponse msg; ListEntitiesBinarySensorResponse msg;
msg.device_class = binary_sensor->get_device_class(); msg.set_device_class(binary_sensor->get_device_class_ref());
msg.is_status_binary_sensor = binary_sensor->is_status_binary_sensor(); msg.is_status_binary_sensor = binary_sensor->is_status_binary_sensor();
return fill_and_encode_entity_info(binary_sensor, msg, ListEntitiesBinarySensorResponse::MESSAGE_TYPE, conn, return fill_and_encode_entity_info(binary_sensor, msg, ListEntitiesBinarySensorResponse::MESSAGE_TYPE, conn,
remaining_size, is_single); remaining_size, is_single);
@ -362,8 +362,6 @@ uint16_t APIConnection::try_send_cover_state(EntityBase *entity, APIConnection *
auto *cover = static_cast<cover::Cover *>(entity); auto *cover = static_cast<cover::Cover *>(entity);
CoverStateResponse msg; CoverStateResponse msg;
auto traits = cover->get_traits(); auto traits = cover->get_traits();
msg.legacy_state =
(cover->position == cover::COVER_OPEN) ? enums::LEGACY_COVER_STATE_OPEN : enums::LEGACY_COVER_STATE_CLOSED;
msg.position = cover->position; msg.position = cover->position;
if (traits.get_supports_tilt()) if (traits.get_supports_tilt())
msg.tilt = cover->tilt; msg.tilt = cover->tilt;
@ -379,25 +377,12 @@ uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *c
msg.supports_position = traits.get_supports_position(); msg.supports_position = traits.get_supports_position();
msg.supports_tilt = traits.get_supports_tilt(); msg.supports_tilt = traits.get_supports_tilt();
msg.supports_stop = traits.get_supports_stop(); msg.supports_stop = traits.get_supports_stop();
msg.device_class = cover->get_device_class(); msg.set_device_class(cover->get_device_class_ref());
return fill_and_encode_entity_info(cover, msg, ListEntitiesCoverResponse::MESSAGE_TYPE, conn, remaining_size, return fill_and_encode_entity_info(cover, msg, ListEntitiesCoverResponse::MESSAGE_TYPE, conn, remaining_size,
is_single); is_single);
} }
void APIConnection::cover_command(const CoverCommandRequest &msg) { void APIConnection::cover_command(const CoverCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(cover::Cover, cover, cover) ENTITY_COMMAND_MAKE_CALL(cover::Cover, cover, cover)
if (msg.has_legacy_command) {
switch (msg.legacy_command) {
case enums::LEGACY_COVER_COMMAND_OPEN:
call.set_command_open();
break;
case enums::LEGACY_COVER_COMMAND_CLOSE:
call.set_command_close();
break;
case enums::LEGACY_COVER_COMMAND_STOP:
call.set_command_stop();
break;
}
}
if (msg.has_position) if (msg.has_position)
call.set_position(msg.position); call.set_position(msg.position);
if (msg.has_tilt) if (msg.has_tilt)
@ -427,7 +412,7 @@ uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *co
if (traits.supports_direction()) if (traits.supports_direction())
msg.direction = static_cast<enums::FanDirection>(fan->direction); msg.direction = static_cast<enums::FanDirection>(fan->direction);
if (traits.supports_preset_modes()) if (traits.supports_preset_modes())
msg.preset_mode = fan->preset_mode; msg.set_preset_mode(StringRef(fan->preset_mode));
return fill_and_encode_entity_state(fan, msg, FanStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return fill_and_encode_entity_state(fan, msg, FanStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@ -484,8 +469,11 @@ uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection *
resp.color_temperature = values.get_color_temperature(); resp.color_temperature = values.get_color_temperature();
resp.cold_white = values.get_cold_white(); resp.cold_white = values.get_cold_white();
resp.warm_white = values.get_warm_white(); resp.warm_white = values.get_warm_white();
if (light->supports_effects()) if (light->supports_effects()) {
resp.effect = light->get_effect_name(); // get_effect_name() returns temporary std::string - must store it
std::string effect_name = light->get_effect_name();
resp.set_effect(StringRef(effect_name));
}
return fill_and_encode_entity_state(light, resp, LightStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return fill_and_encode_entity_state(light, resp, LightStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@ -495,14 +483,8 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c
auto traits = light->get_traits(); auto traits = light->get_traits();
for (auto mode : traits.get_supported_color_modes()) for (auto mode : traits.get_supported_color_modes())
msg.supported_color_modes.push_back(static_cast<enums::ColorMode>(mode)); msg.supported_color_modes.push_back(static_cast<enums::ColorMode>(mode));
msg.legacy_supports_brightness = traits.supports_color_capability(light::ColorCapability::BRIGHTNESS); if (traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) ||
msg.legacy_supports_rgb = traits.supports_color_capability(light::ColorCapability::RGB); traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE)) {
msg.legacy_supports_white_value =
msg.legacy_supports_rgb && (traits.supports_color_capability(light::ColorCapability::WHITE) ||
traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE));
msg.legacy_supports_color_temperature = traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) ||
traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE);
if (msg.legacy_supports_color_temperature) {
msg.min_mireds = traits.get_min_mireds(); msg.min_mireds = traits.get_min_mireds();
msg.max_mireds = traits.get_max_mireds(); msg.max_mireds = traits.get_max_mireds();
} }
@ -567,10 +549,10 @@ uint16_t APIConnection::try_send_sensor_info(EntityBase *entity, APIConnection *
bool is_single) { bool is_single) {
auto *sensor = static_cast<sensor::Sensor *>(entity); auto *sensor = static_cast<sensor::Sensor *>(entity);
ListEntitiesSensorResponse msg; ListEntitiesSensorResponse msg;
msg.unit_of_measurement = sensor->get_unit_of_measurement(); msg.set_unit_of_measurement(sensor->get_unit_of_measurement_ref());
msg.accuracy_decimals = sensor->get_accuracy_decimals(); msg.accuracy_decimals = sensor->get_accuracy_decimals();
msg.force_update = sensor->get_force_update(); msg.force_update = sensor->get_force_update();
msg.device_class = sensor->get_device_class(); msg.set_device_class(sensor->get_device_class_ref());
msg.state_class = static_cast<enums::SensorStateClass>(sensor->get_state_class()); msg.state_class = static_cast<enums::SensorStateClass>(sensor->get_state_class());
return fill_and_encode_entity_info(sensor, msg, ListEntitiesSensorResponse::MESSAGE_TYPE, conn, remaining_size, return fill_and_encode_entity_info(sensor, msg, ListEntitiesSensorResponse::MESSAGE_TYPE, conn, remaining_size,
is_single); is_single);
@ -597,7 +579,7 @@ uint16_t APIConnection::try_send_switch_info(EntityBase *entity, APIConnection *
auto *a_switch = static_cast<switch_::Switch *>(entity); auto *a_switch = static_cast<switch_::Switch *>(entity);
ListEntitiesSwitchResponse msg; ListEntitiesSwitchResponse msg;
msg.assumed_state = a_switch->assumed_state(); msg.assumed_state = a_switch->assumed_state();
msg.device_class = a_switch->get_device_class(); msg.set_device_class(a_switch->get_device_class_ref());
return fill_and_encode_entity_info(a_switch, msg, ListEntitiesSwitchResponse::MESSAGE_TYPE, conn, remaining_size, return fill_and_encode_entity_info(a_switch, msg, ListEntitiesSwitchResponse::MESSAGE_TYPE, conn, remaining_size,
is_single); is_single);
} }
@ -622,7 +604,7 @@ uint16_t APIConnection::try_send_text_sensor_state(EntityBase *entity, APIConnec
bool is_single) { bool is_single) {
auto *text_sensor = static_cast<text_sensor::TextSensor *>(entity); auto *text_sensor = static_cast<text_sensor::TextSensor *>(entity);
TextSensorStateResponse resp; TextSensorStateResponse resp;
resp.state = text_sensor->state; resp.set_state(StringRef(text_sensor->state));
resp.missing_state = !text_sensor->has_state(); resp.missing_state = !text_sensor->has_state();
return fill_and_encode_entity_state(text_sensor, resp, TextSensorStateResponse::MESSAGE_TYPE, conn, remaining_size, return fill_and_encode_entity_state(text_sensor, resp, TextSensorStateResponse::MESSAGE_TYPE, conn, remaining_size,
is_single); is_single);
@ -631,7 +613,7 @@ uint16_t APIConnection::try_send_text_sensor_info(EntityBase *entity, APIConnect
bool is_single) { bool is_single) {
auto *text_sensor = static_cast<text_sensor::TextSensor *>(entity); auto *text_sensor = static_cast<text_sensor::TextSensor *>(entity);
ListEntitiesTextSensorResponse msg; ListEntitiesTextSensorResponse msg;
msg.device_class = text_sensor->get_device_class(); msg.set_device_class(text_sensor->get_device_class_ref());
return fill_and_encode_entity_info(text_sensor, msg, ListEntitiesTextSensorResponse::MESSAGE_TYPE, conn, return fill_and_encode_entity_info(text_sensor, msg, ListEntitiesTextSensorResponse::MESSAGE_TYPE, conn,
remaining_size, is_single); remaining_size, is_single);
} }
@ -659,13 +641,19 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection
} }
if (traits.get_supports_fan_modes() && climate->fan_mode.has_value()) if (traits.get_supports_fan_modes() && climate->fan_mode.has_value())
resp.fan_mode = static_cast<enums::ClimateFanMode>(climate->fan_mode.value()); resp.fan_mode = static_cast<enums::ClimateFanMode>(climate->fan_mode.value());
if (!traits.get_supported_custom_fan_modes().empty() && climate->custom_fan_mode.has_value()) if (!traits.get_supported_custom_fan_modes().empty() && climate->custom_fan_mode.has_value()) {
resp.custom_fan_mode = climate->custom_fan_mode.value(); // custom_fan_mode.value() returns temporary - must store it
std::string custom_fan_mode = climate->custom_fan_mode.value();
resp.set_custom_fan_mode(StringRef(custom_fan_mode));
}
if (traits.get_supports_presets() && climate->preset.has_value()) { if (traits.get_supports_presets() && climate->preset.has_value()) {
resp.preset = static_cast<enums::ClimatePreset>(climate->preset.value()); resp.preset = static_cast<enums::ClimatePreset>(climate->preset.value());
} }
if (!traits.get_supported_custom_presets().empty() && climate->custom_preset.has_value()) if (!traits.get_supported_custom_presets().empty() && climate->custom_preset.has_value()) {
resp.custom_preset = climate->custom_preset.value(); // custom_preset.value() returns temporary - must store it
std::string custom_preset = climate->custom_preset.value();
resp.set_custom_preset(StringRef(custom_preset));
}
if (traits.get_supports_swing_modes()) if (traits.get_supports_swing_modes())
resp.swing_mode = static_cast<enums::ClimateSwingMode>(climate->swing_mode); resp.swing_mode = static_cast<enums::ClimateSwingMode>(climate->swing_mode);
if (traits.get_supports_current_humidity()) if (traits.get_supports_current_humidity())
@ -692,7 +680,6 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection
msg.visual_current_temperature_step = traits.get_visual_current_temperature_step(); msg.visual_current_temperature_step = traits.get_visual_current_temperature_step();
msg.visual_min_humidity = traits.get_visual_min_humidity(); msg.visual_min_humidity = traits.get_visual_min_humidity();
msg.visual_max_humidity = traits.get_visual_max_humidity(); msg.visual_max_humidity = traits.get_visual_max_humidity();
msg.legacy_supports_away = traits.supports_preset(climate::CLIMATE_PRESET_AWAY);
msg.supports_action = traits.get_supports_action(); msg.supports_action = traits.get_supports_action();
for (auto fan_mode : traits.get_supported_fan_modes()) for (auto fan_mode : traits.get_supported_fan_modes())
msg.supported_fan_modes.push_back(static_cast<enums::ClimateFanMode>(fan_mode)); msg.supported_fan_modes.push_back(static_cast<enums::ClimateFanMode>(fan_mode));
@ -752,9 +739,9 @@ uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection *
bool is_single) { bool is_single) {
auto *number = static_cast<number::Number *>(entity); auto *number = static_cast<number::Number *>(entity);
ListEntitiesNumberResponse msg; ListEntitiesNumberResponse msg;
msg.unit_of_measurement = number->traits.get_unit_of_measurement(); msg.set_unit_of_measurement(number->traits.get_unit_of_measurement_ref());
msg.mode = static_cast<enums::NumberMode>(number->traits.get_mode()); msg.mode = static_cast<enums::NumberMode>(number->traits.get_mode());
msg.device_class = number->traits.get_device_class(); msg.set_device_class(number->traits.get_device_class_ref());
msg.min_value = number->traits.get_min_value(); msg.min_value = number->traits.get_min_value();
msg.max_value = number->traits.get_max_value(); msg.max_value = number->traits.get_max_value();
msg.step = number->traits.get_step(); msg.step = number->traits.get_step();
@ -867,7 +854,7 @@ uint16_t APIConnection::try_send_text_state(EntityBase *entity, APIConnection *c
bool is_single) { bool is_single) {
auto *text = static_cast<text::Text *>(entity); auto *text = static_cast<text::Text *>(entity);
TextStateResponse resp; TextStateResponse resp;
resp.state = text->state; resp.set_state(StringRef(text->state));
resp.missing_state = !text->has_state(); resp.missing_state = !text->has_state();
return fill_and_encode_entity_state(text, resp, TextStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return fill_and_encode_entity_state(text, resp, TextStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
@ -879,7 +866,7 @@ uint16_t APIConnection::try_send_text_info(EntityBase *entity, APIConnection *co
msg.mode = static_cast<enums::TextMode>(text->traits.get_mode()); msg.mode = static_cast<enums::TextMode>(text->traits.get_mode());
msg.min_length = text->traits.get_min_length(); msg.min_length = text->traits.get_min_length();
msg.max_length = text->traits.get_max_length(); msg.max_length = text->traits.get_max_length();
msg.pattern = text->traits.get_pattern(); msg.set_pattern(text->traits.get_pattern_ref());
return fill_and_encode_entity_info(text, msg, ListEntitiesTextResponse::MESSAGE_TYPE, conn, remaining_size, return fill_and_encode_entity_info(text, msg, ListEntitiesTextResponse::MESSAGE_TYPE, conn, remaining_size,
is_single); is_single);
} }
@ -900,7 +887,7 @@ uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection
bool is_single) { bool is_single) {
auto *select = static_cast<select::Select *>(entity); auto *select = static_cast<select::Select *>(entity);
SelectStateResponse resp; SelectStateResponse resp;
resp.state = select->state; resp.set_state(StringRef(select->state));
resp.missing_state = !select->has_state(); resp.missing_state = !select->has_state();
return fill_and_encode_entity_state(select, resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return fill_and_encode_entity_state(select, resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
@ -926,7 +913,7 @@ uint16_t APIConnection::try_send_button_info(EntityBase *entity, APIConnection *
bool is_single) { bool is_single) {
auto *button = static_cast<button::Button *>(entity); auto *button = static_cast<button::Button *>(entity);
ListEntitiesButtonResponse msg; ListEntitiesButtonResponse msg;
msg.device_class = button->get_device_class(); msg.set_device_class(button->get_device_class_ref());
return fill_and_encode_entity_info(button, msg, ListEntitiesButtonResponse::MESSAGE_TYPE, conn, remaining_size, return fill_and_encode_entity_info(button, msg, ListEntitiesButtonResponse::MESSAGE_TYPE, conn, remaining_size,
is_single); is_single);
} }
@ -995,7 +982,7 @@ uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *c
auto *valve = static_cast<valve::Valve *>(entity); auto *valve = static_cast<valve::Valve *>(entity);
ListEntitiesValveResponse msg; ListEntitiesValveResponse msg;
auto traits = valve->get_traits(); auto traits = valve->get_traits();
msg.device_class = valve->get_device_class(); msg.set_device_class(valve->get_device_class_ref());
msg.assumed_state = traits.get_is_assumed_state(); msg.assumed_state = traits.get_is_assumed_state();
msg.supports_position = traits.get_supports_position(); msg.supports_position = traits.get_supports_position();
msg.supports_stop = traits.get_supports_stop(); msg.supports_stop = traits.get_supports_stop();
@ -1037,13 +1024,13 @@ uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnec
auto traits = media_player->get_traits(); auto traits = media_player->get_traits();
msg.supports_pause = traits.get_supports_pause(); msg.supports_pause = traits.get_supports_pause();
for (auto &supported_format : traits.get_supported_formats()) { for (auto &supported_format : traits.get_supported_formats()) {
MediaPlayerSupportedFormat media_format; msg.supported_formats.emplace_back();
media_format.format = supported_format.format; auto &media_format = msg.supported_formats.back();
media_format.set_format(StringRef(supported_format.format));
media_format.sample_rate = supported_format.sample_rate; media_format.sample_rate = supported_format.sample_rate;
media_format.num_channels = supported_format.num_channels; media_format.num_channels = supported_format.num_channels;
media_format.purpose = static_cast<enums::MediaPlayerFormatPurpose>(supported_format.purpose); media_format.purpose = static_cast<enums::MediaPlayerFormatPurpose>(supported_format.purpose);
media_format.sample_bytes = supported_format.sample_bytes; media_format.sample_bytes = supported_format.sample_bytes;
msg.supported_formats.push_back(media_format);
} }
return fill_and_encode_entity_info(media_player, msg, ListEntitiesMediaPlayerResponse::MESSAGE_TYPE, conn, return fill_and_encode_entity_info(media_player, msg, ListEntitiesMediaPlayerResponse::MESSAGE_TYPE, conn,
remaining_size, is_single); remaining_size, is_single);
@ -1106,6 +1093,12 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) {
} }
#endif #endif
bool APIConnection::send_get_time_response(const GetTimeRequest &msg) {
GetTimeResponse resp;
resp.epoch_seconds = ::time(nullptr);
return this->send_message(resp, GetTimeResponse::MESSAGE_TYPE);
}
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
void APIConnection::subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) { void APIConnection::subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) {
bluetooth_proxy::global_bluetooth_proxy->subscribe_api_connection(this, msg.flags); bluetooth_proxy::global_bluetooth_proxy->subscribe_api_connection(this, msg.flags);
@ -1113,21 +1106,6 @@ void APIConnection::subscribe_bluetooth_le_advertisements(const SubscribeBluetoo
void APIConnection::unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) { void APIConnection::unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) {
bluetooth_proxy::global_bluetooth_proxy->unsubscribe_api_connection(this); bluetooth_proxy::global_bluetooth_proxy->unsubscribe_api_connection(this);
} }
bool APIConnection::send_bluetooth_le_advertisement(const BluetoothLEAdvertisementResponse &msg) {
if (this->client_api_version_major_ < 1 || this->client_api_version_minor_ < 7) {
BluetoothLEAdvertisementResponse resp = msg;
for (auto &service : resp.service_data) {
service.legacy_data.assign(service.data.begin(), service.data.end());
service.data.clear();
}
for (auto &manufacturer_data : resp.manufacturer_data) {
manufacturer_data.legacy_data.assign(manufacturer_data.data.begin(), manufacturer_data.data.end());
manufacturer_data.data.clear();
}
return this->send_message(resp, BluetoothLEAdvertisementResponse::MESSAGE_TYPE);
}
return this->send_message(msg, BluetoothLEAdvertisementResponse::MESSAGE_TYPE);
}
void APIConnection::bluetooth_device_request(const BluetoothDeviceRequest &msg) { void APIConnection::bluetooth_device_request(const BluetoothDeviceRequest &msg) {
bluetooth_proxy::global_bluetooth_proxy->bluetooth_device_request(msg); bluetooth_proxy::global_bluetooth_proxy->bluetooth_device_request(msg);
} }
@ -1151,12 +1129,12 @@ void APIConnection::bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg)
bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_notify(msg); bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_notify(msg);
} }
BluetoothConnectionsFreeResponse APIConnection::subscribe_bluetooth_connections_free( bool APIConnection::send_subscribe_bluetooth_connections_free_response(
const SubscribeBluetoothConnectionsFreeRequest &msg) { const SubscribeBluetoothConnectionsFreeRequest &msg) {
BluetoothConnectionsFreeResponse resp; BluetoothConnectionsFreeResponse resp;
resp.free = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_connections_free(); resp.free = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_connections_free();
resp.limit = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_connections_limit(); resp.limit = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_connections_limit();
return resp; return this->send_message(resp, BluetoothConnectionsFreeResponse::MESSAGE_TYPE);
} }
void APIConnection::bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) { void APIConnection::bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) {
@ -1217,28 +1195,27 @@ void APIConnection::on_voice_assistant_announce_request(const VoiceAssistantAnno
} }
} }
VoiceAssistantConfigurationResponse APIConnection::voice_assistant_get_configuration( bool APIConnection::send_voice_assistant_get_configuration_response(const VoiceAssistantConfigurationRequest &msg) {
const VoiceAssistantConfigurationRequest &msg) {
VoiceAssistantConfigurationResponse resp; VoiceAssistantConfigurationResponse resp;
if (!this->check_voice_assistant_api_connection_()) { if (!this->check_voice_assistant_api_connection_()) {
return resp; return this->send_message(resp, VoiceAssistantConfigurationResponse::MESSAGE_TYPE);
} }
auto &config = voice_assistant::global_voice_assistant->get_configuration(); auto &config = voice_assistant::global_voice_assistant->get_configuration();
for (auto &wake_word : config.available_wake_words) { for (auto &wake_word : config.available_wake_words) {
VoiceAssistantWakeWord resp_wake_word; resp.available_wake_words.emplace_back();
resp_wake_word.id = wake_word.id; auto &resp_wake_word = resp.available_wake_words.back();
resp_wake_word.wake_word = wake_word.wake_word; resp_wake_word.set_id(StringRef(wake_word.id));
resp_wake_word.set_wake_word(StringRef(wake_word.wake_word));
for (const auto &lang : wake_word.trained_languages) { for (const auto &lang : wake_word.trained_languages) {
resp_wake_word.trained_languages.push_back(lang); resp_wake_word.trained_languages.push_back(lang);
} }
resp.available_wake_words.push_back(std::move(resp_wake_word));
} }
for (auto &wake_word_id : config.active_wake_words) { for (auto &wake_word_id : config.active_wake_words) {
resp.active_wake_words.push_back(wake_word_id); resp.active_wake_words.push_back(wake_word_id);
} }
resp.max_active_wake_words = config.max_active_wake_words; resp.max_active_wake_words = config.max_active_wake_words;
return resp; return this->send_message(resp, VoiceAssistantConfigurationResponse::MESSAGE_TYPE);
} }
void APIConnection::voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) { void APIConnection::voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) {
@ -1311,7 +1288,7 @@ void APIConnection::send_event(event::Event *event, const std::string &event_typ
uint16_t APIConnection::try_send_event_response(event::Event *event, const std::string &event_type, APIConnection *conn, uint16_t APIConnection::try_send_event_response(event::Event *event, const std::string &event_type, APIConnection *conn,
uint32_t remaining_size, bool is_single) { uint32_t remaining_size, bool is_single) {
EventResponse resp; EventResponse resp;
resp.event_type = event_type; resp.set_event_type(StringRef(event_type));
return fill_and_encode_entity_state(event, resp, EventResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return fill_and_encode_entity_state(event, resp, EventResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
@ -1319,7 +1296,7 @@ uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *c
bool is_single) { bool is_single) {
auto *event = static_cast<event::Event *>(entity); auto *event = static_cast<event::Event *>(entity);
ListEntitiesEventResponse msg; ListEntitiesEventResponse msg;
msg.device_class = event->get_device_class(); msg.set_device_class(event->get_device_class_ref());
for (const auto &event_type : event->get_event_types()) for (const auto &event_type : event->get_event_types())
msg.event_types.push_back(event_type); msg.event_types.push_back(event_type);
return fill_and_encode_entity_info(event, msg, ListEntitiesEventResponse::MESSAGE_TYPE, conn, remaining_size, return fill_and_encode_entity_info(event, msg, ListEntitiesEventResponse::MESSAGE_TYPE, conn, remaining_size,
@ -1343,11 +1320,11 @@ uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection
resp.has_progress = true; resp.has_progress = true;
resp.progress = update->update_info.progress; resp.progress = update->update_info.progress;
} }
resp.current_version = update->update_info.current_version; resp.set_current_version(StringRef(update->update_info.current_version));
resp.latest_version = update->update_info.latest_version; resp.set_latest_version(StringRef(update->update_info.latest_version));
resp.title = update->update_info.title; resp.set_title(StringRef(update->update_info.title));
resp.release_summary = update->update_info.summary; resp.set_release_summary(StringRef(update->update_info.summary));
resp.release_url = update->update_info.release_url; resp.set_release_url(StringRef(update->update_info.release_url));
} }
return fill_and_encode_entity_state(update, resp, UpdateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return fill_and_encode_entity_state(update, resp, UpdateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
@ -1355,7 +1332,7 @@ uint16_t APIConnection::try_send_update_info(EntityBase *entity, APIConnection *
bool is_single) { bool is_single) {
auto *update = static_cast<update::UpdateEntity *>(entity); auto *update = static_cast<update::UpdateEntity *>(entity);
ListEntitiesUpdateResponse msg; ListEntitiesUpdateResponse msg;
msg.device_class = update->get_device_class(); msg.set_device_class(update->get_device_class_ref());
return fill_and_encode_entity_info(update, msg, ListEntitiesUpdateResponse::MESSAGE_TYPE, conn, remaining_size, return fill_and_encode_entity_info(update, msg, ListEntitiesUpdateResponse::MESSAGE_TYPE, conn, remaining_size,
is_single); is_single);
} }
@ -1380,26 +1357,10 @@ void APIConnection::update_command(const UpdateCommandRequest &msg) {
#endif #endif
bool APIConnection::try_send_log_message(int level, const char *tag, const char *line, size_t message_len) { bool APIConnection::try_send_log_message(int level, const char *tag, const char *line, size_t message_len) {
// Pre-calculate message size to avoid reallocations SubscribeLogsResponse msg;
uint32_t msg_size = 0; msg.level = static_cast<enums::LogLevel>(level);
msg.set_message(reinterpret_cast<const uint8_t *>(line), message_len);
// Add size for level field (field ID 1, varint type) return this->send_message_(msg, SubscribeLogsResponse::MESSAGE_TYPE);
// 1 byte for field tag + size of the level varint
msg_size += 1 + api::ProtoSize::varint(static_cast<uint32_t>(level));
// Add size for string field (field ID 3, string type)
// 1 byte for field tag + size of length varint + string length
msg_size += 1 + api::ProtoSize::varint(static_cast<uint32_t>(message_len)) + message_len;
// Create a pre-sized buffer
auto buffer = this->create_buffer(msg_size);
// Encode the message (SubscribeLogsResponse)
buffer.encode_uint32(1, static_cast<uint32_t>(level)); // LogLevel level = 1
buffer.encode_string(3, line, message_len); // string message = 3
// SubscribeLogsResponse - 29
return this->send_buffer(buffer, SubscribeLogsResponse::MESSAGE_TYPE);
} }
void APIConnection::complete_authentication_() { void APIConnection::complete_authentication_() {
@ -1411,7 +1372,7 @@ void APIConnection::complete_authentication_() {
this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::AUTHENTICATED); this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::AUTHENTICATED);
ESP_LOGD(TAG, "%s connected", this->get_client_combined_info().c_str()); ESP_LOGD(TAG, "%s connected", this->get_client_combined_info().c_str());
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER #ifdef USE_API_CLIENT_CONNECTED_TRIGGER
this->parent_->get_client_connected_trigger()->trigger(this->client_info_, this->client_peername_); this->parent_->get_client_connected_trigger()->trigger(this->client_info_.name, this->client_info_.peername);
#endif #endif
#ifdef USE_HOMEASSISTANT_TIME #ifdef USE_HOMEASSISTANT_TIME
if (homeassistant::global_homeassistant_time != nullptr) { if (homeassistant::global_homeassistant_time != nullptr) {
@ -1420,20 +1381,21 @@ void APIConnection::complete_authentication_() {
#endif #endif
} }
HelloResponse APIConnection::hello(const HelloRequest &msg) { bool APIConnection::send_hello_response(const HelloRequest &msg) {
this->client_info_ = msg.client_info; this->client_info_.name = msg.client_info;
this->client_peername_ = this->helper_->getpeername(); this->client_info_.peername = this->helper_->getpeername();
this->helper_->set_log_info(this->get_client_combined_info());
this->client_api_version_major_ = msg.api_version_major; this->client_api_version_major_ = msg.api_version_major;
this->client_api_version_minor_ = msg.api_version_minor; this->client_api_version_minor_ = msg.api_version_minor;
ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->client_info_.c_str(), ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->client_info_.name.c_str(),
this->client_peername_.c_str(), this->client_api_version_major_, this->client_api_version_minor_); this->client_info_.peername.c_str(), this->client_api_version_major_, this->client_api_version_minor_);
HelloResponse resp; HelloResponse resp;
resp.api_version_major = 1; resp.api_version_major = 1;
resp.api_version_minor = 10; resp.api_version_minor = 10;
resp.server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")"; // Temporary string for concatenation - will be valid during send_message call
resp.name = App.get_name(); std::string server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")";
resp.set_server_info(StringRef(server_info));
resp.set_name(StringRef(App.get_name()));
#ifdef USE_API_PASSWORD #ifdef USE_API_PASSWORD
// Password required - wait for authentication // Password required - wait for authentication
@ -1443,9 +1405,9 @@ HelloResponse APIConnection::hello(const HelloRequest &msg) {
this->complete_authentication_(); this->complete_authentication_();
#endif #endif
return resp; return this->send_message(resp, HelloResponse::MESSAGE_TYPE);
} }
ConnectResponse APIConnection::connect(const ConnectRequest &msg) { bool APIConnection::send_connect_response(const ConnectRequest &msg) {
bool correct = true; bool correct = true;
#ifdef USE_API_PASSWORD #ifdef USE_API_PASSWORD
correct = this->parent_->check_password(msg.password); correct = this->parent_->check_password(msg.password);
@ -1457,54 +1419,73 @@ ConnectResponse APIConnection::connect(const ConnectRequest &msg) {
if (correct) { if (correct) {
this->complete_authentication_(); this->complete_authentication_();
} }
return resp; return this->send_message(resp, ConnectResponse::MESSAGE_TYPE);
} }
DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) {
bool APIConnection::send_ping_response(const PingRequest &msg) {
PingResponse resp;
return this->send_message(resp, PingResponse::MESSAGE_TYPE);
}
bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
DeviceInfoResponse resp{}; DeviceInfoResponse resp{};
#ifdef USE_API_PASSWORD #ifdef USE_API_PASSWORD
resp.uses_password = true; resp.uses_password = true;
#else
resp.uses_password = false;
#endif #endif
resp.name = App.get_name(); resp.set_name(StringRef(App.get_name()));
resp.friendly_name = App.get_friendly_name(); resp.set_friendly_name(StringRef(App.get_friendly_name()));
#ifdef USE_AREAS #ifdef USE_AREAS
resp.suggested_area = App.get_area(); resp.set_suggested_area(StringRef(App.get_area()));
#endif #endif
resp.mac_address = get_mac_address_pretty(); // mac_address must store temporary string - will be valid during send_message call
resp.esphome_version = ESPHOME_VERSION; std::string mac_address = get_mac_address_pretty();
resp.compilation_time = App.get_compilation_time(); resp.set_mac_address(StringRef(mac_address));
// Compile-time StringRef constants
static constexpr auto ESPHOME_VERSION_REF = StringRef::from_lit(ESPHOME_VERSION);
resp.set_esphome_version(ESPHOME_VERSION_REF);
// get_compilation_time() returns temporary std::string - must store it
std::string compilation_time = App.get_compilation_time();
resp.set_compilation_time(StringRef(compilation_time));
// Compile-time StringRef constants for manufacturers
#if defined(USE_ESP8266) || defined(USE_ESP32) #if defined(USE_ESP8266) || defined(USE_ESP32)
resp.manufacturer = "Espressif"; static constexpr auto MANUFACTURER = StringRef::from_lit("Espressif");
#elif defined(USE_RP2040) #elif defined(USE_RP2040)
resp.manufacturer = "Raspberry Pi"; static constexpr auto MANUFACTURER = StringRef::from_lit("Raspberry Pi");
#elif defined(USE_BK72XX) #elif defined(USE_BK72XX)
resp.manufacturer = "Beken"; static constexpr auto MANUFACTURER = StringRef::from_lit("Beken");
#elif defined(USE_LN882X) #elif defined(USE_LN882X)
resp.manufacturer = "Lightning"; static constexpr auto MANUFACTURER = StringRef::from_lit("Lightning");
#elif defined(USE_RTL87XX) #elif defined(USE_RTL87XX)
resp.manufacturer = "Realtek"; static constexpr auto MANUFACTURER = StringRef::from_lit("Realtek");
#elif defined(USE_HOST) #elif defined(USE_HOST)
resp.manufacturer = "Host"; static constexpr auto MANUFACTURER = StringRef::from_lit("Host");
#endif #endif
resp.model = ESPHOME_BOARD; resp.set_manufacturer(MANUFACTURER);
static constexpr auto MODEL = StringRef::from_lit(ESPHOME_BOARD);
resp.set_model(MODEL);
#ifdef USE_DEEP_SLEEP #ifdef USE_DEEP_SLEEP
resp.has_deep_sleep = deep_sleep::global_has_deep_sleep; resp.has_deep_sleep = deep_sleep::global_has_deep_sleep;
#endif #endif
#ifdef ESPHOME_PROJECT_NAME #ifdef ESPHOME_PROJECT_NAME
resp.project_name = ESPHOME_PROJECT_NAME; static constexpr auto PROJECT_NAME = StringRef::from_lit(ESPHOME_PROJECT_NAME);
resp.project_version = ESPHOME_PROJECT_VERSION; static constexpr auto PROJECT_VERSION = StringRef::from_lit(ESPHOME_PROJECT_VERSION);
resp.set_project_name(PROJECT_NAME);
resp.set_project_version(PROJECT_VERSION);
#endif #endif
#ifdef USE_WEBSERVER #ifdef USE_WEBSERVER
resp.webserver_port = USE_WEBSERVER_PORT; resp.webserver_port = USE_WEBSERVER_PORT;
#endif #endif
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
resp.legacy_bluetooth_proxy_version = bluetooth_proxy::global_bluetooth_proxy->get_legacy_version();
resp.bluetooth_proxy_feature_flags = bluetooth_proxy::global_bluetooth_proxy->get_feature_flags(); resp.bluetooth_proxy_feature_flags = bluetooth_proxy::global_bluetooth_proxy->get_feature_flags();
resp.bluetooth_mac_address = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_mac_address_pretty(); // bt_mac must store temporary string - will be valid during send_message call
std::string bluetooth_mac = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_mac_address_pretty();
resp.set_bluetooth_mac_address(StringRef(bluetooth_mac));
#endif #endif
#ifdef USE_VOICE_ASSISTANT #ifdef USE_VOICE_ASSISTANT
resp.legacy_voice_assistant_version = voice_assistant::global_voice_assistant->get_legacy_version();
resp.voice_assistant_feature_flags = voice_assistant::global_voice_assistant->get_feature_flags(); resp.voice_assistant_feature_flags = voice_assistant::global_voice_assistant->get_feature_flags();
#endif #endif
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
@ -1512,23 +1493,25 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) {
#endif #endif
#ifdef USE_DEVICES #ifdef USE_DEVICES
for (auto const &device : App.get_devices()) { for (auto const &device : App.get_devices()) {
DeviceInfo device_info; resp.devices.emplace_back();
auto &device_info = resp.devices.back();
device_info.device_id = device->get_device_id(); device_info.device_id = device->get_device_id();
device_info.name = device->get_name(); device_info.set_name(StringRef(device->get_name()));
device_info.area_id = device->get_area_id(); device_info.area_id = device->get_area_id();
resp.devices.push_back(device_info);
} }
#endif #endif
#ifdef USE_AREAS #ifdef USE_AREAS
for (auto const &area : App.get_areas()) { for (auto const &area : App.get_areas()) {
AreaInfo area_info; resp.areas.emplace_back();
auto &area_info = resp.areas.back();
area_info.area_id = area->get_area_id(); area_info.area_id = area->get_area_id();
area_info.name = area->get_name(); area_info.set_name(StringRef(area->get_name()));
resp.areas.push_back(area_info);
} }
#endif #endif
return resp;
return this->send_message(resp, DeviceInfoResponse::MESSAGE_TYPE);
} }
void APIConnection::on_home_assistant_state_response(const HomeAssistantStateResponse &msg) { void APIConnection::on_home_assistant_state_response(const HomeAssistantStateResponse &msg) {
for (auto &it : this->parent_->get_state_subs()) { for (auto &it : this->parent_->get_state_subs()) {
if (it.entity_id == msg.entity_id && it.attribute.value() == msg.attribute) { if (it.entity_id == msg.entity_id && it.attribute.value() == msg.attribute) {
@ -1550,23 +1533,21 @@ void APIConnection::execute_service(const ExecuteServiceRequest &msg) {
} }
#endif #endif
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
NoiseEncryptionSetKeyResponse APIConnection::noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) { bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) {
psk_t psk{}; psk_t psk{};
NoiseEncryptionSetKeyResponse resp; NoiseEncryptionSetKeyResponse resp;
if (base64_decode(msg.key, psk.data(), msg.key.size()) != psk.size()) { if (base64_decode(msg.key, psk.data(), msg.key.size()) != psk.size()) {
ESP_LOGW(TAG, "Invalid encryption key length"); ESP_LOGW(TAG, "Invalid encryption key length");
resp.success = false; resp.success = false;
return resp; return this->send_message(resp, NoiseEncryptionSetKeyResponse::MESSAGE_TYPE);
} }
if (!this->parent_->save_noise_psk(psk, true)) { if (!this->parent_->save_noise_psk(psk, true)) {
ESP_LOGW(TAG, "Failed to save encryption key"); ESP_LOGW(TAG, "Failed to save encryption key");
resp.success = false; resp.success = false;
return resp; return this->send_message(resp, NoiseEncryptionSetKeyResponse::MESSAGE_TYPE);
} }
resp.success = true; resp.success = true;
return resp; return this->send_message(resp, NoiseEncryptionSetKeyResponse::MESSAGE_TYPE);
} }
#endif #endif
void APIConnection::subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) { void APIConnection::subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) {
@ -1671,6 +1652,10 @@ ProtoWriteBuffer APIConnection::allocate_batch_message_buffer(uint16_t size) {
} }
void APIConnection::process_batch_() { void APIConnection::process_batch_() {
// Ensure PacketInfo remains trivially destructible for our placement new approach
static_assert(std::is_trivially_destructible<PacketInfo>::value,
"PacketInfo must remain trivially destructible with this placement-new approach");
if (this->deferred_batch_.empty()) { if (this->deferred_batch_.empty()) {
this->flags_.batch_scheduled = false; this->flags_.batch_scheduled = false;
return; return;
@ -1708,9 +1693,12 @@ void APIConnection::process_batch_() {
return; return;
} }
// Pre-allocate storage for packet info size_t packets_to_process = std::min(num_items, MAX_PACKETS_PER_BATCH);
std::vector<PacketInfo> packet_info;
packet_info.reserve(num_items); // Stack-allocated array for packet info
alignas(PacketInfo) char packet_info_storage[MAX_PACKETS_PER_BATCH * sizeof(PacketInfo)];
PacketInfo *packet_info = reinterpret_cast<PacketInfo *>(packet_info_storage);
size_t packet_count = 0;
// Cache these values to avoid repeated virtual calls // Cache these values to avoid repeated virtual calls
const uint8_t header_padding = this->helper_->frame_header_padding(); const uint8_t header_padding = this->helper_->frame_header_padding();
@ -1742,8 +1730,8 @@ void APIConnection::process_batch_() {
// The actual message data follows after the header padding // The actual message data follows after the header padding
uint32_t current_offset = 0; uint32_t current_offset = 0;
// Process items and encode directly to buffer // Process items and encode directly to buffer (up to our limit)
for (size_t i = 0; i < this->deferred_batch_.size(); i++) { for (size_t i = 0; i < packets_to_process; i++) {
const auto &item = this->deferred_batch_[i]; const auto &item = this->deferred_batch_[i];
// Try to encode message // Try to encode message
// The creator will calculate overhead to determine if the message fits // The creator will calculate overhead to determine if the message fits
@ -1757,7 +1745,11 @@ void APIConnection::process_batch_() {
// Message was encoded successfully // Message was encoded successfully
// payload_size is header_padding + actual payload size + footer_size // payload_size is header_padding + actual payload size + footer_size
uint16_t proto_payload_size = payload_size - header_padding - footer_size; uint16_t proto_payload_size = payload_size - header_padding - footer_size;
packet_info.emplace_back(item.message_type, current_offset, proto_payload_size); // Use placement new to construct PacketInfo in pre-allocated stack array
// This avoids default-constructing all MAX_PACKETS_PER_BATCH elements
// Explicit destruction is not needed because PacketInfo is trivially destructible,
// as ensured by the static_assert in its definition.
new (&packet_info[packet_count++]) PacketInfo(item.message_type, current_offset, proto_payload_size);
// Update tracking variables // Update tracking variables
items_processed++; items_processed++;
@ -1783,8 +1775,8 @@ void APIConnection::process_batch_() {
} }
// Send all collected packets // Send all collected packets
APIError err = APIError err = this->helper_->write_protobuf_packets(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()},
this->helper_->write_protobuf_packets(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, packet_info); std::span<const PacketInfo>(packet_info, packet_count));
if (err != APIError::OK && err != APIError::WOULD_BLOCK) { if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
on_fatal_error(); on_fatal_error();
ESP_LOGW(TAG, "%s: Batch write failed %s errno=%d", this->get_client_combined_info().c_str(), api_error_to_str(err), ESP_LOGW(TAG, "%s: Batch write failed %s errno=%d", this->get_client_combined_info().c_str(), api_error_to_str(err),
@ -1844,6 +1836,5 @@ uint16_t APIConnection::try_send_ping_request(EntityBase *entity, APIConnection
return encode_message_to_buffer(req, PingRequest::MESSAGE_TYPE, conn, remaining_size, is_single); return encode_message_to_buffer(req, PingRequest::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
} // namespace api } // namespace esphome::api
} // namespace esphome
#endif #endif

View File

@ -13,13 +13,36 @@
#include <vector> #include <vector>
#include <functional> #include <functional>
namespace esphome { namespace esphome::api {
namespace api {
// Client information structure
struct ClientInfo {
std::string name; // Client name from Hello message
std::string peername; // IP:port from socket
std::string get_combined_info() const {
if (name == peername) {
// Before Hello message, both are the same
return name;
}
return name + " (" + peername + ")";
}
};
// Keepalive timeout in milliseconds // Keepalive timeout in milliseconds
static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000; static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000;
// Maximum number of entities to process in a single batch during initial state/info sending // Maximum number of entities to process in a single batch during initial state/info sending
static constexpr size_t MAX_INITIAL_PER_BATCH = 20; // This was increased from 20 to 24 after removing the unique_id field from entity info messages,
// which reduced message sizes allowing more entities per batch without exceeding packet limits
static constexpr size_t MAX_INITIAL_PER_BATCH = 24;
// Maximum number of packets to process in a single batch (platform-dependent)
// This limit exists to prevent stack overflow from the PacketInfo array in process_batch_
// Each PacketInfo is 8 bytes, so 64 * 8 = 512 bytes, 32 * 8 = 256 bytes
#if defined(USE_ESP32) || defined(USE_HOST)
static constexpr size_t MAX_PACKETS_PER_BATCH = 64; // ESP32 has 8KB+ stack, HOST has plenty
#else
static constexpr size_t MAX_PACKETS_PER_BATCH = 32; // ESP8266/RP2040/etc have smaller stacks
#endif
class APIConnection : public APIServerConnection { class APIConnection : public APIServerConnection {
public: public:
@ -116,7 +139,6 @@ class APIConnection : public APIServerConnection {
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override; void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override; void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override;
bool send_bluetooth_le_advertisement(const BluetoothLEAdvertisementResponse &msg);
void bluetooth_device_request(const BluetoothDeviceRequest &msg) override; void bluetooth_device_request(const BluetoothDeviceRequest &msg) override;
void bluetooth_gatt_read(const BluetoothGATTReadRequest &msg) override; void bluetooth_gatt_read(const BluetoothGATTReadRequest &msg) override;
@ -125,8 +147,7 @@ class APIConnection : public APIServerConnection {
void bluetooth_gatt_write_descriptor(const BluetoothGATTWriteDescriptorRequest &msg) override; void bluetooth_gatt_write_descriptor(const BluetoothGATTWriteDescriptorRequest &msg) override;
void bluetooth_gatt_get_services(const BluetoothGATTGetServicesRequest &msg) override; void bluetooth_gatt_get_services(const BluetoothGATTGetServicesRequest &msg) override;
void bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) override; void bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) override;
BluetoothConnectionsFreeResponse subscribe_bluetooth_connections_free( bool send_subscribe_bluetooth_connections_free_response(const SubscribeBluetoothConnectionsFreeRequest &msg) override;
const SubscribeBluetoothConnectionsFreeRequest &msg) override;
void bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) override; void bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) override;
#endif #endif
@ -144,8 +165,7 @@ class APIConnection : public APIServerConnection {
void on_voice_assistant_audio(const VoiceAssistantAudio &msg) override; void on_voice_assistant_audio(const VoiceAssistantAudio &msg) override;
void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &msg) override; void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &msg) override;
void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &msg) override; void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &msg) override;
VoiceAssistantConfigurationResponse voice_assistant_get_configuration( bool send_voice_assistant_get_configuration_response(const VoiceAssistantConfigurationRequest &msg) override;
const VoiceAssistantConfigurationRequest &msg) override;
void voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) override; void voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) override;
#endif #endif
@ -172,11 +192,11 @@ class APIConnection : public APIServerConnection {
#ifdef USE_HOMEASSISTANT_TIME #ifdef USE_HOMEASSISTANT_TIME
void on_get_time_response(const GetTimeResponse &value) override; void on_get_time_response(const GetTimeResponse &value) override;
#endif #endif
HelloResponse hello(const HelloRequest &msg) override; bool send_hello_response(const HelloRequest &msg) override;
ConnectResponse connect(const ConnectRequest &msg) override; bool send_connect_response(const ConnectRequest &msg) override;
DisconnectResponse disconnect(const DisconnectRequest &msg) override; bool send_disconnect_response(const DisconnectRequest &msg) override;
PingResponse ping(const PingRequest &msg) override { return {}; } bool send_ping_response(const PingRequest &msg) override;
DeviceInfoResponse device_info(const DeviceInfoRequest &msg) override; bool send_device_info_response(const DeviceInfoRequest &msg) override;
void list_entities(const ListEntitiesRequest &msg) override { this->list_entities_iterator_.begin(); } void list_entities(const ListEntitiesRequest &msg) override { this->list_entities_iterator_.begin(); }
void subscribe_states(const SubscribeStatesRequest &msg) override { void subscribe_states(const SubscribeStatesRequest &msg) override {
this->flags_.state_subscription = true; this->flags_.state_subscription = true;
@ -191,15 +211,12 @@ class APIConnection : public APIServerConnection {
this->flags_.service_call_subscription = true; this->flags_.service_call_subscription = true;
} }
void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) override; void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) override;
GetTimeResponse get_time(const GetTimeRequest &msg) override { bool send_get_time_response(const GetTimeRequest &msg) override;
// TODO
return {};
}
#ifdef USE_API_SERVICES #ifdef USE_API_SERVICES
void execute_service(const ExecuteServiceRequest &msg) override; void execute_service(const ExecuteServiceRequest &msg) override;
#endif #endif
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) override; bool send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) override;
#endif #endif
bool is_authenticated() override { bool is_authenticated() override {
@ -261,13 +278,7 @@ class APIConnection : public APIServerConnection {
bool try_to_clear_buffer(bool log_out_of_space); bool try_to_clear_buffer(bool log_out_of_space);
bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override; bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override;
std::string get_client_combined_info() const { std::string get_client_combined_info() const { return this->client_info_.get_combined_info(); }
if (this->client_info_ == this->client_peername_) {
// Before Hello message, both are the same (just IP:port)
return this->client_info_;
}
return this->client_info_ + " (" + this->client_peername_ + ")";
}
// Buffer allocator methods for batch processing // Buffer allocator methods for batch processing
ProtoWriteBuffer allocate_single_message_buffer(uint16_t size); ProtoWriteBuffer allocate_single_message_buffer(uint16_t size);
@ -296,13 +307,18 @@ class APIConnection : public APIServerConnection {
APIConnection *conn, uint32_t remaining_size, bool is_single) { APIConnection *conn, uint32_t remaining_size, bool is_single) {
// Set common fields that are shared by all entity types // Set common fields that are shared by all entity types
msg.key = entity->get_object_id_hash(); msg.key = entity->get_object_id_hash();
msg.object_id = entity->get_object_id(); // IMPORTANT: get_object_id() may return a temporary std::string
std::string object_id = entity->get_object_id();
msg.set_object_id(StringRef(object_id));
if (entity->has_own_name()) if (entity->has_own_name()) {
msg.name = entity->get_name(); msg.set_name(entity->get_name());
}
// Set common EntityBase properties // Set common EntityBase properties
msg.icon = entity->get_icon(); #ifdef USE_ENTITY_ICON
msg.set_icon(entity->get_icon_ref());
#endif
msg.disabled_by_default = entity->is_disabled_by_default(); msg.disabled_by_default = entity->is_disabled_by_default();
msg.entity_category = static_cast<enums::EntityCategory>(entity->get_entity_category()); msg.entity_category = static_cast<enums::EntityCategory>(entity->get_entity_category());
#ifdef USE_DEVICES #ifdef USE_DEVICES
@ -471,9 +487,8 @@ class APIConnection : public APIServerConnection {
std::unique_ptr<camera::CameraImageReader> image_reader_; std::unique_ptr<camera::CameraImageReader> image_reader_;
#endif #endif
// Group 3: Strings (12 bytes each on 32-bit, 4-byte aligned) // Group 3: Client info struct (24 bytes on 32-bit: 2 strings × 12 bytes each)
std::string client_info_; ClientInfo client_info_;
std::string client_peername_;
// Group 4: 4-byte types // Group 4: 4-byte types
uint32_t last_traffic_; uint32_t last_traffic_;
@ -707,6 +722,5 @@ class APIConnection : public APIServerConnection {
} }
}; };
} // namespace api } // namespace esphome::api
} // namespace esphome
#endif #endif

File diff suppressed because it is too large Load Diff

View File

@ -8,16 +8,17 @@
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#ifdef USE_API #ifdef USE_API
#ifdef USE_API_NOISE
#include "noise/protocol.h"
#endif
#include "api_noise_context.h"
#include "esphome/components/socket/socket.h" #include "esphome/components/socket/socket.h"
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/log.h"
namespace esphome { namespace esphome::api {
namespace api {
// uncomment to log raw packets
//#define HELPER_LOG_PACKETS
// Forward declaration
struct ClientInfo;
class ProtoWriteBuffer; class ProtoWriteBuffer;
@ -40,7 +41,6 @@ struct PacketInfo {
enum class APIError : uint16_t { enum class APIError : uint16_t {
OK = 0, OK = 0,
WOULD_BLOCK = 1001, WOULD_BLOCK = 1001,
BAD_HANDSHAKE_PACKET_LEN = 1002,
BAD_INDICATOR = 1003, BAD_INDICATOR = 1003,
BAD_DATA_PACKET = 1004, BAD_DATA_PACKET = 1004,
TCP_NODELAY_FAILED = 1005, TCP_NODELAY_FAILED = 1005,
@ -51,16 +51,19 @@ enum class APIError : uint16_t {
BAD_ARG = 1010, BAD_ARG = 1010,
SOCKET_READ_FAILED = 1011, SOCKET_READ_FAILED = 1011,
SOCKET_WRITE_FAILED = 1012, SOCKET_WRITE_FAILED = 1012,
OUT_OF_MEMORY = 1018,
CONNECTION_CLOSED = 1022,
#ifdef USE_API_NOISE
BAD_HANDSHAKE_PACKET_LEN = 1002,
HANDSHAKESTATE_READ_FAILED = 1013, HANDSHAKESTATE_READ_FAILED = 1013,
HANDSHAKESTATE_WRITE_FAILED = 1014, HANDSHAKESTATE_WRITE_FAILED = 1014,
HANDSHAKESTATE_BAD_STATE = 1015, HANDSHAKESTATE_BAD_STATE = 1015,
CIPHERSTATE_DECRYPT_FAILED = 1016, CIPHERSTATE_DECRYPT_FAILED = 1016,
CIPHERSTATE_ENCRYPT_FAILED = 1017, CIPHERSTATE_ENCRYPT_FAILED = 1017,
OUT_OF_MEMORY = 1018,
HANDSHAKESTATE_SETUP_FAILED = 1019, HANDSHAKESTATE_SETUP_FAILED = 1019,
HANDSHAKESTATE_SPLIT_FAILED = 1020, HANDSHAKESTATE_SPLIT_FAILED = 1020,
BAD_HANDSHAKE_ERROR_BYTE = 1021, BAD_HANDSHAKE_ERROR_BYTE = 1021,
CONNECTION_CLOSED = 1022, #endif
}; };
const char *api_error_to_str(APIError err); const char *api_error_to_str(APIError err);
@ -68,7 +71,8 @@ const char *api_error_to_str(APIError err);
class APIFrameHelper { class APIFrameHelper {
public: public:
APIFrameHelper() = default; APIFrameHelper() = default;
explicit APIFrameHelper(std::unique_ptr<socket::Socket> socket) : socket_owned_(std::move(socket)) { explicit APIFrameHelper(std::unique_ptr<socket::Socket> socket, const ClientInfo *client_info)
: socket_owned_(std::move(socket)), client_info_(client_info) {
socket_ = socket_owned_.get(); socket_ = socket_owned_.get();
} }
virtual ~APIFrameHelper() = default; virtual ~APIFrameHelper() = default;
@ -94,8 +98,6 @@ class APIFrameHelper {
} }
return APIError::OK; return APIError::OK;
} }
// Give this helper a name for logging
void set_log_info(std::string info) { info_ = std::move(info); }
virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0; virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0;
// Write multiple protobuf packets in a single operation // Write multiple protobuf packets in a single operation
// packets contains (message_type, offset, length) for each message in the buffer // packets contains (message_type, offset, length) for each message in the buffer
@ -109,29 +111,28 @@ class APIFrameHelper {
bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); } bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); }
protected: protected:
// Struct for holding parsed frame data
struct ParsedFrame {
std::vector<uint8_t> msg;
};
// Buffer containing data to be sent // Buffer containing data to be sent
struct SendBuffer { struct SendBuffer {
std::vector<uint8_t> data; std::unique_ptr<uint8_t[]> data;
uint16_t offset{0}; // Current offset within the buffer (uint16_t to reduce memory usage) uint16_t size{0}; // Total size of the buffer
uint16_t offset{0}; // Current offset within the buffer
// Using uint16_t reduces memory usage since ESPHome API messages are limited to UINT16_MAX (65535) bytes // Using uint16_t reduces memory usage since ESPHome API messages are limited to UINT16_MAX (65535) bytes
uint16_t remaining() const { return static_cast<uint16_t>(data.size()) - offset; } uint16_t remaining() const { return size - offset; }
const uint8_t *current_data() const { return data.data() + offset; } const uint8_t *current_data() const { return data.get() + offset; }
}; };
// Common implementation for writing raw data to socket // Common implementation for writing raw data to socket
APIError write_raw_(const struct iovec *iov, int iovcnt); APIError write_raw_(const struct iovec *iov, int iovcnt, uint16_t total_write_len);
// Try to send data from the tx buffer // Try to send data from the tx buffer
APIError try_send_tx_buf_(); APIError try_send_tx_buf_();
// Helper method to buffer data from IOVs // Helper method to buffer data from IOVs
void buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len); void buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len, uint16_t offset);
// Common socket write error handling
APIError handle_socket_write_error_();
template<typename StateEnum> template<typename StateEnum>
APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf, APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf,
const std::string &info, StateEnum &state, StateEnum failed_state); const std::string &info, StateEnum &state, StateEnum failed_state);
@ -161,10 +162,13 @@ class APIFrameHelper {
// Containers (size varies, but typically 12+ bytes on 32-bit) // Containers (size varies, but typically 12+ bytes on 32-bit)
std::deque<SendBuffer> tx_buf_; std::deque<SendBuffer> tx_buf_;
std::string info_;
std::vector<struct iovec> reusable_iovs_; std::vector<struct iovec> reusable_iovs_;
std::vector<uint8_t> rx_buf_; std::vector<uint8_t> rx_buf_;
// Pointer to client info (4 bytes on 32-bit)
// Note: The pointed-to ClientInfo object must outlive this APIFrameHelper instance.
const ClientInfo *client_info_{nullptr};
// Group smaller types together // Group smaller types together
uint16_t rx_buf_len_ = 0; uint16_t rx_buf_len_ = 0;
State state_{State::INITIALIZE}; State state_{State::INITIALIZE};
@ -179,105 +183,6 @@ class APIFrameHelper {
APIError handle_socket_read_result_(ssize_t received); APIError handle_socket_read_result_(ssize_t received);
}; };
#ifdef USE_API_NOISE } // namespace esphome::api
class APINoiseFrameHelper : public APIFrameHelper {
public:
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, std::shared_ptr<APINoiseContext> ctx)
: APIFrameHelper(std::move(socket)), ctx_(std::move(ctx)) {
// Noise header structure:
// Pos 0: indicator (0x01)
// Pos 1-2: encrypted payload size (16-bit big-endian)
// Pos 3-6: encrypted type (16-bit) + data_len (16-bit)
// Pos 7+: actual payload data
frame_header_padding_ = 7;
}
~APINoiseFrameHelper() override;
APIError init() override;
APIError loop() override;
APIError read_packet(ReadPacketBuffer *buffer) override;
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override;
// Get the frame header padding required by this protocol
uint8_t frame_header_padding() override { return frame_header_padding_; }
// Get the frame footer size required by this protocol
uint8_t frame_footer_size() override { return frame_footer_size_; }
protected: #endif // USE_API
APIError state_action_();
APIError try_read_frame_(ParsedFrame *frame);
APIError write_frame_(const uint8_t *data, uint16_t len);
APIError init_handshake_();
APIError check_handshake_finished_();
void send_explicit_handshake_reject_(const std::string &reason);
// Pointers first (4 bytes each)
NoiseHandshakeState *handshake_{nullptr};
NoiseCipherState *send_cipher_{nullptr};
NoiseCipherState *recv_cipher_{nullptr};
// Shared pointer (8 bytes on 32-bit = 4 bytes control block pointer + 4 bytes object pointer)
std::shared_ptr<APINoiseContext> ctx_;
// Vector (12 bytes on 32-bit)
std::vector<uint8_t> prologue_;
// NoiseProtocolId (size depends on implementation)
NoiseProtocolId nid_;
// Group small types together
// Fixed-size header buffer for noise protocol:
// 1 byte for indicator + 2 bytes for message size (16-bit value, not varint)
// Note: Maximum message size is UINT16_MAX (65535), with a limit of 128 bytes during handshake phase
uint8_t rx_header_buf_[3];
uint8_t rx_header_buf_len_ = 0;
// 4 bytes total, no padding
};
#endif // USE_API_NOISE
#ifdef USE_API_PLAINTEXT
class APIPlaintextFrameHelper : public APIFrameHelper {
public:
APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket) : APIFrameHelper(std::move(socket)) {
// Plaintext header structure (worst case):
// Pos 0: indicator (0x00)
// Pos 1-3: payload size varint (up to 3 bytes)
// Pos 4-5: message type varint (up to 2 bytes)
// Pos 6+: actual payload data
frame_header_padding_ = 6;
}
~APIPlaintextFrameHelper() override = default;
APIError init() override;
APIError loop() override;
APIError read_packet(ReadPacketBuffer *buffer) override;
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override;
uint8_t frame_header_padding() override { return frame_header_padding_; }
// Get the frame footer size required by this protocol
uint8_t frame_footer_size() override { return frame_footer_size_; }
protected:
APIError try_read_frame_(ParsedFrame *frame);
// Group 2-byte aligned types
uint16_t rx_header_parsed_type_ = 0;
uint16_t rx_header_parsed_len_ = 0;
// Group 1-byte types together
// Fixed-size header buffer for plaintext protocol:
// We now store the indicator byte + the two varints.
// To match noise protocol's maximum message size (UINT16_MAX = 65535), we need:
// 1 byte for indicator + 3 bytes for message size varint (supports up to 2097151) + 2 bytes for message type varint
//
// While varints could theoretically be up to 10 bytes each for 64-bit values,
// attempting to process messages with headers that large would likely crash the
// ESP32 due to memory constraints.
uint8_t rx_header_buf_[6]; // 1 byte indicator + 5 bytes for varints (3 for size + 2 for type)
uint8_t rx_header_buf_pos_ = 0;
bool rx_header_parsed_ = false;
// 8 bytes total, no padding needed
};
#endif
} // namespace api
} // namespace esphome
#endif

View File

@ -0,0 +1,583 @@
#include "api_frame_helper_noise.h"
#ifdef USE_API
#ifdef USE_API_NOISE
#include "api_connection.h" // For ClientInfo struct
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "proto.h"
#include <cstring>
#include <cinttypes>
namespace esphome::api {
static const char *const TAG = "api.noise";
static const char *const PROLOGUE_INIT = "NoiseAPIInit";
static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit")
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->client_info_->get_combined_info().c_str(), ##__VA_ARGS__)
#ifdef HELPER_LOG_PACKETS
#define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str())
#define LOG_PACKET_SENDING(data, len) ESP_LOGVV(TAG, "Sending raw: %s", format_hex_pretty(data, len).c_str())
#else
#define LOG_PACKET_RECEIVED(buffer) ((void) 0)
#define LOG_PACKET_SENDING(data, len) ((void) 0)
#endif
/// Convert a noise error code to a readable error
std::string noise_err_to_str(int err) {
if (err == NOISE_ERROR_NO_MEMORY)
return "NO_MEMORY";
if (err == NOISE_ERROR_UNKNOWN_ID)
return "UNKNOWN_ID";
if (err == NOISE_ERROR_UNKNOWN_NAME)
return "UNKNOWN_NAME";
if (err == NOISE_ERROR_MAC_FAILURE)
return "MAC_FAILURE";
if (err == NOISE_ERROR_NOT_APPLICABLE)
return "NOT_APPLICABLE";
if (err == NOISE_ERROR_SYSTEM)
return "SYSTEM";
if (err == NOISE_ERROR_REMOTE_KEY_REQUIRED)
return "REMOTE_KEY_REQUIRED";
if (err == NOISE_ERROR_LOCAL_KEY_REQUIRED)
return "LOCAL_KEY_REQUIRED";
if (err == NOISE_ERROR_PSK_REQUIRED)
return "PSK_REQUIRED";
if (err == NOISE_ERROR_INVALID_LENGTH)
return "INVALID_LENGTH";
if (err == NOISE_ERROR_INVALID_PARAM)
return "INVALID_PARAM";
if (err == NOISE_ERROR_INVALID_STATE)
return "INVALID_STATE";
if (err == NOISE_ERROR_INVALID_NONCE)
return "INVALID_NONCE";
if (err == NOISE_ERROR_INVALID_PRIVATE_KEY)
return "INVALID_PRIVATE_KEY";
if (err == NOISE_ERROR_INVALID_PUBLIC_KEY)
return "INVALID_PUBLIC_KEY";
if (err == NOISE_ERROR_INVALID_FORMAT)
return "INVALID_FORMAT";
if (err == NOISE_ERROR_INVALID_SIGNATURE)
return "INVALID_SIGNATURE";
return to_string(err);
}
/// Initialize the frame helper, returns OK if successful.
APIError APINoiseFrameHelper::init() {
APIError err = init_common_();
if (err != APIError::OK) {
return err;
}
// init prologue
size_t old_size = prologue_.size();
prologue_.resize(old_size + PROLOGUE_INIT_LEN);
std::memcpy(prologue_.data() + old_size, PROLOGUE_INIT, PROLOGUE_INIT_LEN);
state_ = State::CLIENT_HELLO;
return APIError::OK;
}
// Helper for handling handshake frame errors
APIError APINoiseFrameHelper::handle_handshake_frame_error_(APIError aerr) {
if (aerr == APIError::BAD_INDICATOR) {
send_explicit_handshake_reject_("Bad indicator byte");
} else if (aerr == APIError::BAD_HANDSHAKE_PACKET_LEN) {
send_explicit_handshake_reject_("Bad handshake packet len");
}
return aerr;
}
// Helper for handling noise library errors
APIError APINoiseFrameHelper::handle_noise_error_(int err, const char *func_name, APIError api_err) {
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("%s failed: %s", func_name, noise_err_to_str(err).c_str());
return api_err;
}
return APIError::OK;
}
/// Run through handshake messages (if in that phase)
APIError APINoiseFrameHelper::loop() {
// During handshake phase, process as many actions as possible until we can't progress
// socket_->ready() stays true until next main loop, but state_action() will return
// WOULD_BLOCK when no more data is available to read
while (state_ != State::DATA && this->socket_->ready()) {
APIError err = state_action_();
if (err == APIError::WOULD_BLOCK) {
break;
}
if (err != APIError::OK) {
return err;
}
}
// Use base class implementation for buffer sending
return APIFrameHelper::loop();
}
/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter
*
* @param frame: The struct to hold the frame information in.
* msg_start: points to the start of the payload - this pointer is only valid until the next
* try_receive_raw_ call
*
* @return 0 if a full packet is in rx_buf_
* @return -1 if error, check errno.
*
* errno EWOULDBLOCK: Packet could not be read without blocking. Try again later.
* errno ENOMEM: Not enough memory for reading packet.
* errno API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame.
* errno API_ERROR_HANDSHAKE_PACKET_LEN: Packet too big for this phase.
*/
APIError APINoiseFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
if (frame == nullptr) {
HELPER_LOG("Bad argument for try_read_frame_");
return APIError::BAD_ARG;
}
// read header
if (rx_header_buf_len_ < 3) {
// no header information yet
uint8_t to_read = 3 - rx_header_buf_len_;
ssize_t received = this->socket_->read(&rx_header_buf_[rx_header_buf_len_], to_read);
APIError err = handle_socket_read_result_(received);
if (err != APIError::OK) {
return err;
}
rx_header_buf_len_ += static_cast<uint8_t>(received);
if (static_cast<uint8_t>(received) != to_read) {
// not a full read
return APIError::WOULD_BLOCK;
}
if (rx_header_buf_[0] != 0x01) {
state_ = State::FAILED;
HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]);
return APIError::BAD_INDICATOR;
}
// header reading done
}
// read body
uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2];
if (state_ != State::DATA && msg_size > 128) {
// for handshake message only permit up to 128 bytes
state_ = State::FAILED;
HELPER_LOG("Bad packet len for handshake: %d", msg_size);
return APIError::BAD_HANDSHAKE_PACKET_LEN;
}
// reserve space for body
if (rx_buf_.size() != msg_size) {
rx_buf_.resize(msg_size);
}
if (rx_buf_len_ < msg_size) {
// more data to read
uint16_t to_read = msg_size - rx_buf_len_;
ssize_t received = this->socket_->read(&rx_buf_[rx_buf_len_], to_read);
APIError err = handle_socket_read_result_(received);
if (err != APIError::OK) {
return err;
}
rx_buf_len_ += static_cast<uint16_t>(received);
if (static_cast<uint16_t>(received) != to_read) {
// not all read
return APIError::WOULD_BLOCK;
}
}
LOG_PACKET_RECEIVED(rx_buf_);
*frame = std::move(rx_buf_);
// consume msg
rx_buf_ = {};
rx_buf_len_ = 0;
rx_header_buf_len_ = 0;
return APIError::OK;
}
/** To be called from read/write methods.
*
* This method runs through the internal handshake methods, if in that state.
*
* If the handshake is still active when this method returns and a read/write can't take place at
* the moment, returns WOULD_BLOCK.
* If an error occurred, returns that error. Only returns OK if the transport is ready for data
* traffic.
*/
APIError APINoiseFrameHelper::state_action_() {
int err;
APIError aerr;
if (state_ == State::INITIALIZE) {
HELPER_LOG("Bad state for method: %d", (int) state_);
return APIError::BAD_STATE;
}
if (state_ == State::CLIENT_HELLO) {
// waiting for client hello
std::vector<uint8_t> frame;
aerr = try_read_frame_(&frame);
if (aerr != APIError::OK) {
return handle_handshake_frame_error_(aerr);
}
// ignore contents, may be used in future for flags
// Resize for: existing prologue + 2 size bytes + frame data
size_t old_size = prologue_.size();
prologue_.resize(old_size + 2 + frame.size());
prologue_[old_size] = (uint8_t) (frame.size() >> 8);
prologue_[old_size + 1] = (uint8_t) frame.size();
std::memcpy(prologue_.data() + old_size + 2, frame.data(), frame.size());
state_ = State::SERVER_HELLO;
}
if (state_ == State::SERVER_HELLO) {
// send server hello
const std::string &name = App.get_name();
const std::string &mac = get_mac_address();
std::vector<uint8_t> msg;
// Calculate positions and sizes
size_t name_len = name.size() + 1; // including null terminator
size_t mac_len = mac.size() + 1; // including null terminator
size_t name_offset = 1;
size_t mac_offset = name_offset + name_len;
size_t total_size = 1 + name_len + mac_len;
msg.resize(total_size);
// chosen proto
msg[0] = 0x01;
// node name, terminated by null byte
std::memcpy(msg.data() + name_offset, name.c_str(), name_len);
// node mac, terminated by null byte
std::memcpy(msg.data() + mac_offset, mac.c_str(), mac_len);
aerr = write_frame_(msg.data(), msg.size());
if (aerr != APIError::OK)
return aerr;
// start handshake
aerr = init_handshake_();
if (aerr != APIError::OK)
return aerr;
state_ = State::HANDSHAKE;
}
if (state_ == State::HANDSHAKE) {
int action = noise_handshakestate_get_action(handshake_);
if (action == NOISE_ACTION_READ_MESSAGE) {
// waiting for handshake msg
std::vector<uint8_t> frame;
aerr = try_read_frame_(&frame);
if (aerr != APIError::OK) {
return handle_handshake_frame_error_(aerr);
}
if (frame.empty()) {
send_explicit_handshake_reject_("Empty handshake message");
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
} else if (frame[0] != 0x00) {
HELPER_LOG("Bad handshake error byte: %u", frame[0]);
send_explicit_handshake_reject_("Bad handshake error byte");
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
}
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_input(mbuf, frame.data() + 1, frame.size() - 1);
err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr);
if (err != 0) {
// Special handling for MAC failure
send_explicit_handshake_reject_(err == NOISE_ERROR_MAC_FAILURE ? "Handshake MAC failure" : "Handshake error");
return handle_noise_error_(err, "noise_handshakestate_read_message", APIError::HANDSHAKESTATE_READ_FAILED);
}
aerr = check_handshake_finished_();
if (aerr != APIError::OK)
return aerr;
} else if (action == NOISE_ACTION_WRITE_MESSAGE) {
uint8_t buffer[65];
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_output(mbuf, buffer + 1, sizeof(buffer) - 1);
err = noise_handshakestate_write_message(handshake_, &mbuf, nullptr);
APIError aerr_write =
handle_noise_error_(err, "noise_handshakestate_write_message", APIError::HANDSHAKESTATE_WRITE_FAILED);
if (aerr_write != APIError::OK)
return aerr_write;
buffer[0] = 0x00; // success
aerr = write_frame_(buffer, mbuf.size + 1);
if (aerr != APIError::OK)
return aerr;
aerr = check_handshake_finished_();
if (aerr != APIError::OK)
return aerr;
} else {
// bad state for action
state_ = State::FAILED;
HELPER_LOG("Bad action for handshake: %d", action);
return APIError::HANDSHAKESTATE_BAD_STATE;
}
}
if (state_ == State::CLOSED || state_ == State::FAILED) {
return APIError::BAD_STATE;
}
return APIError::OK;
}
void APINoiseFrameHelper::send_explicit_handshake_reject_(const std::string &reason) {
std::vector<uint8_t> data;
data.resize(reason.length() + 1);
data[0] = 0x01; // failure
// Copy error message in bulk
if (!reason.empty()) {
std::memcpy(data.data() + 1, reason.c_str(), reason.length());
}
// temporarily remove failed state
auto orig_state = state_;
state_ = State::EXPLICIT_REJECT;
write_frame_(data.data(), data.size());
state_ = orig_state;
}
APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
int err;
APIError aerr;
aerr = state_action_();
if (aerr != APIError::OK) {
return aerr;
}
if (state_ != State::DATA) {
return APIError::WOULD_BLOCK;
}
std::vector<uint8_t> frame;
aerr = try_read_frame_(&frame);
if (aerr != APIError::OK)
return aerr;
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_inout(mbuf, frame.data(), frame.size(), frame.size());
err = noise_cipherstate_decrypt(recv_cipher_, &mbuf);
APIError decrypt_err = handle_noise_error_(err, "noise_cipherstate_decrypt", APIError::CIPHERSTATE_DECRYPT_FAILED);
if (decrypt_err != APIError::OK)
return decrypt_err;
uint16_t msg_size = mbuf.size;
uint8_t *msg_data = frame.data();
if (msg_size < 4) {
state_ = State::FAILED;
HELPER_LOG("Bad data packet: size %d too short", msg_size);
return APIError::BAD_DATA_PACKET;
}
uint16_t type = (((uint16_t) msg_data[0]) << 8) | msg_data[1];
uint16_t data_len = (((uint16_t) msg_data[2]) << 8) | msg_data[3];
if (data_len > msg_size - 4) {
state_ = State::FAILED;
HELPER_LOG("Bad data packet: data_len %u greater than msg_size %u", data_len, msg_size);
return APIError::BAD_DATA_PACKET;
}
buffer->container = std::move(frame);
buffer->data_offset = 4;
buffer->data_len = data_len;
buffer->type = type;
return APIError::OK;
}
APIError APINoiseFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
// Resize to include MAC space (required for Noise encryption)
buffer.get_buffer()->resize(buffer.get_buffer()->size() + frame_footer_size_);
PacketInfo packet{type, 0,
static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)};
return write_protobuf_packets(buffer, std::span<const PacketInfo>(&packet, 1));
}
APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) {
APIError aerr = state_action_();
if (aerr != APIError::OK) {
return aerr;
}
if (state_ != State::DATA) {
return APIError::WOULD_BLOCK;
}
if (packets.empty()) {
return APIError::OK;
}
std::vector<uint8_t> *raw_buffer = buffer.get_buffer();
uint8_t *buffer_data = raw_buffer->data(); // Cache buffer pointer
this->reusable_iovs_.clear();
this->reusable_iovs_.reserve(packets.size());
uint16_t total_write_len = 0;
// We need to encrypt each packet in place
for (const auto &packet : packets) {
// The buffer already has padding at offset
uint8_t *buf_start = buffer_data + packet.offset;
// Write noise header
buf_start[0] = 0x01; // indicator
// buf_start[1], buf_start[2] to be set after encryption
// Write message header (to be encrypted)
const uint8_t msg_offset = 3;
buf_start[msg_offset] = static_cast<uint8_t>(packet.message_type >> 8); // type high byte
buf_start[msg_offset + 1] = static_cast<uint8_t>(packet.message_type); // type low byte
buf_start[msg_offset + 2] = static_cast<uint8_t>(packet.payload_size >> 8); // data_len high byte
buf_start[msg_offset + 3] = static_cast<uint8_t>(packet.payload_size); // data_len low byte
// payload data is already in the buffer starting at offset + 7
// Make sure we have space for MAC
// The buffer should already have been sized appropriately
// Encrypt the message in place
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_inout(mbuf, buf_start + msg_offset, 4 + packet.payload_size,
4 + packet.payload_size + frame_footer_size_);
int err = noise_cipherstate_encrypt(send_cipher_, &mbuf);
APIError aerr = handle_noise_error_(err, "noise_cipherstate_encrypt", APIError::CIPHERSTATE_ENCRYPT_FAILED);
if (aerr != APIError::OK)
return aerr;
// Fill in the encrypted size
buf_start[1] = static_cast<uint8_t>(mbuf.size >> 8);
buf_start[2] = static_cast<uint8_t>(mbuf.size);
// Add iovec for this encrypted packet
size_t packet_len = static_cast<size_t>(3 + mbuf.size); // indicator + size + encrypted data
this->reusable_iovs_.push_back({buf_start, packet_len});
total_write_len += packet_len;
}
// Send all encrypted packets in one writev call
return this->write_raw_(this->reusable_iovs_.data(), this->reusable_iovs_.size(), total_write_len);
}
APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) {
uint8_t header[3];
header[0] = 0x01; // indicator
header[1] = (uint8_t) (len >> 8);
header[2] = (uint8_t) len;
struct iovec iov[2];
iov[0].iov_base = header;
iov[0].iov_len = 3;
if (len == 0) {
return this->write_raw_(iov, 1, 3); // Just header
}
iov[1].iov_base = const_cast<uint8_t *>(data);
iov[1].iov_len = len;
return this->write_raw_(iov, 2, 3 + len); // Header + data
}
/** Initiate the data structures for the handshake.
*
* @return 0 on success, -1 on error (check errno)
*/
APIError APINoiseFrameHelper::init_handshake_() {
int err;
memset(&nid_, 0, sizeof(nid_));
// const char *proto = "Noise_NNpsk0_25519_ChaChaPoly_SHA256";
// err = noise_protocol_name_to_id(&nid_, proto, strlen(proto));
nid_.pattern_id = NOISE_PATTERN_NN;
nid_.cipher_id = NOISE_CIPHER_CHACHAPOLY;
nid_.dh_id = NOISE_DH_CURVE25519;
nid_.prefix_id = NOISE_PREFIX_STANDARD;
nid_.hybrid_id = NOISE_DH_NONE;
nid_.hash_id = NOISE_HASH_SHA256;
nid_.modifier_ids[0] = NOISE_MODIFIER_PSK0;
err = noise_handshakestate_new_by_id(&handshake_, &nid_, NOISE_ROLE_RESPONDER);
APIError aerr = handle_noise_error_(err, "noise_handshakestate_new_by_id", APIError::HANDSHAKESTATE_SETUP_FAILED);
if (aerr != APIError::OK)
return aerr;
const auto &psk = ctx_->get_psk();
err = noise_handshakestate_set_pre_shared_key(handshake_, psk.data(), psk.size());
aerr = handle_noise_error_(err, "noise_handshakestate_set_pre_shared_key", APIError::HANDSHAKESTATE_SETUP_FAILED);
if (aerr != APIError::OK)
return aerr;
err = noise_handshakestate_set_prologue(handshake_, prologue_.data(), prologue_.size());
aerr = handle_noise_error_(err, "noise_handshakestate_set_prologue", APIError::HANDSHAKESTATE_SETUP_FAILED);
if (aerr != APIError::OK)
return aerr;
// set_prologue copies it into handshakestate, so we can get rid of it now
prologue_ = {};
err = noise_handshakestate_start(handshake_);
aerr = handle_noise_error_(err, "noise_handshakestate_start", APIError::HANDSHAKESTATE_SETUP_FAILED);
if (aerr != APIError::OK)
return aerr;
return APIError::OK;
}
APIError APINoiseFrameHelper::check_handshake_finished_() {
assert(state_ == State::HANDSHAKE);
int action = noise_handshakestate_get_action(handshake_);
if (action == NOISE_ACTION_READ_MESSAGE || action == NOISE_ACTION_WRITE_MESSAGE)
return APIError::OK;
if (action != NOISE_ACTION_SPLIT) {
state_ = State::FAILED;
HELPER_LOG("Bad action for handshake: %d", action);
return APIError::HANDSHAKESTATE_BAD_STATE;
}
int err = noise_handshakestate_split(handshake_, &send_cipher_, &recv_cipher_);
APIError aerr = handle_noise_error_(err, "noise_handshakestate_split", APIError::HANDSHAKESTATE_SPLIT_FAILED);
if (aerr != APIError::OK)
return aerr;
frame_footer_size_ = noise_cipherstate_get_mac_length(send_cipher_);
HELPER_LOG("Handshake complete!");
noise_handshakestate_free(handshake_);
handshake_ = nullptr;
state_ = State::DATA;
return APIError::OK;
}
APINoiseFrameHelper::~APINoiseFrameHelper() {
if (handshake_ != nullptr) {
noise_handshakestate_free(handshake_);
handshake_ = nullptr;
}
if (send_cipher_ != nullptr) {
noise_cipherstate_free(send_cipher_);
send_cipher_ = nullptr;
}
if (recv_cipher_ != nullptr) {
noise_cipherstate_free(recv_cipher_);
recv_cipher_ = nullptr;
}
}
extern "C" {
// declare how noise generates random bytes (here with a good HWRNG based on the RF system)
void noise_rand_bytes(void *output, size_t len) {
if (!esphome::random_bytes(reinterpret_cast<uint8_t *>(output), len)) {
ESP_LOGE(TAG, "Acquiring random bytes failed; rebooting");
arch_restart();
}
}
}
} // namespace esphome::api
#endif // USE_API_NOISE
#endif // USE_API

View File

@ -0,0 +1,68 @@
#pragma once
#include "api_frame_helper.h"
#ifdef USE_API
#ifdef USE_API_NOISE
#include "noise/protocol.h"
#include "api_noise_context.h"
namespace esphome::api {
class APINoiseFrameHelper : public APIFrameHelper {
public:
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, std::shared_ptr<APINoiseContext> ctx,
const ClientInfo *client_info)
: APIFrameHelper(std::move(socket), client_info), ctx_(std::move(ctx)) {
// Noise header structure:
// Pos 0: indicator (0x01)
// Pos 1-2: encrypted payload size (16-bit big-endian)
// Pos 3-6: encrypted type (16-bit) + data_len (16-bit)
// Pos 7+: actual payload data
frame_header_padding_ = 7;
}
~APINoiseFrameHelper() override;
APIError init() override;
APIError loop() override;
APIError read_packet(ReadPacketBuffer *buffer) override;
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override;
// Get the frame header padding required by this protocol
uint8_t frame_header_padding() override { return frame_header_padding_; }
// Get the frame footer size required by this protocol
uint8_t frame_footer_size() override { return frame_footer_size_; }
protected:
APIError state_action_();
APIError try_read_frame_(std::vector<uint8_t> *frame);
APIError write_frame_(const uint8_t *data, uint16_t len);
APIError init_handshake_();
APIError check_handshake_finished_();
void send_explicit_handshake_reject_(const std::string &reason);
APIError handle_handshake_frame_error_(APIError aerr);
APIError handle_noise_error_(int err, const char *func_name, APIError api_err);
// Pointers first (4 bytes each)
NoiseHandshakeState *handshake_{nullptr};
NoiseCipherState *send_cipher_{nullptr};
NoiseCipherState *recv_cipher_{nullptr};
// Shared pointer (8 bytes on 32-bit = 4 bytes control block pointer + 4 bytes object pointer)
std::shared_ptr<APINoiseContext> ctx_;
// Vector (12 bytes on 32-bit)
std::vector<uint8_t> prologue_;
// NoiseProtocolId (size depends on implementation)
NoiseProtocolId nid_;
// Group small types together
// Fixed-size header buffer for noise protocol:
// 1 byte for indicator + 2 bytes for message size (16-bit value, not varint)
// Note: Maximum message size is UINT16_MAX (65535), with a limit of 128 bytes during handshake phase
uint8_t rx_header_buf_[3];
uint8_t rx_header_buf_len_ = 0;
// 4 bytes total, no padding
};
} // namespace esphome::api
#endif // USE_API_NOISE
#endif // USE_API

View File

@ -0,0 +1,290 @@
#include "api_frame_helper_plaintext.h"
#ifdef USE_API
#ifdef USE_API_PLAINTEXT
#include "api_connection.h" // For ClientInfo struct
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "proto.h"
#include <cstring>
#include <cinttypes>
namespace esphome::api {
static const char *const TAG = "api.plaintext";
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->client_info_->get_combined_info().c_str(), ##__VA_ARGS__)
#ifdef HELPER_LOG_PACKETS
#define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str())
#define LOG_PACKET_SENDING(data, len) ESP_LOGVV(TAG, "Sending raw: %s", format_hex_pretty(data, len).c_str())
#else
#define LOG_PACKET_RECEIVED(buffer) ((void) 0)
#define LOG_PACKET_SENDING(data, len) ((void) 0)
#endif
/// Initialize the frame helper, returns OK if successful.
APIError APIPlaintextFrameHelper::init() {
APIError err = init_common_();
if (err != APIError::OK) {
return err;
}
state_ = State::DATA;
return APIError::OK;
}
APIError APIPlaintextFrameHelper::loop() {
if (state_ != State::DATA) {
return APIError::BAD_STATE;
}
// Use base class implementation for buffer sending
return APIFrameHelper::loop();
}
/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter
*
* @param frame: The struct to hold the frame information in.
* msg: store the parsed frame in that struct
*
* @return See APIError
*
* error API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame.
*/
APIError APIPlaintextFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
if (frame == nullptr) {
HELPER_LOG("Bad argument for try_read_frame_");
return APIError::BAD_ARG;
}
// read header
while (!rx_header_parsed_) {
// Now that we know when the socket is ready, we can read up to 3 bytes
// into the rx_header_buf_ before we have to switch back to reading
// one byte at a time to ensure we don't read past the message and
// into the next one.
// Read directly into rx_header_buf_ at the current position
// Try to get to at least 3 bytes total (indicator + 2 varint bytes), then read one byte at a time
ssize_t received =
this->socket_->read(&rx_header_buf_[rx_header_buf_pos_], rx_header_buf_pos_ < 3 ? 3 - rx_header_buf_pos_ : 1);
APIError err = handle_socket_read_result_(received);
if (err != APIError::OK) {
return err;
}
// If this was the first read, validate the indicator byte
if (rx_header_buf_pos_ == 0 && received > 0) {
if (rx_header_buf_[0] != 0x00) {
state_ = State::FAILED;
HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]);
return APIError::BAD_INDICATOR;
}
}
rx_header_buf_pos_ += received;
// Check for buffer overflow
if (rx_header_buf_pos_ >= sizeof(rx_header_buf_)) {
state_ = State::FAILED;
HELPER_LOG("Header buffer overflow");
return APIError::BAD_DATA_PACKET;
}
// Need at least 3 bytes total (indicator + 2 varint bytes) before trying to parse
if (rx_header_buf_pos_ < 3) {
continue;
}
// At this point, we have at least 3 bytes total:
// - Validated indicator byte (0x00) stored at position 0
// - At least 2 bytes in the buffer for the varints
// Buffer layout:
// [0]: indicator byte (0x00)
// [1-3]: Message size varint (variable length)
// - 2 bytes would only allow up to 16383, which is less than noise's UINT16_MAX (65535)
// - 3 bytes allows up to 2097151, ensuring we support at least as much as noise
// [2-5]: Message type varint (variable length)
// We now attempt to parse both varints. If either is incomplete,
// we'll continue reading more bytes.
// Skip indicator byte at position 0
uint8_t varint_pos = 1;
uint32_t consumed = 0;
auto msg_size_varint = ProtoVarInt::parse(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos, &consumed);
if (!msg_size_varint.has_value()) {
// not enough data there yet
continue;
}
if (msg_size_varint->as_uint32() > std::numeric_limits<uint16_t>::max()) {
state_ = State::FAILED;
HELPER_LOG("Bad packet: message size %" PRIu32 " exceeds maximum %u", msg_size_varint->as_uint32(),
std::numeric_limits<uint16_t>::max());
return APIError::BAD_DATA_PACKET;
}
rx_header_parsed_len_ = msg_size_varint->as_uint16();
// Move to next varint position
varint_pos += consumed;
auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos, &consumed);
if (!msg_type_varint.has_value()) {
// not enough data there yet
continue;
}
if (msg_type_varint->as_uint32() > std::numeric_limits<uint16_t>::max()) {
state_ = State::FAILED;
HELPER_LOG("Bad packet: message type %" PRIu32 " exceeds maximum %u", msg_type_varint->as_uint32(),
std::numeric_limits<uint16_t>::max());
return APIError::BAD_DATA_PACKET;
}
rx_header_parsed_type_ = msg_type_varint->as_uint16();
rx_header_parsed_ = true;
}
// header reading done
// reserve space for body
if (rx_buf_.size() != rx_header_parsed_len_) {
rx_buf_.resize(rx_header_parsed_len_);
}
if (rx_buf_len_ < rx_header_parsed_len_) {
// more data to read
uint16_t to_read = rx_header_parsed_len_ - rx_buf_len_;
ssize_t received = this->socket_->read(&rx_buf_[rx_buf_len_], to_read);
APIError err = handle_socket_read_result_(received);
if (err != APIError::OK) {
return err;
}
rx_buf_len_ += static_cast<uint16_t>(received);
if (static_cast<uint16_t>(received) != to_read) {
// not all read
return APIError::WOULD_BLOCK;
}
}
LOG_PACKET_RECEIVED(rx_buf_);
*frame = std::move(rx_buf_);
// consume msg
rx_buf_ = {};
rx_buf_len_ = 0;
rx_header_buf_pos_ = 0;
rx_header_parsed_ = false;
return APIError::OK;
}
APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
APIError aerr;
if (state_ != State::DATA) {
return APIError::WOULD_BLOCK;
}
std::vector<uint8_t> frame;
aerr = try_read_frame_(&frame);
if (aerr != APIError::OK) {
if (aerr == APIError::BAD_INDICATOR) {
// Make sure to tell the remote that we don't
// understand the indicator byte so it knows
// we do not support it.
struct iovec iov[1];
// The \x00 first byte is the marker for plaintext.
//
// The remote will know how to handle the indicator byte,
// but it likely won't understand the rest of the message.
//
// We must send at least 3 bytes to be read, so we add
// a message after the indicator byte to ensures its long
// enough and can aid in debugging.
const char msg[] = "\x00"
"Bad indicator byte";
iov[0].iov_base = (void *) msg;
iov[0].iov_len = 19;
this->write_raw_(iov, 1, 19);
}
return aerr;
}
buffer->container = std::move(frame);
buffer->data_offset = 0;
buffer->data_len = rx_header_parsed_len_;
buffer->type = rx_header_parsed_type_;
return APIError::OK;
}
APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
PacketInfo packet{type, 0, static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_)};
return write_protobuf_packets(buffer, std::span<const PacketInfo>(&packet, 1));
}
APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) {
if (state_ != State::DATA) {
return APIError::BAD_STATE;
}
if (packets.empty()) {
return APIError::OK;
}
std::vector<uint8_t> *raw_buffer = buffer.get_buffer();
uint8_t *buffer_data = raw_buffer->data(); // Cache buffer pointer
this->reusable_iovs_.clear();
this->reusable_iovs_.reserve(packets.size());
uint16_t total_write_len = 0;
for (const auto &packet : packets) {
// Calculate varint sizes for header layout
uint8_t size_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(packet.payload_size));
uint8_t type_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(packet.message_type));
uint8_t total_header_len = 1 + size_varint_len + type_varint_len;
// Calculate where to start writing the header
// The header starts at the latest possible position to minimize unused padding
//
// Example 1 (small values): total_header_len = 3, header_offset = 6 - 3 = 3
// [0-2] - Unused padding
// [3] - 0x00 indicator byte
// [4] - Payload size varint (1 byte, for sizes 0-127)
// [5] - Message type varint (1 byte, for types 0-127)
// [6...] - Actual payload data
//
// Example 2 (medium values): total_header_len = 4, header_offset = 6 - 4 = 2
// [0-1] - Unused padding
// [2] - 0x00 indicator byte
// [3-4] - Payload size varint (2 bytes, for sizes 128-16383)
// [5] - Message type varint (1 byte, for types 0-127)
// [6...] - Actual payload data
//
// Example 3 (large values): total_header_len = 6, header_offset = 6 - 6 = 0
// [0] - 0x00 indicator byte
// [1-3] - Payload size varint (3 bytes, for sizes 16384-2097151)
// [4-5] - Message type varint (2 bytes, for types 128-32767)
// [6...] - Actual payload data
//
// The message starts at offset + frame_header_padding_
// So we write the header starting at offset + frame_header_padding_ - total_header_len
uint8_t *buf_start = buffer_data + packet.offset;
uint32_t header_offset = frame_header_padding_ - total_header_len;
// Write the plaintext header
buf_start[header_offset] = 0x00; // indicator
// Encode varints directly into buffer
ProtoVarInt(packet.payload_size).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len);
ProtoVarInt(packet.message_type)
.encode_to_buffer_unchecked(buf_start + header_offset + 1 + size_varint_len, type_varint_len);
// Add iovec for this packet (header + payload)
size_t packet_len = static_cast<size_t>(total_header_len + packet.payload_size);
this->reusable_iovs_.push_back({buf_start + header_offset, packet_len});
total_write_len += packet_len;
}
// Send all packets in one writev call
return write_raw_(this->reusable_iovs_.data(), this->reusable_iovs_.size(), total_write_len);
}
} // namespace esphome::api
#endif // USE_API_PLAINTEXT
#endif // USE_API

View File

@ -0,0 +1,53 @@
#pragma once
#include "api_frame_helper.h"
#ifdef USE_API
#ifdef USE_API_PLAINTEXT
namespace esphome::api {
class APIPlaintextFrameHelper : public APIFrameHelper {
public:
APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket, const ClientInfo *client_info)
: APIFrameHelper(std::move(socket), client_info) {
// Plaintext header structure (worst case):
// Pos 0: indicator (0x00)
// Pos 1-3: payload size varint (up to 3 bytes)
// Pos 4-5: message type varint (up to 2 bytes)
// Pos 6+: actual payload data
frame_header_padding_ = 6;
}
~APIPlaintextFrameHelper() override = default;
APIError init() override;
APIError loop() override;
APIError read_packet(ReadPacketBuffer *buffer) override;
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override;
uint8_t frame_header_padding() override { return frame_header_padding_; }
// Get the frame footer size required by this protocol
uint8_t frame_footer_size() override { return frame_footer_size_; }
protected:
APIError try_read_frame_(std::vector<uint8_t> *frame);
// Group 2-byte aligned types
uint16_t rx_header_parsed_type_ = 0;
uint16_t rx_header_parsed_len_ = 0;
// Group 1-byte types together
// Fixed-size header buffer for plaintext protocol:
// We now store the indicator byte + the two varints.
// To match noise protocol's maximum message size (UINT16_MAX = 65535), we need:
// 1 byte for indicator + 3 bytes for message size varint (supports up to 2097151) + 2 bytes for message type varint
//
// While varints could theoretically be up to 10 bytes each for 64-bit values,
// attempting to process messages with headers that large would likely crash the
// ESP32 due to memory constraints.
uint8_t rx_header_buf_[6]; // 1 byte indicator + 5 bytes for varints (3 for size + 2 for type)
uint8_t rx_header_buf_pos_ = 0;
bool rx_header_parsed_ = false;
// 8 bytes total, no padding needed
};
} // namespace esphome::api
#endif // USE_API_PLAINTEXT
#endif // USE_API

View File

@ -3,8 +3,7 @@
#include <cstdint> #include <cstdint>
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
namespace esphome { namespace esphome::api {
namespace api {
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
using psk_t = std::array<uint8_t, 32>; using psk_t = std::array<uint8_t, 32>;
@ -28,5 +27,4 @@ class APINoiseContext {
}; };
#endif // USE_API_NOISE #endif // USE_API_NOISE
} // namespace api } // namespace esphome::api
} // namespace esphome

File diff suppressed because it is too large Load Diff

View File

@ -3,11 +3,11 @@
#pragma once #pragma once
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#include "esphome/core/string_ref.h"
#include "proto.h" #include "proto.h"
namespace esphome { namespace esphome::api {
namespace api {
namespace enums { namespace enums {
@ -17,27 +17,13 @@ enum EntityCategory : uint32_t {
ENTITY_CATEGORY_DIAGNOSTIC = 2, ENTITY_CATEGORY_DIAGNOSTIC = 2,
}; };
#ifdef USE_COVER #ifdef USE_COVER
enum LegacyCoverState : uint32_t {
LEGACY_COVER_STATE_OPEN = 0,
LEGACY_COVER_STATE_CLOSED = 1,
};
enum CoverOperation : uint32_t { enum CoverOperation : uint32_t {
COVER_OPERATION_IDLE = 0, COVER_OPERATION_IDLE = 0,
COVER_OPERATION_IS_OPENING = 1, COVER_OPERATION_IS_OPENING = 1,
COVER_OPERATION_IS_CLOSING = 2, COVER_OPERATION_IS_CLOSING = 2,
}; };
enum LegacyCoverCommand : uint32_t {
LEGACY_COVER_COMMAND_OPEN = 0,
LEGACY_COVER_COMMAND_CLOSE = 1,
LEGACY_COVER_COMMAND_STOP = 2,
};
#endif #endif
#ifdef USE_FAN #ifdef USE_FAN
enum FanSpeed : uint32_t {
FAN_SPEED_LOW = 0,
FAN_SPEED_MEDIUM = 1,
FAN_SPEED_HIGH = 2,
};
enum FanDirection : uint32_t { enum FanDirection : uint32_t {
FAN_DIRECTION_FORWARD = 0, FAN_DIRECTION_FORWARD = 0,
FAN_DIRECTION_REVERSE = 1, FAN_DIRECTION_REVERSE = 1,
@ -65,11 +51,6 @@ enum SensorStateClass : uint32_t {
STATE_CLASS_TOTAL_INCREASING = 2, STATE_CLASS_TOTAL_INCREASING = 2,
STATE_CLASS_TOTAL = 3, STATE_CLASS_TOTAL = 3,
}; };
enum SensorLastResetType : uint32_t {
LAST_RESET_NONE = 0,
LAST_RESET_NEVER = 1,
LAST_RESET_AUTO = 2,
};
#endif #endif
enum LogLevel : uint32_t { enum LogLevel : uint32_t {
LOG_LEVEL_NONE = 0, LOG_LEVEL_NONE = 0,
@ -288,13 +269,20 @@ enum UpdateCommand : uint32_t {
class InfoResponseProtoMessage : public ProtoMessage { class InfoResponseProtoMessage : public ProtoMessage {
public: public:
~InfoResponseProtoMessage() override = default; ~InfoResponseProtoMessage() override = default;
std::string object_id{}; StringRef object_id_ref_{};
void set_object_id(const StringRef &ref) { this->object_id_ref_ = ref; }
uint32_t key{0}; uint32_t key{0};
std::string name{}; StringRef name_ref_{};
void set_name(const StringRef &ref) { this->name_ref_ = ref; }
bool disabled_by_default{false}; bool disabled_by_default{false};
std::string icon{}; #ifdef USE_ENTITY_ICON
StringRef icon_ref_{};
void set_icon(const StringRef &ref) { this->icon_ref_ = ref; }
#endif
enums::EntityCategory entity_category{}; enums::EntityCategory entity_category{};
#ifdef USE_DEVICES
uint32_t device_id{0}; uint32_t device_id{0};
#endif
protected: protected:
}; };
@ -303,7 +291,9 @@ class StateResponseProtoMessage : public ProtoMessage {
public: public:
~StateResponseProtoMessage() override = default; ~StateResponseProtoMessage() override = default;
uint32_t key{0}; uint32_t key{0};
#ifdef USE_DEVICES
uint32_t device_id{0}; uint32_t device_id{0};
#endif
protected: protected:
}; };
@ -312,7 +302,9 @@ class CommandProtoMessage : public ProtoDecodableMessage {
public: public:
~CommandProtoMessage() override = default; ~CommandProtoMessage() override = default;
uint32_t key{0}; uint32_t key{0};
#ifdef USE_DEVICES
uint32_t device_id{0}; uint32_t device_id{0};
#endif
protected: protected:
}; };
@ -343,8 +335,10 @@ class HelloResponse : public ProtoMessage {
#endif #endif
uint32_t api_version_major{0}; uint32_t api_version_major{0};
uint32_t api_version_minor{0}; uint32_t api_version_minor{0};
std::string server_info{}; StringRef server_info_ref_{};
std::string name{}; void set_server_info(const StringRef &ref) { this->server_info_ref_ = ref; }
StringRef name_ref_{};
void set_name(const StringRef &ref) { this->name_ref_ = ref; }
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -449,10 +443,12 @@ class DeviceInfoRequest : public ProtoDecodableMessage {
protected: protected:
}; };
#ifdef USE_AREAS
class AreaInfo : public ProtoMessage { class AreaInfo : public ProtoMessage {
public: public:
uint32_t area_id{0}; uint32_t area_id{0};
std::string name{}; StringRef name_ref_{};
void set_name(const StringRef &ref) { this->name_ref_ = ref; }
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -461,10 +457,13 @@ class AreaInfo : public ProtoMessage {
protected: protected:
}; };
#endif
#ifdef USE_DEVICES
class DeviceInfo : public ProtoMessage { class DeviceInfo : public ProtoMessage {
public: public:
uint32_t device_id{0}; uint32_t device_id{0};
std::string name{}; StringRef name_ref_{};
void set_name(const StringRef &ref) { this->name_ref_ = ref; }
uint32_t area_id{0}; uint32_t area_id{0};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
@ -474,50 +473,58 @@ class DeviceInfo : public ProtoMessage {
protected: protected:
}; };
#endif
class DeviceInfoResponse : public ProtoMessage { class DeviceInfoResponse : public ProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 10; static constexpr uint8_t MESSAGE_TYPE = 10;
static constexpr uint8_t ESTIMATED_SIZE = 219; static constexpr uint8_t ESTIMATED_SIZE = 211;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "device_info_response"; } const char *message_name() const override { return "device_info_response"; }
#endif #endif
#ifdef USE_API_PASSWORD
bool uses_password{false}; bool uses_password{false};
std::string name{}; #endif
std::string mac_address{}; StringRef name_ref_{};
std::string esphome_version{}; void set_name(const StringRef &ref) { this->name_ref_ = ref; }
std::string compilation_time{}; StringRef mac_address_ref_{};
std::string model{}; void set_mac_address(const StringRef &ref) { this->mac_address_ref_ = ref; }
StringRef esphome_version_ref_{};
void set_esphome_version(const StringRef &ref) { this->esphome_version_ref_ = ref; }
StringRef compilation_time_ref_{};
void set_compilation_time(const StringRef &ref) { this->compilation_time_ref_ = ref; }
StringRef model_ref_{};
void set_model(const StringRef &ref) { this->model_ref_ = ref; }
#ifdef USE_DEEP_SLEEP #ifdef USE_DEEP_SLEEP
bool has_deep_sleep{false}; bool has_deep_sleep{false};
#endif #endif
#ifdef ESPHOME_PROJECT_NAME #ifdef ESPHOME_PROJECT_NAME
std::string project_name{}; StringRef project_name_ref_{};
void set_project_name(const StringRef &ref) { this->project_name_ref_ = ref; }
#endif #endif
#ifdef ESPHOME_PROJECT_NAME #ifdef ESPHOME_PROJECT_NAME
std::string project_version{}; StringRef project_version_ref_{};
void set_project_version(const StringRef &ref) { this->project_version_ref_ = ref; }
#endif #endif
#ifdef USE_WEBSERVER #ifdef USE_WEBSERVER
uint32_t webserver_port{0}; uint32_t webserver_port{0};
#endif #endif
#ifdef USE_BLUETOOTH_PROXY
uint32_t legacy_bluetooth_proxy_version{0};
#endif
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
uint32_t bluetooth_proxy_feature_flags{0}; uint32_t bluetooth_proxy_feature_flags{0};
#endif #endif
std::string manufacturer{}; StringRef manufacturer_ref_{};
std::string friendly_name{}; void set_manufacturer(const StringRef &ref) { this->manufacturer_ref_ = ref; }
#ifdef USE_VOICE_ASSISTANT StringRef friendly_name_ref_{};
uint32_t legacy_voice_assistant_version{0}; void set_friendly_name(const StringRef &ref) { this->friendly_name_ref_ = ref; }
#endif
#ifdef USE_VOICE_ASSISTANT #ifdef USE_VOICE_ASSISTANT
uint32_t voice_assistant_feature_flags{0}; uint32_t voice_assistant_feature_flags{0};
#endif #endif
#ifdef USE_AREAS #ifdef USE_AREAS
std::string suggested_area{}; StringRef suggested_area_ref_{};
void set_suggested_area(const StringRef &ref) { this->suggested_area_ref_ = ref; }
#endif #endif
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
std::string bluetooth_mac_address{}; StringRef bluetooth_mac_address_ref_{};
void set_bluetooth_mac_address(const StringRef &ref) { this->bluetooth_mac_address_ref_ = ref; }
#endif #endif
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
bool api_encryption_supported{false}; bool api_encryption_supported{false};
@ -586,7 +593,8 @@ class ListEntitiesBinarySensorResponse : public InfoResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_binary_sensor_response"; } const char *message_name() const override { return "list_entities_binary_sensor_response"; }
#endif #endif
std::string device_class{}; StringRef device_class_ref_{};
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
bool is_status_binary_sensor{false}; bool is_status_binary_sensor{false};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
@ -625,7 +633,8 @@ class ListEntitiesCoverResponse : public InfoResponseProtoMessage {
bool assumed_state{false}; bool assumed_state{false};
bool supports_position{false}; bool supports_position{false};
bool supports_tilt{false}; bool supports_tilt{false};
std::string device_class{}; StringRef device_class_ref_{};
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
bool supports_stop{false}; bool supports_stop{false};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
@ -638,11 +647,10 @@ class ListEntitiesCoverResponse : public InfoResponseProtoMessage {
class CoverStateResponse : public StateResponseProtoMessage { class CoverStateResponse : public StateResponseProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 22; static constexpr uint8_t MESSAGE_TYPE = 22;
static constexpr uint8_t ESTIMATED_SIZE = 23; static constexpr uint8_t ESTIMATED_SIZE = 21;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "cover_state_response"; } const char *message_name() const override { return "cover_state_response"; }
#endif #endif
enums::LegacyCoverState legacy_state{};
float position{0.0f}; float position{0.0f};
float tilt{0.0f}; float tilt{0.0f};
enums::CoverOperation current_operation{}; enums::CoverOperation current_operation{};
@ -657,12 +665,10 @@ class CoverStateResponse : public StateResponseProtoMessage {
class CoverCommandRequest : public CommandProtoMessage { class CoverCommandRequest : public CommandProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 30; static constexpr uint8_t MESSAGE_TYPE = 30;
static constexpr uint8_t ESTIMATED_SIZE = 29; static constexpr uint8_t ESTIMATED_SIZE = 25;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "cover_command_request"; } const char *message_name() const override { return "cover_command_request"; }
#endif #endif
bool has_legacy_command{false};
enums::LegacyCoverCommand legacy_command{};
bool has_position{false}; bool has_position{false};
float position{0.0f}; float position{0.0f};
bool has_tilt{false}; bool has_tilt{false};
@ -701,16 +707,16 @@ class ListEntitiesFanResponse : public InfoResponseProtoMessage {
class FanStateResponse : public StateResponseProtoMessage { class FanStateResponse : public StateResponseProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 23; static constexpr uint8_t MESSAGE_TYPE = 23;
static constexpr uint8_t ESTIMATED_SIZE = 30; static constexpr uint8_t ESTIMATED_SIZE = 28;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "fan_state_response"; } const char *message_name() const override { return "fan_state_response"; }
#endif #endif
bool state{false}; bool state{false};
bool oscillating{false}; bool oscillating{false};
enums::FanSpeed speed{};
enums::FanDirection direction{}; enums::FanDirection direction{};
int32_t speed_level{0}; int32_t speed_level{0};
std::string preset_mode{}; StringRef preset_mode_ref_{};
void set_preset_mode(const StringRef &ref) { this->preset_mode_ref_ = ref; }
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -722,14 +728,12 @@ class FanStateResponse : public StateResponseProtoMessage {
class FanCommandRequest : public CommandProtoMessage { class FanCommandRequest : public CommandProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 31; static constexpr uint8_t MESSAGE_TYPE = 31;
static constexpr uint8_t ESTIMATED_SIZE = 42; static constexpr uint8_t ESTIMATED_SIZE = 38;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "fan_command_request"; } const char *message_name() const override { return "fan_command_request"; }
#endif #endif
bool has_state{false}; bool has_state{false};
bool state{false}; bool state{false};
bool has_speed{false};
enums::FanSpeed speed{};
bool has_oscillating{false}; bool has_oscillating{false};
bool oscillating{false}; bool oscillating{false};
bool has_direction{false}; bool has_direction{false};
@ -752,15 +756,11 @@ class FanCommandRequest : public CommandProtoMessage {
class ListEntitiesLightResponse : public InfoResponseProtoMessage { class ListEntitiesLightResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 15; static constexpr uint8_t MESSAGE_TYPE = 15;
static constexpr uint8_t ESTIMATED_SIZE = 81; static constexpr uint8_t ESTIMATED_SIZE = 73;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_light_response"; } const char *message_name() const override { return "list_entities_light_response"; }
#endif #endif
std::vector<enums::ColorMode> supported_color_modes{}; std::vector<enums::ColorMode> supported_color_modes{};
bool legacy_supports_brightness{false};
bool legacy_supports_rgb{false};
bool legacy_supports_white_value{false};
bool legacy_supports_color_temperature{false};
float min_mireds{0.0f}; float min_mireds{0.0f};
float max_mireds{0.0f}; float max_mireds{0.0f};
std::vector<std::string> effects{}; std::vector<std::string> effects{};
@ -790,7 +790,8 @@ class LightStateResponse : public StateResponseProtoMessage {
float color_temperature{0.0f}; float color_temperature{0.0f};
float cold_white{0.0f}; float cold_white{0.0f};
float warm_white{0.0f}; float warm_white{0.0f};
std::string effect{}; StringRef effect_ref_{};
void set_effect(const StringRef &ref) { this->effect_ref_ = ref; }
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -846,16 +847,17 @@ class LightCommandRequest : public CommandProtoMessage {
class ListEntitiesSensorResponse : public InfoResponseProtoMessage { class ListEntitiesSensorResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 16; static constexpr uint8_t MESSAGE_TYPE = 16;
static constexpr uint8_t ESTIMATED_SIZE = 68; static constexpr uint8_t ESTIMATED_SIZE = 66;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_sensor_response"; } const char *message_name() const override { return "list_entities_sensor_response"; }
#endif #endif
std::string unit_of_measurement{}; StringRef unit_of_measurement_ref_{};
void set_unit_of_measurement(const StringRef &ref) { this->unit_of_measurement_ref_ = ref; }
int32_t accuracy_decimals{0}; int32_t accuracy_decimals{0};
bool force_update{false}; bool force_update{false};
std::string device_class{}; StringRef device_class_ref_{};
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
enums::SensorStateClass state_class{}; enums::SensorStateClass state_class{};
enums::SensorLastResetType legacy_last_reset_type{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -891,7 +893,8 @@ class ListEntitiesSwitchResponse : public InfoResponseProtoMessage {
const char *message_name() const override { return "list_entities_switch_response"; } const char *message_name() const override { return "list_entities_switch_response"; }
#endif #endif
bool assumed_state{false}; bool assumed_state{false};
std::string device_class{}; StringRef device_class_ref_{};
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -941,7 +944,8 @@ class ListEntitiesTextSensorResponse : public InfoResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_text_sensor_response"; } const char *message_name() const override { return "list_entities_text_sensor_response"; }
#endif #endif
std::string device_class{}; StringRef device_class_ref_{};
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -957,7 +961,8 @@ class TextSensorStateResponse : public StateResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "text_sensor_state_response"; } const char *message_name() const override { return "text_sensor_state_response"; }
#endif #endif
std::string state{}; StringRef state_ref_{};
void set_state(const StringRef &ref) { this->state_ref_ = ref; }
bool missing_state{false}; bool missing_state{false};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
@ -987,13 +992,17 @@ class SubscribeLogsRequest : public ProtoDecodableMessage {
class SubscribeLogsResponse : public ProtoMessage { class SubscribeLogsResponse : public ProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 29; static constexpr uint8_t MESSAGE_TYPE = 29;
static constexpr uint8_t ESTIMATED_SIZE = 13; static constexpr uint8_t ESTIMATED_SIZE = 11;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "subscribe_logs_response"; } const char *message_name() const override { return "subscribe_logs_response"; }
#endif #endif
enums::LogLevel level{}; enums::LogLevel level{};
std::string message{}; const uint8_t *message_ptr_{nullptr};
bool send_failed{false}; size_t message_len_{0};
void set_message(const uint8_t *data, size_t len) {
this->message_ptr_ = data;
this->message_len_ = len;
}
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -1050,8 +1059,10 @@ class SubscribeHomeassistantServicesRequest : public ProtoDecodableMessage {
}; };
class HomeassistantServiceMap : public ProtoMessage { class HomeassistantServiceMap : public ProtoMessage {
public: public:
std::string key{}; StringRef key_ref_{};
std::string value{}; void set_key(const StringRef &ref) { this->key_ref_ = ref; }
StringRef value_ref_{};
void set_value(const StringRef &ref) { this->value_ref_ = ref; }
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -1067,7 +1078,8 @@ class HomeassistantServiceResponse : public ProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "homeassistant_service_response"; } const char *message_name() const override { return "homeassistant_service_response"; }
#endif #endif
std::string service{}; StringRef service_ref_{};
void set_service(const StringRef &ref) { this->service_ref_ = ref; }
std::vector<HomeassistantServiceMap> data{}; std::vector<HomeassistantServiceMap> data{};
std::vector<HomeassistantServiceMap> data_template{}; std::vector<HomeassistantServiceMap> data_template{};
std::vector<HomeassistantServiceMap> variables{}; std::vector<HomeassistantServiceMap> variables{};
@ -1100,8 +1112,10 @@ class SubscribeHomeAssistantStateResponse : public ProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "subscribe_home_assistant_state_response"; } const char *message_name() const override { return "subscribe_home_assistant_state_response"; }
#endif #endif
std::string entity_id{}; StringRef entity_id_ref_{};
std::string attribute{}; void set_entity_id(const StringRef &ref) { this->entity_id_ref_ = ref; }
StringRef attribute_ref_{};
void set_attribute(const StringRef &ref) { this->attribute_ref_ = ref; }
bool once{false}; bool once{false};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
@ -1161,7 +1175,8 @@ class GetTimeResponse : public ProtoDecodableMessage {
#ifdef USE_API_SERVICES #ifdef USE_API_SERVICES
class ListEntitiesServicesArgument : public ProtoMessage { class ListEntitiesServicesArgument : public ProtoMessage {
public: public:
std::string name{}; StringRef name_ref_{};
void set_name(const StringRef &ref) { this->name_ref_ = ref; }
enums::ServiceArgType type{}; enums::ServiceArgType type{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
@ -1178,7 +1193,8 @@ class ListEntitiesServicesResponse : public ProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_services_response"; } const char *message_name() const override { return "list_entities_services_response"; }
#endif #endif
std::string name{}; StringRef name_ref_{};
void set_name(const StringRef &ref) { this->name_ref_ = ref; }
uint32_t key{0}; uint32_t key{0};
std::vector<ListEntitiesServicesArgument> args{}; std::vector<ListEntitiesServicesArgument> args{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
@ -1250,7 +1266,12 @@ class CameraImageResponse : public StateResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "camera_image_response"; } const char *message_name() const override { return "camera_image_response"; }
#endif #endif
std::string data{}; const uint8_t *data_ptr_{nullptr};
size_t data_len_{0};
void set_data(const uint8_t *data, size_t len) {
this->data_ptr_ = data;
this->data_len_ = len;
}
bool done{false}; bool done{false};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
@ -1281,7 +1302,7 @@ class CameraImageRequest : public ProtoDecodableMessage {
class ListEntitiesClimateResponse : public InfoResponseProtoMessage { class ListEntitiesClimateResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 46; static constexpr uint8_t MESSAGE_TYPE = 46;
static constexpr uint8_t ESTIMATED_SIZE = 147; static constexpr uint8_t ESTIMATED_SIZE = 145;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_climate_response"; } const char *message_name() const override { return "list_entities_climate_response"; }
#endif #endif
@ -1291,7 +1312,6 @@ class ListEntitiesClimateResponse : public InfoResponseProtoMessage {
float visual_min_temperature{0.0f}; float visual_min_temperature{0.0f};
float visual_max_temperature{0.0f}; float visual_max_temperature{0.0f};
float visual_target_temperature_step{0.0f}; float visual_target_temperature_step{0.0f};
bool legacy_supports_away{false};
bool supports_action{false}; bool supports_action{false};
std::vector<enums::ClimateFanMode> supported_fan_modes{}; std::vector<enums::ClimateFanMode> supported_fan_modes{};
std::vector<enums::ClimateSwingMode> supported_swing_modes{}; std::vector<enums::ClimateSwingMode> supported_swing_modes{};
@ -1314,7 +1334,7 @@ class ListEntitiesClimateResponse : public InfoResponseProtoMessage {
class ClimateStateResponse : public StateResponseProtoMessage { class ClimateStateResponse : public StateResponseProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 47; static constexpr uint8_t MESSAGE_TYPE = 47;
static constexpr uint8_t ESTIMATED_SIZE = 70; static constexpr uint8_t ESTIMATED_SIZE = 68;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "climate_state_response"; } const char *message_name() const override { return "climate_state_response"; }
#endif #endif
@ -1323,13 +1343,14 @@ class ClimateStateResponse : public StateResponseProtoMessage {
float target_temperature{0.0f}; float target_temperature{0.0f};
float target_temperature_low{0.0f}; float target_temperature_low{0.0f};
float target_temperature_high{0.0f}; float target_temperature_high{0.0f};
bool unused_legacy_away{false};
enums::ClimateAction action{}; enums::ClimateAction action{};
enums::ClimateFanMode fan_mode{}; enums::ClimateFanMode fan_mode{};
enums::ClimateSwingMode swing_mode{}; enums::ClimateSwingMode swing_mode{};
std::string custom_fan_mode{}; StringRef custom_fan_mode_ref_{};
void set_custom_fan_mode(const StringRef &ref) { this->custom_fan_mode_ref_ = ref; }
enums::ClimatePreset preset{}; enums::ClimatePreset preset{};
std::string custom_preset{}; StringRef custom_preset_ref_{};
void set_custom_preset(const StringRef &ref) { this->custom_preset_ref_ = ref; }
float current_humidity{0.0f}; float current_humidity{0.0f};
float target_humidity{0.0f}; float target_humidity{0.0f};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
@ -1343,7 +1364,7 @@ class ClimateStateResponse : public StateResponseProtoMessage {
class ClimateCommandRequest : public CommandProtoMessage { class ClimateCommandRequest : public CommandProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 48; static constexpr uint8_t MESSAGE_TYPE = 48;
static constexpr uint8_t ESTIMATED_SIZE = 88; static constexpr uint8_t ESTIMATED_SIZE = 84;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "climate_command_request"; } const char *message_name() const override { return "climate_command_request"; }
#endif #endif
@ -1355,8 +1376,6 @@ class ClimateCommandRequest : public CommandProtoMessage {
float target_temperature_low{0.0f}; float target_temperature_low{0.0f};
bool has_target_temperature_high{false}; bool has_target_temperature_high{false};
float target_temperature_high{0.0f}; float target_temperature_high{0.0f};
bool unused_has_legacy_away{false};
bool unused_legacy_away{false};
bool has_fan_mode{false}; bool has_fan_mode{false};
enums::ClimateFanMode fan_mode{}; enums::ClimateFanMode fan_mode{};
bool has_swing_mode{false}; bool has_swing_mode{false};
@ -1390,9 +1409,11 @@ class ListEntitiesNumberResponse : public InfoResponseProtoMessage {
float min_value{0.0f}; float min_value{0.0f};
float max_value{0.0f}; float max_value{0.0f};
float step{0.0f}; float step{0.0f};
std::string unit_of_measurement{}; StringRef unit_of_measurement_ref_{};
void set_unit_of_measurement(const StringRef &ref) { this->unit_of_measurement_ref_ = ref; }
enums::NumberMode mode{}; enums::NumberMode mode{};
std::string device_class{}; StringRef device_class_ref_{};
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -1459,7 +1480,8 @@ class SelectStateResponse : public StateResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "select_state_response"; } const char *message_name() const override { return "select_state_response"; }
#endif #endif
std::string state{}; StringRef state_ref_{};
void set_state(const StringRef &ref) { this->state_ref_ = ref; }
bool missing_state{false}; bool missing_state{false};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
@ -1558,7 +1580,8 @@ class ListEntitiesLockResponse : public InfoResponseProtoMessage {
bool assumed_state{false}; bool assumed_state{false};
bool supports_open{false}; bool supports_open{false};
bool requires_code{false}; bool requires_code{false};
std::string code_format{}; StringRef code_format_ref_{};
void set_code_format(const StringRef &ref) { this->code_format_ref_ = ref; }
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -1611,7 +1634,8 @@ class ListEntitiesButtonResponse : public InfoResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_button_response"; } const char *message_name() const override { return "list_entities_button_response"; }
#endif #endif
std::string device_class{}; StringRef device_class_ref_{};
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -1639,7 +1663,8 @@ class ButtonCommandRequest : public CommandProtoMessage {
#ifdef USE_MEDIA_PLAYER #ifdef USE_MEDIA_PLAYER
class MediaPlayerSupportedFormat : public ProtoMessage { class MediaPlayerSupportedFormat : public ProtoMessage {
public: public:
std::string format{}; StringRef format_ref_{};
void set_format(const StringRef &ref) { this->format_ref_ = ref; }
uint32_t sample_rate{0}; uint32_t sample_rate{0};
uint32_t num_channels{0}; uint32_t num_channels{0};
enums::MediaPlayerFormatPurpose purpose{}; enums::MediaPlayerFormatPurpose purpose{};
@ -1728,41 +1753,6 @@ class SubscribeBluetoothLEAdvertisementsRequest : public ProtoDecodableMessage {
protected: protected:
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class BluetoothServiceData : public ProtoMessage {
public:
std::string uuid{};
std::vector<uint32_t> legacy_data{};
std::string data{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
};
class BluetoothLEAdvertisementResponse : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 67;
static constexpr uint8_t ESTIMATED_SIZE = 107;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "bluetooth_le_advertisement_response"; }
#endif
uint64_t address{0};
std::string name{};
int32_t rssi{0};
std::vector<std::string> service_uuids{};
std::vector<BluetoothServiceData> service_data{};
std::vector<BluetoothServiceData> manufacturer_data{};
uint32_t address_type{0};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
};
class BluetoothLERawAdvertisement : public ProtoMessage { class BluetoothLERawAdvertisement : public ProtoMessage {
public: public:
uint64_t address{0}; uint64_t address{0};
@ -1848,7 +1838,7 @@ class BluetoothGATTGetServicesRequest : public ProtoDecodableMessage {
}; };
class BluetoothGATTDescriptor : public ProtoMessage { class BluetoothGATTDescriptor : public ProtoMessage {
public: public:
std::vector<uint64_t> uuid{}; std::array<uint64_t, 2> uuid{};
uint32_t handle{0}; uint32_t handle{0};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
@ -1860,7 +1850,7 @@ class BluetoothGATTDescriptor : public ProtoMessage {
}; };
class BluetoothGATTCharacteristic : public ProtoMessage { class BluetoothGATTCharacteristic : public ProtoMessage {
public: public:
std::vector<uint64_t> uuid{}; std::array<uint64_t, 2> uuid{};
uint32_t handle{0}; uint32_t handle{0};
uint32_t properties{0}; uint32_t properties{0};
std::vector<BluetoothGATTDescriptor> descriptors{}; std::vector<BluetoothGATTDescriptor> descriptors{};
@ -1874,7 +1864,7 @@ class BluetoothGATTCharacteristic : public ProtoMessage {
}; };
class BluetoothGATTService : public ProtoMessage { class BluetoothGATTService : public ProtoMessage {
public: public:
std::vector<uint64_t> uuid{}; std::array<uint64_t, 2> uuid{};
uint32_t handle{0}; uint32_t handle{0};
std::vector<BluetoothGATTCharacteristic> characteristics{}; std::vector<BluetoothGATTCharacteristic> characteristics{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
@ -1888,12 +1878,12 @@ class BluetoothGATTService : public ProtoMessage {
class BluetoothGATTGetServicesResponse : public ProtoMessage { class BluetoothGATTGetServicesResponse : public ProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 71; static constexpr uint8_t MESSAGE_TYPE = 71;
static constexpr uint8_t ESTIMATED_SIZE = 38; static constexpr uint8_t ESTIMATED_SIZE = 21;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "bluetooth_gatt_get_services_response"; } const char *message_name() const override { return "bluetooth_gatt_get_services_response"; }
#endif #endif
uint64_t address{0}; uint64_t address{0};
std::vector<BluetoothGATTService> services{}; std::array<BluetoothGATTService, 1> services{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -1943,7 +1933,12 @@ class BluetoothGATTReadResponse : public ProtoMessage {
#endif #endif
uint64_t address{0}; uint64_t address{0};
uint32_t handle{0}; uint32_t handle{0};
std::string data{}; const uint8_t *data_ptr_{nullptr};
size_t data_len_{0};
void set_data(const uint8_t *data, size_t len) {
this->data_ptr_ = data;
this->data_len_ = len;
}
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -2031,7 +2026,12 @@ class BluetoothGATTNotifyDataResponse : public ProtoMessage {
#endif #endif
uint64_t address{0}; uint64_t address{0};
uint32_t handle{0}; uint32_t handle{0};
std::string data{}; const uint8_t *data_ptr_{nullptr};
size_t data_len_{0};
void set_data(const uint8_t *data, size_t len) {
this->data_ptr_ = data;
this->data_len_ = len;
}
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -2261,10 +2261,12 @@ class VoiceAssistantRequest : public ProtoMessage {
const char *message_name() const override { return "voice_assistant_request"; } const char *message_name() const override { return "voice_assistant_request"; }
#endif #endif
bool start{false}; bool start{false};
std::string conversation_id{}; StringRef conversation_id_ref_{};
void set_conversation_id(const StringRef &ref) { this->conversation_id_ref_ = ref; }
uint32_t flags{0}; uint32_t flags{0};
VoiceAssistantAudioSettings audio_settings{}; VoiceAssistantAudioSettings audio_settings{};
std::string wake_word_phrase{}; StringRef wake_word_phrase_ref_{};
void set_wake_word_phrase(const StringRef &ref) { this->wake_word_phrase_ref_ = ref; }
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -2325,6 +2327,12 @@ class VoiceAssistantAudio : public ProtoDecodableMessage {
const char *message_name() const override { return "voice_assistant_audio"; } const char *message_name() const override { return "voice_assistant_audio"; }
#endif #endif
std::string data{}; std::string data{};
const uint8_t *data_ptr_{nullptr};
size_t data_len_{0};
void set_data(const uint8_t *data, size_t len) {
this->data_ptr_ = data;
this->data_len_ = len;
}
bool end{false}; bool end{false};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
@ -2394,8 +2402,10 @@ class VoiceAssistantAnnounceFinished : public ProtoMessage {
}; };
class VoiceAssistantWakeWord : public ProtoMessage { class VoiceAssistantWakeWord : public ProtoMessage {
public: public:
std::string id{}; StringRef id_ref_{};
std::string wake_word{}; void set_id(const StringRef &ref) { this->id_ref_ = ref; }
StringRef wake_word_ref_{};
void set_wake_word(const StringRef &ref) { this->wake_word_ref_ = ref; }
std::vector<std::string> trained_languages{}; std::vector<std::string> trained_languages{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
@ -2516,7 +2526,8 @@ class ListEntitiesTextResponse : public InfoResponseProtoMessage {
#endif #endif
uint32_t min_length{0}; uint32_t min_length{0};
uint32_t max_length{0}; uint32_t max_length{0};
std::string pattern{}; StringRef pattern_ref_{};
void set_pattern(const StringRef &ref) { this->pattern_ref_ = ref; }
enums::TextMode mode{}; enums::TextMode mode{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
@ -2533,7 +2544,8 @@ class TextStateResponse : public StateResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "text_state_response"; } const char *message_name() const override { return "text_state_response"; }
#endif #endif
std::string state{}; StringRef state_ref_{};
void set_state(const StringRef &ref) { this->state_ref_ = ref; }
bool missing_state{false}; bool missing_state{false};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
@ -2677,7 +2689,8 @@ class ListEntitiesEventResponse : public InfoResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_event_response"; } const char *message_name() const override { return "list_entities_event_response"; }
#endif #endif
std::string device_class{}; StringRef device_class_ref_{};
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
std::vector<std::string> event_types{}; std::vector<std::string> event_types{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
@ -2694,7 +2707,8 @@ class EventResponse : public StateResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "event_response"; } const char *message_name() const override { return "event_response"; }
#endif #endif
std::string event_type{}; StringRef event_type_ref_{};
void set_event_type(const StringRef &ref) { this->event_type_ref_ = ref; }
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -2712,7 +2726,8 @@ class ListEntitiesValveResponse : public InfoResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_valve_response"; } const char *message_name() const override { return "list_entities_valve_response"; }
#endif #endif
std::string device_class{}; StringRef device_class_ref_{};
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
bool assumed_state{false}; bool assumed_state{false};
bool supports_position{false}; bool supports_position{false};
bool supports_stop{false}; bool supports_stop{false};
@ -2818,7 +2833,8 @@ class ListEntitiesUpdateResponse : public InfoResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_update_response"; } const char *message_name() const override { return "list_entities_update_response"; }
#endif #endif
std::string device_class{}; StringRef device_class_ref_{};
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -2838,11 +2854,16 @@ class UpdateStateResponse : public StateResponseProtoMessage {
bool in_progress{false}; bool in_progress{false};
bool has_progress{false}; bool has_progress{false};
float progress{0.0f}; float progress{0.0f};
std::string current_version{}; StringRef current_version_ref_{};
std::string latest_version{}; void set_current_version(const StringRef &ref) { this->current_version_ref_ = ref; }
std::string title{}; StringRef latest_version_ref_{};
std::string release_summary{}; void set_latest_version(const StringRef &ref) { this->latest_version_ref_ = ref; }
std::string release_url{}; StringRef title_ref_{};
void set_title(const StringRef &ref) { this->title_ref_ = ref; }
StringRef release_summary_ref_{};
void set_release_summary(const StringRef &ref) { this->release_summary_ref_ = ref; }
StringRef release_url_ref_{};
void set_release_url(const StringRef &ref) { this->release_url_ref_ = ref; }
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -2869,5 +2890,4 @@ class UpdateCommandRequest : public CommandProtoMessage {
}; };
#endif #endif
} // namespace api } // namespace esphome::api
} // namespace esphome

File diff suppressed because it is too large Load Diff

View File

@ -3,8 +3,7 @@
#include "api_pb2_service.h" #include "api_pb2_service.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
namespace esphome { namespace esphome::api {
namespace api {
static const char *const TAG = "api.service"; static const char *const TAG = "api.service";
@ -16,7 +15,7 @@ void APIServerConnectionBase::log_send_message_(const char *name, const std::str
void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) { void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {
switch (msg_type) { switch (msg_type) {
case 1: { case HelloRequest::MESSAGE_TYPE: {
HelloRequest msg; HelloRequest msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -25,7 +24,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
this->on_hello_request(msg); this->on_hello_request(msg);
break; break;
} }
case 3: { case ConnectRequest::MESSAGE_TYPE: {
ConnectRequest msg; ConnectRequest msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -34,7 +33,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
this->on_connect_request(msg); this->on_connect_request(msg);
break; break;
} }
case 5: { case DisconnectRequest::MESSAGE_TYPE: {
DisconnectRequest msg; DisconnectRequest msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -43,7 +42,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
this->on_disconnect_request(msg); this->on_disconnect_request(msg);
break; break;
} }
case 6: { case DisconnectResponse::MESSAGE_TYPE: {
DisconnectResponse msg; DisconnectResponse msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -52,7 +51,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
this->on_disconnect_response(msg); this->on_disconnect_response(msg);
break; break;
} }
case 7: { case PingRequest::MESSAGE_TYPE: {
PingRequest msg; PingRequest msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -61,7 +60,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
this->on_ping_request(msg); this->on_ping_request(msg);
break; break;
} }
case 8: { case PingResponse::MESSAGE_TYPE: {
PingResponse msg; PingResponse msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -70,7 +69,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
this->on_ping_response(msg); this->on_ping_response(msg);
break; break;
} }
case 9: { case DeviceInfoRequest::MESSAGE_TYPE: {
DeviceInfoRequest msg; DeviceInfoRequest msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -79,7 +78,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
this->on_device_info_request(msg); this->on_device_info_request(msg);
break; break;
} }
case 11: { case ListEntitiesRequest::MESSAGE_TYPE: {
ListEntitiesRequest msg; ListEntitiesRequest msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -88,7 +87,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
this->on_list_entities_request(msg); this->on_list_entities_request(msg);
break; break;
} }
case 20: { case SubscribeStatesRequest::MESSAGE_TYPE: {
SubscribeStatesRequest msg; SubscribeStatesRequest msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -97,7 +96,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
this->on_subscribe_states_request(msg); this->on_subscribe_states_request(msg);
break; break;
} }
case 28: { case SubscribeLogsRequest::MESSAGE_TYPE: {
SubscribeLogsRequest msg; SubscribeLogsRequest msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -107,7 +106,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
break; break;
} }
#ifdef USE_COVER #ifdef USE_COVER
case 30: { case CoverCommandRequest::MESSAGE_TYPE: {
CoverCommandRequest msg; CoverCommandRequest msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -118,7 +117,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
#endif #endif
#ifdef USE_FAN #ifdef USE_FAN
case 31: { case FanCommandRequest::MESSAGE_TYPE: {
FanCommandRequest msg; FanCommandRequest msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -129,7 +128,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
#endif #endif
#ifdef USE_LIGHT #ifdef USE_LIGHT
case 32: { case LightCommandRequest::MESSAGE_TYPE: {
LightCommandRequest msg; LightCommandRequest msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -140,7 +139,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
#endif #endif
#ifdef USE_SWITCH #ifdef USE_SWITCH
case 33: { case SwitchCommandRequest::MESSAGE_TYPE: {
SwitchCommandRequest msg; SwitchCommandRequest msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -150,7 +149,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
break; break;
} }
#endif #endif
case 34: { case SubscribeHomeassistantServicesRequest::MESSAGE_TYPE: {
SubscribeHomeassistantServicesRequest msg; SubscribeHomeassistantServicesRequest msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -159,7 +158,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
this->on_subscribe_homeassistant_services_request(msg); this->on_subscribe_homeassistant_services_request(msg);
break; break;
} }
case 36: { case GetTimeRequest::MESSAGE_TYPE: {
GetTimeRequest msg; GetTimeRequest msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -168,7 +167,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
this->on_get_time_request(msg); this->on_get_time_request(msg);
break; break;
} }
case 37: { case GetTimeResponse::MESSAGE_TYPE: {
GetTimeResponse msg; GetTimeResponse msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -177,7 +176,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
this->on_get_time_response(msg); this->on_get_time_response(msg);
break; break;
} }
case 38: { case SubscribeHomeAssistantStatesRequest::MESSAGE_TYPE: {
SubscribeHomeAssistantStatesRequest msg; SubscribeHomeAssistantStatesRequest msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -186,7 +185,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
this->on_subscribe_home_assistant_states_request(msg); this->on_subscribe_home_assistant_states_request(msg);
break; break;
} }
case 40: { case HomeAssistantStateResponse::MESSAGE_TYPE: {
HomeAssistantStateResponse msg; HomeAssistantStateResponse msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -196,7 +195,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
break; break;
} }
#ifdef USE_API_SERVICES #ifdef USE_API_SERVICES
case 42: { case ExecuteServiceRequest::MESSAGE_TYPE: {
ExecuteServiceRequest msg; ExecuteServiceRequest msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -207,7 +206,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
#endif #endif
#ifdef USE_CAMERA #ifdef USE_CAMERA
case 45: { case CameraImageRequest::MESSAGE_TYPE: {
CameraImageRequest msg; CameraImageRequest msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -218,7 +217,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
#endif #endif
#ifdef USE_CLIMATE #ifdef USE_CLIMATE
case 48: { case ClimateCommandRequest::MESSAGE_TYPE: {
ClimateCommandRequest msg; ClimateCommandRequest msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -229,7 +228,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
#endif #endif
#ifdef USE_NUMBER #ifdef USE_NUMBER
case 51: { case NumberCommandRequest::MESSAGE_TYPE: {
NumberCommandRequest msg; NumberCommandRequest msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -240,7 +239,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
#endif #endif
#ifdef USE_SELECT #ifdef USE_SELECT
case 54: { case SelectCommandRequest::MESSAGE_TYPE: {
SelectCommandRequest msg; SelectCommandRequest msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -251,7 +250,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
#endif #endif
#ifdef USE_SIREN #ifdef USE_SIREN
case 57: { case SirenCommandRequest::MESSAGE_TYPE: {
SirenCommandRequest msg; SirenCommandRequest msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -262,7 +261,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
#endif #endif
#ifdef USE_LOCK #ifdef USE_LOCK
case 60: { case LockCommandRequest::MESSAGE_TYPE: {
LockCommandRequest msg; LockCommandRequest msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -273,7 +272,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
#endif #endif
#ifdef USE_BUTTON #ifdef USE_BUTTON
case 62: { case ButtonCommandRequest::MESSAGE_TYPE: {
ButtonCommandRequest msg; ButtonCommandRequest msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -284,7 +283,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
#endif #endif
#ifdef USE_MEDIA_PLAYER #ifdef USE_MEDIA_PLAYER
case 65: { case MediaPlayerCommandRequest::MESSAGE_TYPE: {
MediaPlayerCommandRequest msg; MediaPlayerCommandRequest msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -295,7 +294,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
#endif #endif
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
case 66: { case SubscribeBluetoothLEAdvertisementsRequest::MESSAGE_TYPE: {
SubscribeBluetoothLEAdvertisementsRequest msg; SubscribeBluetoothLEAdvertisementsRequest msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -306,7 +305,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
#endif #endif
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
case 68: { case BluetoothDeviceRequest::MESSAGE_TYPE: {
BluetoothDeviceRequest msg; BluetoothDeviceRequest msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -317,7 +316,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
#endif #endif
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
case 70: { case BluetoothGATTGetServicesRequest::MESSAGE_TYPE: {
BluetoothGATTGetServicesRequest msg; BluetoothGATTGetServicesRequest msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -328,7 +327,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
#endif #endif
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
case 73: { case BluetoothGATTReadRequest::MESSAGE_TYPE: {
BluetoothGATTReadRequest msg; BluetoothGATTReadRequest msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -339,7 +338,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
#endif #endif
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
case 75: { case BluetoothGATTWriteRequest::MESSAGE_TYPE: {
BluetoothGATTWriteRequest msg; BluetoothGATTWriteRequest msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -350,7 +349,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
#endif #endif
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
case 76: { case BluetoothGATTReadDescriptorRequest::MESSAGE_TYPE: {
BluetoothGATTReadDescriptorRequest msg; BluetoothGATTReadDescriptorRequest msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -361,7 +360,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
#endif #endif
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
case 77: { case BluetoothGATTWriteDescriptorRequest::MESSAGE_TYPE: {
BluetoothGATTWriteDescriptorRequest msg; BluetoothGATTWriteDescriptorRequest msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -372,7 +371,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
#endif #endif
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
case 78: { case BluetoothGATTNotifyRequest::MESSAGE_TYPE: {
BluetoothGATTNotifyRequest msg; BluetoothGATTNotifyRequest msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -383,7 +382,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
#endif #endif
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
case 80: { case SubscribeBluetoothConnectionsFreeRequest::MESSAGE_TYPE: {
SubscribeBluetoothConnectionsFreeRequest msg; SubscribeBluetoothConnectionsFreeRequest msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -394,7 +393,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
#endif #endif
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
case 87: { case UnsubscribeBluetoothLEAdvertisementsRequest::MESSAGE_TYPE: {
UnsubscribeBluetoothLEAdvertisementsRequest msg; UnsubscribeBluetoothLEAdvertisementsRequest msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -405,7 +404,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
#endif #endif
#ifdef USE_VOICE_ASSISTANT #ifdef USE_VOICE_ASSISTANT
case 89: { case SubscribeVoiceAssistantRequest::MESSAGE_TYPE: {
SubscribeVoiceAssistantRequest msg; SubscribeVoiceAssistantRequest msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -416,7 +415,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
#endif #endif
#ifdef USE_VOICE_ASSISTANT #ifdef USE_VOICE_ASSISTANT
case 91: { case VoiceAssistantResponse::MESSAGE_TYPE: {
VoiceAssistantResponse msg; VoiceAssistantResponse msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -427,7 +426,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
#endif #endif
#ifdef USE_VOICE_ASSISTANT #ifdef USE_VOICE_ASSISTANT
case 92: { case VoiceAssistantEventResponse::MESSAGE_TYPE: {
VoiceAssistantEventResponse msg; VoiceAssistantEventResponse msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -438,7 +437,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
#endif #endif
#ifdef USE_ALARM_CONTROL_PANEL #ifdef USE_ALARM_CONTROL_PANEL
case 96: { case AlarmControlPanelCommandRequest::MESSAGE_TYPE: {
AlarmControlPanelCommandRequest msg; AlarmControlPanelCommandRequest msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -449,7 +448,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
#endif #endif
#ifdef USE_TEXT #ifdef USE_TEXT
case 99: { case TextCommandRequest::MESSAGE_TYPE: {
TextCommandRequest msg; TextCommandRequest msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -460,7 +459,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
#endif #endif
#ifdef USE_DATETIME_DATE #ifdef USE_DATETIME_DATE
case 102: { case DateCommandRequest::MESSAGE_TYPE: {
DateCommandRequest msg; DateCommandRequest msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -471,7 +470,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
#endif #endif
#ifdef USE_DATETIME_TIME #ifdef USE_DATETIME_TIME
case 105: { case TimeCommandRequest::MESSAGE_TYPE: {
TimeCommandRequest msg; TimeCommandRequest msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -482,7 +481,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
#endif #endif
#ifdef USE_VOICE_ASSISTANT #ifdef USE_VOICE_ASSISTANT
case 106: { case VoiceAssistantAudio::MESSAGE_TYPE: {
VoiceAssistantAudio msg; VoiceAssistantAudio msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -493,7 +492,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
#endif #endif
#ifdef USE_VALVE #ifdef USE_VALVE
case 111: { case ValveCommandRequest::MESSAGE_TYPE: {
ValveCommandRequest msg; ValveCommandRequest msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -504,7 +503,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
#endif #endif
#ifdef USE_DATETIME_DATETIME #ifdef USE_DATETIME_DATETIME
case 114: { case DateTimeCommandRequest::MESSAGE_TYPE: {
DateTimeCommandRequest msg; DateTimeCommandRequest msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -515,7 +514,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
#endif #endif
#ifdef USE_VOICE_ASSISTANT #ifdef USE_VOICE_ASSISTANT
case 115: { case VoiceAssistantTimerEventResponse::MESSAGE_TYPE: {
VoiceAssistantTimerEventResponse msg; VoiceAssistantTimerEventResponse msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -526,7 +525,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
#endif #endif
#ifdef USE_UPDATE #ifdef USE_UPDATE
case 118: { case UpdateCommandRequest::MESSAGE_TYPE: {
UpdateCommandRequest msg; UpdateCommandRequest msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -537,7 +536,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
#endif #endif
#ifdef USE_VOICE_ASSISTANT #ifdef USE_VOICE_ASSISTANT
case 119: { case VoiceAssistantAnnounceRequest::MESSAGE_TYPE: {
VoiceAssistantAnnounceRequest msg; VoiceAssistantAnnounceRequest msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -548,7 +547,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
#endif #endif
#ifdef USE_VOICE_ASSISTANT #ifdef USE_VOICE_ASSISTANT
case 121: { case VoiceAssistantConfigurationRequest::MESSAGE_TYPE: {
VoiceAssistantConfigurationRequest msg; VoiceAssistantConfigurationRequest msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -559,7 +558,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
#endif #endif
#ifdef USE_VOICE_ASSISTANT #ifdef USE_VOICE_ASSISTANT
case 123: { case VoiceAssistantSetConfiguration::MESSAGE_TYPE: {
VoiceAssistantSetConfiguration msg; VoiceAssistantSetConfiguration msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -570,7 +569,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
#endif #endif
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
case 124: { case NoiseEncryptionSetKeyRequest::MESSAGE_TYPE: {
NoiseEncryptionSetKeyRequest msg; NoiseEncryptionSetKeyRequest msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -581,7 +580,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
#endif #endif
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
case 127: { case BluetoothScannerSetModeRequest::MESSAGE_TYPE: {
BluetoothScannerSetModeRequest msg; BluetoothScannerSetModeRequest msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -597,35 +596,28 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
void APIServerConnection::on_hello_request(const HelloRequest &msg) { void APIServerConnection::on_hello_request(const HelloRequest &msg) {
HelloResponse ret = this->hello(msg); if (!this->send_hello_response(msg)) {
if (!this->send_message(ret, HelloResponse::MESSAGE_TYPE)) {
this->on_fatal_error(); this->on_fatal_error();
} }
} }
void APIServerConnection::on_connect_request(const ConnectRequest &msg) { void APIServerConnection::on_connect_request(const ConnectRequest &msg) {
ConnectResponse ret = this->connect(msg); if (!this->send_connect_response(msg)) {
if (!this->send_message(ret, ConnectResponse::MESSAGE_TYPE)) {
this->on_fatal_error(); this->on_fatal_error();
} }
} }
void APIServerConnection::on_disconnect_request(const DisconnectRequest &msg) { void APIServerConnection::on_disconnect_request(const DisconnectRequest &msg) {
DisconnectResponse ret = this->disconnect(msg); if (!this->send_disconnect_response(msg)) {
if (!this->send_message(ret, DisconnectResponse::MESSAGE_TYPE)) {
this->on_fatal_error(); this->on_fatal_error();
} }
} }
void APIServerConnection::on_ping_request(const PingRequest &msg) { void APIServerConnection::on_ping_request(const PingRequest &msg) {
PingResponse ret = this->ping(msg); if (!this->send_ping_response(msg)) {
if (!this->send_message(ret, PingResponse::MESSAGE_TYPE)) {
this->on_fatal_error(); this->on_fatal_error();
} }
} }
void APIServerConnection::on_device_info_request(const DeviceInfoRequest &msg) { void APIServerConnection::on_device_info_request(const DeviceInfoRequest &msg) {
if (this->check_connection_setup_()) { if (this->check_connection_setup_() && !this->send_device_info_response(msg)) {
DeviceInfoResponse ret = this->device_info(msg); this->on_fatal_error();
if (!this->send_message(ret, DeviceInfoResponse::MESSAGE_TYPE)) {
this->on_fatal_error();
}
} }
} }
void APIServerConnection::on_list_entities_request(const ListEntitiesRequest &msg) { void APIServerConnection::on_list_entities_request(const ListEntitiesRequest &msg) {
@ -655,11 +647,8 @@ void APIServerConnection::on_subscribe_home_assistant_states_request(const Subsc
} }
} }
void APIServerConnection::on_get_time_request(const GetTimeRequest &msg) { void APIServerConnection::on_get_time_request(const GetTimeRequest &msg) {
if (this->check_connection_setup_()) { if (this->check_connection_setup_() && !this->send_get_time_response(msg)) {
GetTimeResponse ret = this->get_time(msg); this->on_fatal_error();
if (!this->send_message(ret, GetTimeResponse::MESSAGE_TYPE)) {
this->on_fatal_error();
}
} }
} }
#ifdef USE_API_SERVICES #ifdef USE_API_SERVICES
@ -671,11 +660,8 @@ void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest
#endif #endif
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
void APIServerConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) { void APIServerConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) {
if (this->check_authenticated_()) { if (this->check_authenticated_() && !this->send_noise_encryption_set_key_response(msg)) {
NoiseEncryptionSetKeyResponse ret = this->noise_encryption_set_key(msg); this->on_fatal_error();
if (!this->send_message(ret, NoiseEncryptionSetKeyResponse::MESSAGE_TYPE)) {
this->on_fatal_error();
}
} }
} }
#endif #endif
@ -865,11 +851,8 @@ void APIServerConnection::on_bluetooth_gatt_notify_request(const BluetoothGATTNo
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
void APIServerConnection::on_subscribe_bluetooth_connections_free_request( void APIServerConnection::on_subscribe_bluetooth_connections_free_request(
const SubscribeBluetoothConnectionsFreeRequest &msg) { const SubscribeBluetoothConnectionsFreeRequest &msg) {
if (this->check_authenticated_()) { if (this->check_authenticated_() && !this->send_subscribe_bluetooth_connections_free_response(msg)) {
BluetoothConnectionsFreeResponse ret = this->subscribe_bluetooth_connections_free(msg); this->on_fatal_error();
if (!this->send_message(ret, BluetoothConnectionsFreeResponse::MESSAGE_TYPE)) {
this->on_fatal_error();
}
} }
} }
#endif #endif
@ -897,11 +880,8 @@ void APIServerConnection::on_subscribe_voice_assistant_request(const SubscribeVo
#endif #endif
#ifdef USE_VOICE_ASSISTANT #ifdef USE_VOICE_ASSISTANT
void APIServerConnection::on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) { void APIServerConnection::on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) {
if (this->check_authenticated_()) { if (this->check_authenticated_() && !this->send_voice_assistant_get_configuration_response(msg)) {
VoiceAssistantConfigurationResponse ret = this->voice_assistant_get_configuration(msg); this->on_fatal_error();
if (!this->send_message(ret, VoiceAssistantConfigurationResponse::MESSAGE_TYPE)) {
this->on_fatal_error();
}
} }
} }
#endif #endif
@ -920,5 +900,4 @@ void APIServerConnection::on_alarm_control_panel_command_request(const AlarmCont
} }
#endif #endif
} // namespace api } // namespace esphome::api
} // namespace esphome

View File

@ -6,8 +6,7 @@
#include "api_pb2.h" #include "api_pb2.h"
namespace esphome { namespace esphome::api {
namespace api {
class APIServerConnectionBase : public ProtoService { class APIServerConnectionBase : public ProtoService {
public: public:
@ -207,22 +206,22 @@ class APIServerConnectionBase : public ProtoService {
class APIServerConnection : public APIServerConnectionBase { class APIServerConnection : public APIServerConnectionBase {
public: public:
virtual HelloResponse hello(const HelloRequest &msg) = 0; virtual bool send_hello_response(const HelloRequest &msg) = 0;
virtual ConnectResponse connect(const ConnectRequest &msg) = 0; virtual bool send_connect_response(const ConnectRequest &msg) = 0;
virtual DisconnectResponse disconnect(const DisconnectRequest &msg) = 0; virtual bool send_disconnect_response(const DisconnectRequest &msg) = 0;
virtual PingResponse ping(const PingRequest &msg) = 0; virtual bool send_ping_response(const PingRequest &msg) = 0;
virtual DeviceInfoResponse device_info(const DeviceInfoRequest &msg) = 0; virtual bool send_device_info_response(const DeviceInfoRequest &msg) = 0;
virtual void list_entities(const ListEntitiesRequest &msg) = 0; virtual void list_entities(const ListEntitiesRequest &msg) = 0;
virtual void subscribe_states(const SubscribeStatesRequest &msg) = 0; virtual void subscribe_states(const SubscribeStatesRequest &msg) = 0;
virtual void subscribe_logs(const SubscribeLogsRequest &msg) = 0; virtual void subscribe_logs(const SubscribeLogsRequest &msg) = 0;
virtual void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) = 0; virtual void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) = 0;
virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0; virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0;
virtual GetTimeResponse get_time(const GetTimeRequest &msg) = 0; virtual bool send_get_time_response(const GetTimeRequest &msg) = 0;
#ifdef USE_API_SERVICES #ifdef USE_API_SERVICES
virtual void execute_service(const ExecuteServiceRequest &msg) = 0; virtual void execute_service(const ExecuteServiceRequest &msg) = 0;
#endif #endif
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
virtual NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) = 0; virtual bool send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) = 0;
#endif #endif
#ifdef USE_BUTTON #ifdef USE_BUTTON
virtual void button_command(const ButtonCommandRequest &msg) = 0; virtual void button_command(const ButtonCommandRequest &msg) = 0;
@ -303,7 +302,7 @@ class APIServerConnection : public APIServerConnectionBase {
virtual void bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) = 0; virtual void bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) = 0;
#endif #endif
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
virtual BluetoothConnectionsFreeResponse subscribe_bluetooth_connections_free( virtual bool send_subscribe_bluetooth_connections_free_response(
const SubscribeBluetoothConnectionsFreeRequest &msg) = 0; const SubscribeBluetoothConnectionsFreeRequest &msg) = 0;
#endif #endif
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
@ -316,8 +315,7 @@ class APIServerConnection : public APIServerConnectionBase {
virtual void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) = 0; virtual void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) = 0;
#endif #endif
#ifdef USE_VOICE_ASSISTANT #ifdef USE_VOICE_ASSISTANT
virtual VoiceAssistantConfigurationResponse voice_assistant_get_configuration( virtual bool send_voice_assistant_get_configuration_response(const VoiceAssistantConfigurationRequest &msg) = 0;
const VoiceAssistantConfigurationRequest &msg) = 0;
#endif #endif
#ifdef USE_VOICE_ASSISTANT #ifdef USE_VOICE_ASSISTANT
virtual void voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) = 0; virtual void voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) = 0;
@ -445,5 +443,4 @@ class APIServerConnection : public APIServerConnectionBase {
#endif #endif
}; };
} // namespace api } // namespace esphome::api
} // namespace esphome

View File

@ -16,8 +16,7 @@
#include <algorithm> #include <algorithm>
namespace esphome { namespace esphome::api {
namespace api {
static const char *const TAG = "api"; static const char *const TAG = "api";
@ -184,9 +183,9 @@ void APIServer::loop() {
// Rare case: handle disconnection // Rare case: handle disconnection
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER #ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
this->client_disconnected_trigger_->trigger(client->client_info_, client->client_peername_); this->client_disconnected_trigger_->trigger(client->client_info_.name, client->client_info_.peername);
#endif #endif
ESP_LOGV(TAG, "Remove connection %s", client->client_info_.c_str()); ESP_LOGV(TAG, "Remove connection %s", client->client_info_.name.c_str());
// Swap with the last element and pop (avoids expensive vector shifts) // Swap with the last element and pop (avoids expensive vector shifts)
if (client_index < this->clients_.size() - 1) { if (client_index < this->clients_.size() - 1) {
@ -483,6 +482,5 @@ bool APIServer::teardown() {
return this->clients_.empty(); return this->clients_.empty();
} }
} // namespace api } // namespace esphome::api
} // namespace esphome
#endif #endif

View File

@ -18,8 +18,7 @@
#include <vector> #include <vector>
namespace esphome { namespace esphome::api {
namespace api {
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
struct SavedNoisePsk { struct SavedNoisePsk {
@ -196,6 +195,5 @@ template<typename... Ts> class APIConnectedCondition : public Condition<Ts...> {
bool check(Ts... x) override { return global_api_server->is_connected(); } bool check(Ts... x) override { return global_api_server->is_connected(); }
}; };
} // namespace api } // namespace esphome::api
} // namespace esphome
#endif #endif

View File

@ -14,6 +14,8 @@ with warnings.catch_warnings():
from aioesphomeapi import APIClient, parse_log_message from aioesphomeapi import APIClient, parse_log_message
from aioesphomeapi.log_runner import async_run from aioesphomeapi.log_runner import async_run
import contextlib
from esphome.const import CONF_KEY, CONF_PASSWORD, CONF_PORT, __version__ from esphome.const import CONF_KEY, CONF_PASSWORD, CONF_PORT, __version__
from esphome.core import CORE from esphome.core import CORE
@ -66,7 +68,5 @@ async def async_run_logs(config: dict[str, Any], address: str) -> None:
def run_logs(config: dict[str, Any], address: str) -> None: def run_logs(config: dict[str, Any], address: str) -> None:
"""Run the logs command.""" """Run the logs command."""
try: with contextlib.suppress(KeyboardInterrupt):
asyncio.run(async_run_logs(config, address)) asyncio.run(async_run_logs(config, address))
except KeyboardInterrupt:
pass

View File

@ -6,8 +6,7 @@
#ifdef USE_API_SERVICES #ifdef USE_API_SERVICES
#include "user_services.h" #include "user_services.h"
#endif #endif
namespace esphome { namespace esphome::api {
namespace api {
#ifdef USE_API_SERVICES #ifdef USE_API_SERVICES
template<typename T, typename... Ts> class CustomAPIDeviceService : public UserServiceBase<Ts...> { template<typename T, typename... Ts> class CustomAPIDeviceService : public UserServiceBase<Ts...> {
@ -148,7 +147,7 @@ class CustomAPIDevice {
*/ */
void call_homeassistant_service(const std::string &service_name) { void call_homeassistant_service(const std::string &service_name) {
HomeassistantServiceResponse resp; HomeassistantServiceResponse resp;
resp.service = service_name; resp.set_service(StringRef(service_name));
global_api_server->send_homeassistant_service_call(resp); global_api_server->send_homeassistant_service_call(resp);
} }
@ -168,12 +167,12 @@ class CustomAPIDevice {
*/ */
void call_homeassistant_service(const std::string &service_name, const std::map<std::string, std::string> &data) { void call_homeassistant_service(const std::string &service_name, const std::map<std::string, std::string> &data) {
HomeassistantServiceResponse resp; HomeassistantServiceResponse resp;
resp.service = service_name; resp.set_service(StringRef(service_name));
for (auto &it : data) { for (auto &it : data) {
HomeassistantServiceMap kv; resp.data.emplace_back();
kv.key = it.first; auto &kv = resp.data.back();
kv.value = it.second; kv.set_key(StringRef(it.first));
resp.data.push_back(kv); kv.set_value(StringRef(it.second));
} }
global_api_server->send_homeassistant_service_call(resp); global_api_server->send_homeassistant_service_call(resp);
} }
@ -190,7 +189,7 @@ class CustomAPIDevice {
*/ */
void fire_homeassistant_event(const std::string &event_name) { void fire_homeassistant_event(const std::string &event_name) {
HomeassistantServiceResponse resp; HomeassistantServiceResponse resp;
resp.service = event_name; resp.set_service(StringRef(event_name));
resp.is_event = true; resp.is_event = true;
global_api_server->send_homeassistant_service_call(resp); global_api_server->send_homeassistant_service_call(resp);
} }
@ -210,18 +209,17 @@ class CustomAPIDevice {
*/ */
void fire_homeassistant_event(const std::string &service_name, const std::map<std::string, std::string> &data) { void fire_homeassistant_event(const std::string &service_name, const std::map<std::string, std::string> &data) {
HomeassistantServiceResponse resp; HomeassistantServiceResponse resp;
resp.service = service_name; resp.set_service(StringRef(service_name));
resp.is_event = true; resp.is_event = true;
for (auto &it : data) { for (auto &it : data) {
HomeassistantServiceMap kv; resp.data.emplace_back();
kv.key = it.first; auto &kv = resp.data.back();
kv.value = it.second; kv.set_key(StringRef(it.first));
resp.data.push_back(kv); kv.set_value(StringRef(it.second));
} }
global_api_server->send_homeassistant_service_call(resp); global_api_server->send_homeassistant_service_call(resp);
} }
}; };
} // namespace api } // namespace esphome::api
} // namespace esphome
#endif #endif

View File

@ -7,8 +7,7 @@
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include <vector> #include <vector>
namespace esphome { namespace esphome::api {
namespace api {
template<typename... X> class TemplatableStringValue : public TemplatableValue<std::string, X...> { template<typename... X> class TemplatableStringValue : public TemplatableValue<std::string, X...> {
private: private:
@ -36,6 +35,9 @@ template<typename... X> class TemplatableStringValue : public TemplatableValue<s
template<typename... Ts> class TemplatableKeyValuePair { template<typename... Ts> class TemplatableKeyValuePair {
public: public:
// Keys are always string literals from YAML dictionary keys (e.g., "code", "event")
// and never templatable values or lambdas. Only the value parameter can be a lambda/template.
// Using pass-by-value with std::move allows optimal performance for both lvalues and rvalues.
template<typename T> TemplatableKeyValuePair(std::string key, T value) : key(std::move(key)), value(value) {} template<typename T> TemplatableKeyValuePair(std::string key, T value) : key(std::move(key)), value(value) {}
std::string key; std::string key;
TemplatableStringValue<Ts...> value; TemplatableStringValue<Ts...> value;
@ -47,37 +49,42 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
template<typename T> void set_service(T service) { this->service_ = service; } template<typename T> void set_service(T service) { this->service_ = service; }
template<typename T> void add_data(std::string key, T value) { // Keys are always string literals from the Python code generation (e.g., cg.add(var.add_data("tag_id", templ))).
this->data_.push_back(TemplatableKeyValuePair<Ts...>(key, value)); // The value parameter can be a lambda/template, but keys are never templatable.
} // Using pass-by-value allows the compiler to optimize for both lvalues and rvalues.
template<typename T> void add_data(std::string key, T value) { this->data_.emplace_back(std::move(key), value); }
template<typename T> void add_data_template(std::string key, T value) { template<typename T> void add_data_template(std::string key, T value) {
this->data_template_.push_back(TemplatableKeyValuePair<Ts...>(key, value)); this->data_template_.emplace_back(std::move(key), value);
} }
template<typename T> void add_variable(std::string key, T value) { template<typename T> void add_variable(std::string key, T value) {
this->variables_.push_back(TemplatableKeyValuePair<Ts...>(key, value)); this->variables_.emplace_back(std::move(key), value);
} }
void play(Ts... x) override { void play(Ts... x) override {
HomeassistantServiceResponse resp; HomeassistantServiceResponse resp;
resp.service = this->service_.value(x...); std::string service_value = this->service_.value(x...);
resp.set_service(StringRef(service_value));
resp.is_event = this->is_event_; resp.is_event = this->is_event_;
for (auto &it : this->data_) { for (auto &it : this->data_) {
HomeassistantServiceMap kv; resp.data.emplace_back();
kv.key = it.key; auto &kv = resp.data.back();
kv.value = it.value.value(x...); kv.set_key(StringRef(it.key));
resp.data.push_back(kv); std::string value = it.value.value(x...);
kv.set_value(StringRef(value));
} }
for (auto &it : this->data_template_) { for (auto &it : this->data_template_) {
HomeassistantServiceMap kv; resp.data_template.emplace_back();
kv.key = it.key; auto &kv = resp.data_template.back();
kv.value = it.value.value(x...); kv.set_key(StringRef(it.key));
resp.data_template.push_back(kv); std::string value = it.value.value(x...);
kv.set_value(StringRef(value));
} }
for (auto &it : this->variables_) { for (auto &it : this->variables_) {
HomeassistantServiceMap kv; resp.variables.emplace_back();
kv.key = it.key; auto &kv = resp.variables.back();
kv.value = it.value.value(x...); kv.set_key(StringRef(it.key));
resp.variables.push_back(kv); std::string value = it.value.value(x...);
kv.set_value(StringRef(value));
} }
this->parent_->send_homeassistant_service_call(resp); this->parent_->send_homeassistant_service_call(resp);
} }
@ -91,6 +98,5 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
std::vector<TemplatableKeyValuePair<Ts...>> variables_; std::vector<TemplatableKeyValuePair<Ts...>> variables_;
}; };
} // namespace api } // namespace esphome::api
} // namespace esphome
#endif #endif

View File

@ -6,8 +6,7 @@
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/util.h" #include "esphome/core/util.h"
namespace esphome { namespace esphome::api {
namespace api {
// Generate entity handler implementations using macros // Generate entity handler implementations using macros
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
@ -90,6 +89,5 @@ bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) {
} }
#endif #endif
} // namespace api } // namespace esphome::api
} // namespace esphome
#endif #endif

View File

@ -4,8 +4,7 @@
#ifdef USE_API #ifdef USE_API
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/component_iterator.h" #include "esphome/core/component_iterator.h"
namespace esphome { namespace esphome::api {
namespace api {
class APIConnection; class APIConnection;
@ -96,6 +95,5 @@ class ListEntitiesIterator : public ComponentIterator {
APIConnection *client_; APIConnection *client_;
}; };
} // namespace api } // namespace esphome::api
} // namespace esphome
#endif #endif

View File

@ -3,8 +3,7 @@
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
namespace esphome { namespace esphome::api {
namespace api {
static const char *const TAG = "api.proto"; static const char *const TAG = "api.proto";
@ -89,5 +88,4 @@ std::string ProtoMessage::dump() const {
} }
#endif #endif
} // namespace api } // namespace esphome::api
} // namespace esphome

View File

@ -3,16 +3,48 @@
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/string_ref.h"
#include <cassert> #include <cassert>
#include <cstring>
#include <vector> #include <vector>
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE #ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
#define HAS_PROTO_MESSAGE_DUMP #define HAS_PROTO_MESSAGE_DUMP
#endif #endif
namespace esphome { namespace esphome::api {
namespace api {
/*
* StringRef Ownership Model for API Protocol Messages
* ===================================================
*
* StringRef is used for zero-copy string handling in outgoing (SOURCE_SERVER) messages.
* It holds a pointer and length to existing string data without copying.
*
* CRITICAL: The referenced string data MUST remain valid until message encoding completes.
*
* Safe StringRef Patterns:
* 1. String literals: StringRef("literal") - Always safe (static storage duration)
* 2. Member variables: StringRef(this->member_string_) - Safe if object outlives encoding
* 3. Global/static strings: StringRef(GLOBAL_CONSTANT) - Always safe
* 4. Local variables: Safe ONLY if encoding happens before function returns:
* std::string temp = compute_value();
* msg.set_field(StringRef(temp));
* return this->send_message(msg); // temp is valid during encoding
*
* Unsafe Patterns (WILL cause crashes/corruption):
* 1. Temporaries: msg.set_field(StringRef(obj.get_string())) // get_string() returns by value
* 2. Optional values: msg.set_field(StringRef(optional.value())) // value() returns a copy
* 3. Concatenation: msg.set_field(StringRef(str1 + str2)) // Result is temporary
*
* For unsafe patterns, store in a local variable first:
* std::string temp = optional.value(); // or get_string() or str1 + str2
* msg.set_field(StringRef(temp));
*
* The send_*_response pattern ensures proper lifetime management by encoding
* within the same function scope where temporaries are created.
*/
/// Representation of a VarInt - in ProtoBuf should be 64bit but we only use 32bit /// Representation of a VarInt - in ProtoBuf should be 64bit but we only use 32bit
class ProtoVarInt { class ProtoVarInt {
@ -206,12 +238,20 @@ class ProtoWriteBuffer {
this->encode_field_raw(field_id, 2); // type 2: Length-delimited string this->encode_field_raw(field_id, 2); // type 2: Length-delimited string
this->encode_varint_raw(len); this->encode_varint_raw(len);
auto *data = reinterpret_cast<const uint8_t *>(string);
this->buffer_->insert(this->buffer_->end(), data, data + len); // Using resize + memcpy instead of insert provides significant performance improvement:
// ~10-11x faster for 16-32 byte strings, ~3x faster for 64-byte strings
// as it avoids iterator checks and potential element moves that insert performs
size_t old_size = this->buffer_->size();
this->buffer_->resize(old_size + len);
std::memcpy(this->buffer_->data() + old_size, string, len);
} }
void encode_string(uint32_t field_id, const std::string &value, bool force = false) { void encode_string(uint32_t field_id, const std::string &value, bool force = false) {
this->encode_string(field_id, value.data(), value.size(), force); this->encode_string(field_id, value.data(), value.size(), force);
} }
void encode_string(uint32_t field_id, const StringRef &ref, bool force = false) {
this->encode_string(field_id, ref.c_str(), ref.size(), force);
}
void encode_bytes(uint32_t field_id, const uint8_t *data, size_t len, bool force = false) { void encode_bytes(uint32_t field_id, const uint8_t *data, size_t len, bool force = false) {
this->encode_string(field_id, reinterpret_cast<const char *>(data), len, force); this->encode_string(field_id, reinterpret_cast<const char *>(data), len, force);
} }
@ -527,25 +567,6 @@ class ProtoSize {
total_size += field_id_size + 1; total_size += field_id_size + 1;
} }
/**
* @brief Calculates and adds the size of a fixed field to the total message size
*
* Fixed fields always take exactly N bytes (4 for fixed32/float, 8 for fixed64/double).
*
* @tparam NumBytes The number of bytes for this fixed field (4 or 8)
* @param is_nonzero Whether the value is non-zero
*/
template<uint32_t NumBytes>
static inline void add_fixed_field(uint32_t &total_size, uint32_t field_id_size, bool is_nonzero) {
// Skip calculation if value is zero
if (!is_nonzero) {
return; // No need to update total_size
}
// Fixed fields always take exactly NumBytes
total_size += field_id_size + NumBytes;
}
/** /**
* @brief Calculates and adds the size of a float field to the total message size * @brief Calculates and adds the size of a float field to the total message size
*/ */
@ -682,17 +703,16 @@ class ProtoSize {
// sint64 type is not supported by ESPHome API to reduce overhead on embedded systems // sint64 type is not supported by ESPHome API to reduce overhead on embedded systems
/** /**
* @brief Calculates and adds the size of a string/bytes field to the total message size * @brief Calculates and adds the size of a string field using length
*/ */
static inline void add_string_field(uint32_t &total_size, uint32_t field_id_size, const std::string &str) { static inline void add_string_field(uint32_t &total_size, uint32_t field_id_size, size_t len) {
// Skip calculation if string is empty // Skip calculation if string is empty
if (str.empty()) { if (len == 0) {
return; // No need to update total_size return; // No need to update total_size
} }
// Calculate and directly add to total_size // Field ID + length varint + string bytes
const uint32_t str_size = static_cast<uint32_t>(str.size()); total_size += field_id_size + varint(static_cast<uint32_t>(len)) + static_cast<uint32_t>(len);
total_size += field_id_size + varint(str_size) + str_size;
} }
/** /**
@ -704,6 +724,19 @@ class ProtoSize {
total_size += field_id_size + varint(str_size) + str_size; total_size += field_id_size + varint(str_size) + str_size;
} }
/**
* @brief Calculates and adds the size of a bytes field to the total message size
*/
static inline void add_bytes_field(uint32_t &total_size, uint32_t field_id_size, size_t len) {
// Skip calculation if bytes is empty
if (len == 0) {
return; // No need to update total_size
}
// Field ID + length varint + data bytes
total_size += field_id_size + varint(static_cast<uint32_t>(len)) + static_cast<uint32_t>(len);
}
/** /**
* @brief Calculates and adds the size of a nested message field to the total message size * @brief Calculates and adds the size of a nested message field to the total message size
* *
@ -876,5 +909,4 @@ class ProtoService {
} }
}; };
} // namespace api } // namespace esphome::api
} // namespace esphome

View File

@ -3,8 +3,7 @@
#include "api_connection.h" #include "api_connection.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
namespace esphome { namespace esphome::api {
namespace api {
// Generate entity handler implementations using macros // Generate entity handler implementations using macros
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
@ -69,6 +68,5 @@ INITIAL_STATE_HANDLER(update, update::UpdateEntity)
InitialStateIterator::InitialStateIterator(APIConnection *client) : client_(client) {} InitialStateIterator::InitialStateIterator(APIConnection *client) : client_(client) {}
} // namespace api } // namespace esphome::api
} // namespace esphome
#endif #endif

View File

@ -5,8 +5,7 @@
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/component_iterator.h" #include "esphome/core/component_iterator.h"
#include "esphome/core/controller.h" #include "esphome/core/controller.h"
namespace esphome { namespace esphome::api {
namespace api {
class APIConnection; class APIConnection;
@ -89,6 +88,5 @@ class InitialStateIterator : public ComponentIterator {
APIConnection *client_; APIConnection *client_;
}; };
} // namespace api } // namespace esphome::api
} // namespace esphome
#endif #endif

View File

@ -1,8 +1,7 @@
#include "user_services.h" #include "user_services.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
namespace esphome { namespace esphome::api {
namespace api {
template<> bool get_execute_arg_value<bool>(const ExecuteServiceArgument &arg) { return arg.bool_; } template<> bool get_execute_arg_value<bool>(const ExecuteServiceArgument &arg) { return arg.bool_; }
template<> int32_t get_execute_arg_value<int32_t>(const ExecuteServiceArgument &arg) { template<> int32_t get_execute_arg_value<int32_t>(const ExecuteServiceArgument &arg) {
@ -40,5 +39,4 @@ template<> enums::ServiceArgType to_service_arg_type<std::vector<std::string>>()
return enums::SERVICE_ARG_TYPE_STRING_ARRAY; return enums::SERVICE_ARG_TYPE_STRING_ARRAY;
} }
} // namespace api } // namespace esphome::api
} // namespace esphome

View File

@ -8,8 +8,7 @@
#include "api_pb2.h" #include "api_pb2.h"
#ifdef USE_API_SERVICES #ifdef USE_API_SERVICES
namespace esphome { namespace esphome::api {
namespace api {
class UserServiceDescriptor { class UserServiceDescriptor {
public: public:
@ -33,14 +32,14 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
ListEntitiesServicesResponse encode_list_service_response() override { ListEntitiesServicesResponse encode_list_service_response() override {
ListEntitiesServicesResponse msg; ListEntitiesServicesResponse msg;
msg.name = this->name_; msg.set_name(StringRef(this->name_));
msg.key = this->key_; msg.key = this->key_;
std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...}; std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
for (int i = 0; i < sizeof...(Ts); i++) { for (int i = 0; i < sizeof...(Ts); i++) {
ListEntitiesServicesArgument arg; msg.args.emplace_back();
auto &arg = msg.args.back();
arg.type = arg_types[i]; arg.type = arg_types[i];
arg.name = this->arg_names_[i]; arg.set_name(StringRef(this->arg_names_[i]));
msg.args.push_back(arg);
} }
return msg; return msg;
} }
@ -74,6 +73,5 @@ template<typename... Ts> class UserServiceTrigger : public UserServiceBase<Ts...
void execute(Ts... x) override { this->trigger(x...); } // NOLINT void execute(Ts... x) override { this->trigger(x...); } // NOLINT
}; };
} // namespace api } // namespace esphome::api
} // namespace esphome
#endif // USE_API_SERVICES #endif // USE_API_SERVICES

View File

@ -15,7 +15,7 @@ class AudioStreamInfo {
* - An audio sample represents a unit of audio for one channel. * - An audio sample represents a unit of audio for one channel.
* - A frame represents a unit of audio with a sample for every channel. * - A frame represents a unit of audio with a sample for every channel.
* *
* In gneneral, converting between bytes, samples, and frames shouldn't result in rounding errors so long as frames * In general, converting between bytes, samples, and frames shouldn't result in rounding errors so long as frames
* are used as the main unit when transferring audio data. Durations may result in rounding for certain sample rates; * are used as the main unit when transferring audio data. Durations may result in rounding for certain sample rates;
* e.g., 44.1 KHz. The ``frames_to_milliseconds_with_remainder`` function should be used for accuracy, as it takes * e.g., 44.1 KHz. The ``frames_to_milliseconds_with_remainder`` function should be used for accuracy, as it takes
* into account the remainder rather than just ignoring any rounding. * into account the remainder rather than just ignoring any rounding.
@ -76,7 +76,7 @@ class AudioStreamInfo {
/// @brief Computes the duration, in microseconds, the given amount of frames represents. /// @brief Computes the duration, in microseconds, the given amount of frames represents.
/// @param frames Number of audio frames /// @param frames Number of audio frames
/// @return Duration in microseconds `frames` respresents. May be slightly inaccurate due to integer divison rounding /// @return Duration in microseconds `frames` represents. May be slightly inaccurate due to integer division rounding
/// for certain sample rates. /// for certain sample rates.
uint32_t frames_to_microseconds(uint32_t frames) const; uint32_t frames_to_microseconds(uint32_t frames) const;

View File

@ -266,8 +266,10 @@ async def delayed_off_filter_to_code(config, filter_id):
async def autorepeat_filter_to_code(config, filter_id): async def autorepeat_filter_to_code(config, filter_id):
timings = [] timings = []
if len(config) > 0: if len(config) > 0:
for conf in config: timings.extend(
timings.append((conf[CONF_DELAY], conf[CONF_TIME_OFF], conf[CONF_TIME_ON])) (conf[CONF_DELAY], conf[CONF_TIME_OFF], conf[CONF_TIME_ON])
for conf in config
)
else: else:
timings.append( timings.append(
( (
@ -573,16 +575,15 @@ async def setup_binary_sensor_core_(var, config):
await automation.build_automation(trigger, [], conf) await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_MULTI_CLICK, []): for conf in config.get(CONF_ON_MULTI_CLICK, []):
timings = [] timings = [
for tim in conf[CONF_TIMING]: cg.StructInitializer(
timings.append( MultiClickTriggerEvent,
cg.StructInitializer( ("state", tim[CONF_STATE]),
MultiClickTriggerEvent, ("min_length", tim[CONF_MIN_LENGTH]),
("state", tim[CONF_STATE]), ("max_length", tim.get(CONF_MAX_LENGTH, 4294967294)),
("min_length", tim[CONF_MIN_LENGTH]),
("max_length", tim.get(CONF_MAX_LENGTH, 4294967294)),
)
) )
for tim in conf[CONF_TIMING]
]
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var, timings) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var, timings)
if CONF_INVALID_COOLDOWN in conf: if CONF_INVALID_COOLDOWN in conf:
cg.add(trigger.set_invalid_cooldown(conf[CONF_INVALID_COOLDOWN])) cg.add(trigger.set_invalid_cooldown(conf[CONF_INVALID_COOLDOWN]))

View File

@ -8,16 +8,182 @@
#include "bluetooth_proxy.h" #include "bluetooth_proxy.h"
namespace esphome { namespace esphome::bluetooth_proxy {
namespace bluetooth_proxy {
static const char *const TAG = "bluetooth_proxy.connection"; static const char *const TAG = "bluetooth_proxy.connection";
static void fill_128bit_uuid_array(std::array<uint64_t, 2> &out, esp_bt_uuid_t uuid_source) {
esp_bt_uuid_t uuid = espbt::ESPBTUUID::from_uuid(uuid_source).as_128bit().get_uuid();
out[0] = ((uint64_t) uuid.uuid.uuid128[15] << 56) | ((uint64_t) uuid.uuid.uuid128[14] << 48) |
((uint64_t) uuid.uuid.uuid128[13] << 40) | ((uint64_t) uuid.uuid.uuid128[12] << 32) |
((uint64_t) uuid.uuid.uuid128[11] << 24) | ((uint64_t) uuid.uuid.uuid128[10] << 16) |
((uint64_t) uuid.uuid.uuid128[9] << 8) | ((uint64_t) uuid.uuid.uuid128[8]);
out[1] = ((uint64_t) uuid.uuid.uuid128[7] << 56) | ((uint64_t) uuid.uuid.uuid128[6] << 48) |
((uint64_t) uuid.uuid.uuid128[5] << 40) | ((uint64_t) uuid.uuid.uuid128[4] << 32) |
((uint64_t) uuid.uuid.uuid128[3] << 24) | ((uint64_t) uuid.uuid.uuid128[2] << 16) |
((uint64_t) uuid.uuid.uuid128[1] << 8) | ((uint64_t) uuid.uuid.uuid128[0]);
}
void BluetoothConnection::dump_config() { void BluetoothConnection::dump_config() {
ESP_LOGCONFIG(TAG, "BLE Connection:"); ESP_LOGCONFIG(TAG, "BLE Connection:");
BLEClientBase::dump_config(); BLEClientBase::dump_config();
} }
void BluetoothConnection::loop() {
BLEClientBase::loop();
// Early return if no active connection or not in service discovery phase
if (this->address_ == 0 || this->send_service_ < 0 || this->send_service_ > this->service_count_) {
return;
}
// Handle service discovery
this->send_service_for_discovery_();
}
void BluetoothConnection::reset_connection_(esp_err_t reason) {
// Send disconnection notification
this->proxy_->send_device_connection(this->address_, false, 0, reason);
// Important: If we were in the middle of sending services, we do NOT send
// send_gatt_services_done() here. This ensures the client knows that
// the service discovery was interrupted and can retry. The client
// (aioesphomeapi) implements a 30-second timeout (DEFAULT_BLE_TIMEOUT)
// to detect incomplete service discovery rather than relying on us to
// tell them about a partial list.
this->set_address(0);
this->send_service_ = DONE_SENDING_SERVICES;
this->proxy_->send_connections_free();
}
void BluetoothConnection::send_service_for_discovery_() {
if (this->send_service_ == this->service_count_) {
this->send_service_ = DONE_SENDING_SERVICES;
this->proxy_->send_gatt_services_done(this->address_);
if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
this->release_services();
}
return;
}
// Early return if no API connection
auto *api_conn = this->proxy_->get_api_connection();
if (api_conn == nullptr) {
return;
}
// Send next service
esp_gattc_service_elem_t service_result;
uint16_t service_count = 1;
esp_gatt_status_t service_status = esp_ble_gattc_get_service(this->gattc_if_, this->conn_id_, nullptr,
&service_result, &service_count, this->send_service_);
this->send_service_++;
if (service_status != ESP_GATT_OK) {
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_service error at offset=%d, status=%d", this->connection_index_,
this->address_str().c_str(), this->send_service_ - 1, service_status);
return;
}
if (service_count == 0) {
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_service missing, service_count=%d", this->connection_index_,
this->address_str().c_str(), service_count);
return;
}
api::BluetoothGATTGetServicesResponse resp;
resp.address = this->address_;
auto &service_resp = resp.services[0];
fill_128bit_uuid_array(service_resp.uuid, service_result.uuid);
service_resp.handle = service_result.start_handle;
// Get the number of characteristics directly with one call
uint16_t total_char_count = 0;
esp_gatt_status_t char_count_status =
esp_ble_gattc_get_attr_count(this->gattc_if_, this->conn_id_, ESP_GATT_DB_CHARACTERISTIC,
service_result.start_handle, service_result.end_handle, 0, &total_char_count);
if (char_count_status == ESP_GATT_OK && total_char_count > 0) {
// Only reserve if we successfully got a count
service_resp.characteristics.reserve(total_char_count);
} else if (char_count_status != ESP_GATT_OK) {
ESP_LOGW(TAG, "[%d] [%s] Error getting characteristic count, status=%d", this->connection_index_,
this->address_str().c_str(), char_count_status);
}
// Now process characteristics
uint16_t char_offset = 0;
esp_gattc_char_elem_t char_result;
while (true) { // characteristics
uint16_t char_count = 1;
esp_gatt_status_t char_status =
esp_ble_gattc_get_all_char(this->gattc_if_, this->conn_id_, service_result.start_handle,
service_result.end_handle, &char_result, &char_count, char_offset);
if (char_status == ESP_GATT_INVALID_OFFSET || char_status == ESP_GATT_NOT_FOUND) {
break;
}
if (char_status != ESP_GATT_OK) {
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", this->connection_index_,
this->address_str().c_str(), char_status);
break;
}
if (char_count == 0) {
break;
}
service_resp.characteristics.emplace_back();
auto &characteristic_resp = service_resp.characteristics.back();
fill_128bit_uuid_array(characteristic_resp.uuid, char_result.uuid);
characteristic_resp.handle = char_result.char_handle;
characteristic_resp.properties = char_result.properties;
char_offset++;
// Get the number of descriptors directly with one call
uint16_t total_desc_count = 0;
esp_gatt_status_t desc_count_status =
esp_ble_gattc_get_attr_count(this->gattc_if_, this->conn_id_, ESP_GATT_DB_DESCRIPTOR, char_result.char_handle,
service_result.end_handle, 0, &total_desc_count);
if (desc_count_status == ESP_GATT_OK && total_desc_count > 0) {
// Only reserve if we successfully got a count
characteristic_resp.descriptors.reserve(total_desc_count);
} else if (desc_count_status != ESP_GATT_OK) {
ESP_LOGW(TAG, "[%d] [%s] Error getting descriptor count for char handle %d, status=%d", this->connection_index_,
this->address_str().c_str(), char_result.char_handle, desc_count_status);
}
// Now process descriptors
uint16_t desc_offset = 0;
esp_gattc_descr_elem_t desc_result;
while (true) { // descriptors
uint16_t desc_count = 1;
esp_gatt_status_t desc_status = esp_ble_gattc_get_all_descr(
this->gattc_if_, this->conn_id_, char_result.char_handle, &desc_result, &desc_count, desc_offset);
if (desc_status == ESP_GATT_INVALID_OFFSET || desc_status == ESP_GATT_NOT_FOUND) {
break;
}
if (desc_status != ESP_GATT_OK) {
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_descr error, status=%d", this->connection_index_,
this->address_str().c_str(), desc_status);
break;
}
if (desc_count == 0) {
break;
}
characteristic_resp.descriptors.emplace_back();
auto &descriptor_resp = characteristic_resp.descriptors.back();
fill_128bit_uuid_array(descriptor_resp.uuid, desc_result.uuid);
descriptor_resp.handle = desc_result.handle;
desc_offset++;
}
}
// Send the message (we already checked api_conn is not null at the beginning)
api_conn->send_message(resp, api::BluetoothGATTGetServicesResponse::MESSAGE_TYPE);
}
bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) { esp_ble_gattc_cb_param_t *param) {
if (!BLEClientBase::gattc_event_handler(event, gattc_if, param)) if (!BLEClientBase::gattc_event_handler(event, gattc_if, param))
@ -25,22 +191,16 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
switch (event) { switch (event) {
case ESP_GATTC_DISCONNECT_EVT: { case ESP_GATTC_DISCONNECT_EVT: {
this->proxy_->send_device_connection(this->address_, false, 0, param->disconnect.reason); this->reset_connection_(param->disconnect.reason);
this->set_address(0);
this->proxy_->send_connections_free();
break; break;
} }
case ESP_GATTC_CLOSE_EVT: { case ESP_GATTC_CLOSE_EVT: {
this->proxy_->send_device_connection(this->address_, false, 0, param->close.reason); this->reset_connection_(param->close.reason);
this->set_address(0);
this->proxy_->send_connections_free();
break; break;
} }
case ESP_GATTC_OPEN_EVT: { case ESP_GATTC_OPEN_EVT: {
if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) { if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) {
this->proxy_->send_device_connection(this->address_, false, 0, param->open.status); this->reset_connection_(param->open.status);
this->set_address(0);
this->proxy_->send_connections_free();
} else if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) { } else if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
this->proxy_->send_device_connection(this->address_, true, this->mtu_); this->proxy_->send_device_connection(this->address_, true, this->mtu_);
this->proxy_->send_connections_free(); this->proxy_->send_connections_free();
@ -72,9 +232,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
api::BluetoothGATTReadResponse resp; api::BluetoothGATTReadResponse resp;
resp.address = this->address_; resp.address = this->address_;
resp.handle = param->read.handle; resp.handle = param->read.handle;
resp.data.reserve(param->read.value_len); resp.set_data(param->read.value, param->read.value_len);
// Use bulk insert instead of individual push_backs
resp.data.insert(resp.data.end(), param->read.value, param->read.value + param->read.value_len);
this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTReadResponse::MESSAGE_TYPE); this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTReadResponse::MESSAGE_TYPE);
break; break;
} }
@ -125,9 +283,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
api::BluetoothGATTNotifyDataResponse resp; api::BluetoothGATTNotifyDataResponse resp;
resp.address = this->address_; resp.address = this->address_;
resp.handle = param->notify.handle; resp.handle = param->notify.handle;
resp.data.reserve(param->notify.value_len); resp.set_data(param->notify.value, param->notify.value_len);
// Use bulk insert instead of individual push_backs
resp.data.insert(resp.data.end(), param->notify.value, param->notify.value + param->notify.value_len);
this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTNotifyDataResponse::MESSAGE_TYPE); this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTNotifyDataResponse::MESSAGE_TYPE);
break; break;
} }
@ -265,7 +421,6 @@ esp32_ble_tracker::AdvertisementParserType BluetoothConnection::get_advertisemen
return this->proxy_->get_advertisement_parser_type(); return this->proxy_->get_advertisement_parser_type();
} }
} // namespace bluetooth_proxy } // namespace esphome::bluetooth_proxy
} // namespace esphome
#endif // USE_ESP32 #endif // USE_ESP32

View File

@ -4,14 +4,14 @@
#include "esphome/components/esp32_ble_client/ble_client_base.h" #include "esphome/components/esp32_ble_client/ble_client_base.h"
namespace esphome { namespace esphome::bluetooth_proxy {
namespace bluetooth_proxy {
class BluetoothProxy; class BluetoothProxy;
class BluetoothConnection : public esp32_ble_client::BLEClientBase { class BluetoothConnection : public esp32_ble_client::BLEClientBase {
public: public:
void dump_config() override; void dump_config() override;
void loop() override;
bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override; esp_ble_gattc_cb_param_t *param) override;
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override;
@ -27,6 +27,9 @@ class BluetoothConnection : public esp32_ble_client::BLEClientBase {
protected: protected:
friend class BluetoothProxy; friend class BluetoothProxy;
void send_service_for_discovery_();
void reset_connection_(esp_err_t reason);
// Memory optimized layout for 32-bit systems // Memory optimized layout for 32-bit systems
// Group 1: Pointers (4 bytes each, naturally aligned) // Group 1: Pointers (4 bytes each, naturally aligned)
BluetoothProxy *proxy_; BluetoothProxy *proxy_;
@ -39,7 +42,6 @@ class BluetoothConnection : public esp32_ble_client::BLEClientBase {
// 1 byte used, 1 byte padding // 1 byte used, 1 byte padding
}; };
} // namespace bluetooth_proxy } // namespace esphome::bluetooth_proxy
} // namespace esphome
#endif // USE_ESP32 #endif // USE_ESP32

View File

@ -7,23 +7,9 @@
#ifdef USE_ESP32 #ifdef USE_ESP32
namespace esphome { namespace esphome::bluetooth_proxy {
namespace bluetooth_proxy {
static const char *const TAG = "bluetooth_proxy"; static const char *const TAG = "bluetooth_proxy";
static const int DONE_SENDING_SERVICES = -2;
std::vector<uint64_t> get_128bit_uuid_vec(esp_bt_uuid_t uuid_source) {
esp_bt_uuid_t uuid = espbt::ESPBTUUID::from_uuid(uuid_source).as_128bit().get_uuid();
return std::vector<uint64_t>{((uint64_t) uuid.uuid.uuid128[15] << 56) | ((uint64_t) uuid.uuid.uuid128[14] << 48) |
((uint64_t) uuid.uuid.uuid128[13] << 40) | ((uint64_t) uuid.uuid.uuid128[12] << 32) |
((uint64_t) uuid.uuid.uuid128[11] << 24) | ((uint64_t) uuid.uuid.uuid128[10] << 16) |
((uint64_t) uuid.uuid.uuid128[9] << 8) | ((uint64_t) uuid.uuid.uuid128[8]),
((uint64_t) uuid.uuid.uuid128[7] << 56) | ((uint64_t) uuid.uuid.uuid128[6] << 48) |
((uint64_t) uuid.uuid.uuid128[5] << 40) | ((uint64_t) uuid.uuid.uuid128[4] << 32) |
((uint64_t) uuid.uuid.uuid128[3] << 24) | ((uint64_t) uuid.uuid.uuid128[2] << 16) |
((uint64_t) uuid.uuid.uuid128[1] << 8) | ((uint64_t) uuid.uuid.uuid128[0])};
}
// Batch size for BLE advertisements to maximize WiFi efficiency // Batch size for BLE advertisements to maximize WiFi efficiency
// Each advertisement is up to 80 bytes when packaged (including protocol overhead) // Each advertisement is up to 80 bytes when packaged (including protocol overhead)
@ -140,46 +126,6 @@ void BluetoothProxy::flush_pending_advertisements() {
this->advertisement_count_ = 0; this->advertisement_count_ = 0;
} }
#ifdef USE_ESP32_BLE_DEVICE
void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device) {
api::BluetoothLEAdvertisementResponse resp;
resp.address = device.address_uint64();
resp.address_type = device.get_address_type();
if (!device.get_name().empty())
resp.name = device.get_name();
resp.rssi = device.get_rssi();
// Pre-allocate vectors based on known sizes
auto service_uuids = device.get_service_uuids();
resp.service_uuids.reserve(service_uuids.size());
for (auto &uuid : service_uuids) {
resp.service_uuids.emplace_back(uuid.to_string());
}
// Pre-allocate service data vector
auto service_datas = device.get_service_datas();
resp.service_data.reserve(service_datas.size());
for (auto &data : service_datas) {
resp.service_data.emplace_back();
auto &service_data = resp.service_data.back();
service_data.uuid = data.uuid.to_string();
service_data.data.assign(data.data.begin(), data.data.end());
}
// Pre-allocate manufacturer data vector
auto manufacturer_datas = device.get_manufacturer_datas();
resp.manufacturer_data.reserve(manufacturer_datas.size());
for (auto &data : manufacturer_datas) {
resp.manufacturer_data.emplace_back();
auto &manufacturer_data = resp.manufacturer_data.back();
manufacturer_data.uuid = data.uuid.to_string();
manufacturer_data.data.assign(data.data.begin(), data.data.end());
}
this->api_connection_->send_message(resp, api::BluetoothLEAdvertisementResponse::MESSAGE_TYPE);
}
#endif // USE_ESP32_BLE_DEVICE
void BluetoothProxy::dump_config() { void BluetoothProxy::dump_config() {
ESP_LOGCONFIG(TAG, "Bluetooth Proxy:"); ESP_LOGCONFIG(TAG, "Bluetooth Proxy:");
ESP_LOGCONFIG(TAG, ESP_LOGCONFIG(TAG,
@ -213,130 +159,12 @@ void BluetoothProxy::loop() {
} }
// Flush any pending BLE advertisements that have been accumulated but not yet sent // Flush any pending BLE advertisements that have been accumulated but not yet sent
static uint32_t last_flush_time = 0;
uint32_t now = App.get_loop_component_start_time(); uint32_t now = App.get_loop_component_start_time();
// Flush accumulated advertisements every 100ms // Flush accumulated advertisements every 100ms
if (now - last_flush_time >= 100) { if (now - this->last_advertisement_flush_time_ >= 100) {
this->flush_pending_advertisements(); this->flush_pending_advertisements();
last_flush_time = now; this->last_advertisement_flush_time_ = now;
}
for (auto *connection : this->connections_) {
if (connection->send_service_ == connection->service_count_) {
connection->send_service_ = DONE_SENDING_SERVICES;
this->send_gatt_services_done(connection->get_address());
if (connection->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
connection->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
connection->release_services();
}
} else if (connection->send_service_ >= 0) {
esp_gattc_service_elem_t service_result;
uint16_t service_count = 1;
esp_gatt_status_t service_status =
esp_ble_gattc_get_service(connection->get_gattc_if(), connection->get_conn_id(), nullptr, &service_result,
&service_count, connection->send_service_);
connection->send_service_++;
if (service_status != ESP_GATT_OK) {
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_service error at offset=%d, status=%d",
connection->get_connection_index(), connection->address_str().c_str(), connection->send_service_ - 1,
service_status);
continue;
}
if (service_count == 0) {
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_service missing, service_count=%d",
connection->get_connection_index(), connection->address_str().c_str(), service_count);
continue;
}
api::BluetoothGATTGetServicesResponse resp;
resp.address = connection->get_address();
resp.services.reserve(1); // Always one service per response in this implementation
api::BluetoothGATTService service_resp;
service_resp.uuid = get_128bit_uuid_vec(service_result.uuid);
service_resp.handle = service_result.start_handle;
uint16_t char_offset = 0;
esp_gattc_char_elem_t char_result;
// Get the number of characteristics directly with one call
uint16_t total_char_count = 0;
esp_gatt_status_t char_count_status = esp_ble_gattc_get_attr_count(
connection->get_gattc_if(), connection->get_conn_id(), ESP_GATT_DB_CHARACTERISTIC,
service_result.start_handle, service_result.end_handle, 0, &total_char_count);
if (char_count_status == ESP_GATT_OK && total_char_count > 0) {
// Only reserve if we successfully got a count
service_resp.characteristics.reserve(total_char_count);
} else if (char_count_status != ESP_GATT_OK) {
ESP_LOGW(TAG, "[%d] [%s] Error getting characteristic count, status=%d", connection->get_connection_index(),
connection->address_str().c_str(), char_count_status);
}
// Now process characteristics
while (true) { // characteristics
uint16_t char_count = 1;
esp_gatt_status_t char_status = esp_ble_gattc_get_all_char(
connection->get_gattc_if(), connection->get_conn_id(), service_result.start_handle,
service_result.end_handle, &char_result, &char_count, char_offset);
if (char_status == ESP_GATT_INVALID_OFFSET || char_status == ESP_GATT_NOT_FOUND) {
break;
}
if (char_status != ESP_GATT_OK) {
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", connection->get_connection_index(),
connection->address_str().c_str(), char_status);
break;
}
if (char_count == 0) {
break;
}
api::BluetoothGATTCharacteristic characteristic_resp;
characteristic_resp.uuid = get_128bit_uuid_vec(char_result.uuid);
characteristic_resp.handle = char_result.char_handle;
characteristic_resp.properties = char_result.properties;
char_offset++;
// Get the number of descriptors directly with one call
uint16_t total_desc_count = 0;
esp_gatt_status_t desc_count_status =
esp_ble_gattc_get_attr_count(connection->get_gattc_if(), connection->get_conn_id(), ESP_GATT_DB_DESCRIPTOR,
char_result.char_handle, service_result.end_handle, 0, &total_desc_count);
if (desc_count_status == ESP_GATT_OK && total_desc_count > 0) {
// Only reserve if we successfully got a count
characteristic_resp.descriptors.reserve(total_desc_count);
} else if (desc_count_status != ESP_GATT_OK) {
ESP_LOGW(TAG, "[%d] [%s] Error getting descriptor count for char handle %d, status=%d",
connection->get_connection_index(), connection->address_str().c_str(), char_result.char_handle,
desc_count_status);
}
// Now process descriptors
uint16_t desc_offset = 0;
esp_gattc_descr_elem_t desc_result;
while (true) { // descriptors
uint16_t desc_count = 1;
esp_gatt_status_t desc_status =
esp_ble_gattc_get_all_descr(connection->get_gattc_if(), connection->get_conn_id(),
char_result.char_handle, &desc_result, &desc_count, desc_offset);
if (desc_status == ESP_GATT_INVALID_OFFSET || desc_status == ESP_GATT_NOT_FOUND) {
break;
}
if (desc_status != ESP_GATT_OK) {
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_descr error, status=%d", connection->get_connection_index(),
connection->address_str().c_str(), desc_status);
break;
}
if (desc_count == 0) {
break;
}
api::BluetoothGATTDescriptor descriptor_resp;
descriptor_resp.uuid = get_128bit_uuid_vec(desc_result.uuid);
descriptor_resp.handle = desc_result.handle;
characteristic_resp.descriptors.push_back(std::move(descriptor_resp));
desc_offset++;
}
service_resp.characteristics.push_back(std::move(characteristic_resp));
}
resp.services.push_back(std::move(service_resp));
this->api_connection_->send_message(resp, api::BluetoothGATTGetServicesResponse::MESSAGE_TYPE);
}
} }
} }
@ -673,7 +501,6 @@ void BluetoothProxy::bluetooth_scanner_set_mode(bool active) {
BluetoothProxy *global_bluetooth_proxy = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) BluetoothProxy *global_bluetooth_proxy = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace bluetooth_proxy } // namespace esphome::bluetooth_proxy
} // namespace esphome
#endif // USE_ESP32 #endif // USE_ESP32

View File

@ -18,10 +18,10 @@
#include <esp_bt.h> #include <esp_bt.h>
#include <esp_bt_device.h> #include <esp_bt_device.h>
namespace esphome { namespace esphome::bluetooth_proxy {
namespace bluetooth_proxy {
static const esp_err_t ESP_GATT_NOT_CONNECTED = -1; static const esp_err_t ESP_GATT_NOT_CONNECTED = -1;
static const int DONE_SENDING_SERVICES = -2;
using namespace esp32_ble_client; using namespace esp32_ble_client;
@ -131,9 +131,6 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
} }
protected: protected:
#ifdef USE_ESP32_BLE_DEVICE
void send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device);
#endif
void send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerState state); void send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerState state);
BluetoothConnection *get_connection_(uint64_t address, bool reserve); BluetoothConnection *get_connection_(uint64_t address, bool reserve);
@ -149,7 +146,10 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
std::vector<api::BluetoothLERawAdvertisement> advertisement_pool_; std::vector<api::BluetoothLERawAdvertisement> advertisement_pool_;
std::unique_ptr<api::BluetoothLERawAdvertisementsResponse> response_; std::unique_ptr<api::BluetoothLERawAdvertisementsResponse> response_;
// Group 3: 1-byte types grouped together // Group 3: 4-byte types
uint32_t last_advertisement_flush_time_{0};
// Group 4: 1-byte types grouped together
bool active_; bool active_;
uint8_t advertisement_count_{0}; uint8_t advertisement_count_{0};
// 2 bytes used, 2 bytes padding // 2 bytes used, 2 bytes padding
@ -157,7 +157,6 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
extern BluetoothProxy *global_bluetooth_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) extern BluetoothProxy *global_bluetooth_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace bluetooth_proxy } // namespace esphome::bluetooth_proxy
} // namespace esphome
#endif // USE_ESP32 #endif // USE_ESP32

View File

@ -1,7 +1,7 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import esp32, i2c from esphome.components import esp32, i2c
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_SAMPLE_RATE, CONF_TEMPERATURE_OFFSET from esphome.const import CONF_ID, CONF_SAMPLE_RATE, CONF_TEMPERATURE_OFFSET, Framework
CODEOWNERS = ["@trvrnrth"] CODEOWNERS = ["@trvrnrth"]
DEPENDENCIES = ["i2c"] DEPENDENCIES = ["i2c"]
@ -56,7 +56,15 @@ CONFIG_SCHEMA = cv.All(
): cv.positive_time_period_minutes, ): cv.positive_time_period_minutes,
} }
).extend(i2c.i2c_device_schema(0x76)), ).extend(i2c.i2c_device_schema(0x76)),
cv.only_with_arduino, cv.only_with_framework(
frameworks=Framework.ARDUINO,
suggestions={
Framework.ESP_IDF: (
"bme68x_bsec2_i2c",
"sensor/bme68x_bsec2",
)
},
),
cv.Any( cv.Any(
cv.only_on_esp8266, cv.only_on_esp8266,
cv.All( cv.All(

View File

@ -22,9 +22,8 @@ def validate_id(config):
if CONF_CAN_ID in config: if CONF_CAN_ID in config:
can_id = config[CONF_CAN_ID] can_id = config[CONF_CAN_ID]
id_ext = config[CONF_USE_EXTENDED_ID] id_ext = config[CONF_USE_EXTENDED_ID]
if not id_ext: if not id_ext and can_id > 0x7FF:
if can_id > 0x7FF: raise cv.Invalid("Standard IDs must be 11 Bit (0x000-0x7ff / 0-2047)")
raise cv.Invalid("Standard IDs must be 11 Bit (0x000-0x7ff / 0-2047)")
return config return config

View File

@ -3,7 +3,12 @@
CODEOWNERS = ["@esphome/core"] CODEOWNERS = ["@esphome/core"]
CONF_BYTE_ORDER = "byte_order" CONF_BYTE_ORDER = "byte_order"
BYTE_ORDER_LITTLE = "little_endian"
BYTE_ORDER_BIG = "big_endian"
CONF_COLOR_DEPTH = "color_depth" CONF_COLOR_DEPTH = "color_depth"
CONF_DRAW_ROUNDING = "draw_rounding" CONF_DRAW_ROUNDING = "draw_rounding"
CONF_ON_RECEIVE = "on_receive"
CONF_ON_STATE_CHANGE = "on_state_change" CONF_ON_STATE_CHANGE = "on_state_change"
CONF_REQUEST_HEADERS = "request_headers" CONF_REQUEST_HEADERS = "request_headers"
CONF_USE_PSRAM = "use_psram"

View File

@ -74,8 +74,7 @@ def range_segment_list(input):
if isinstance(input, list): if isinstance(input, list):
for list_item in input: for list_item in input:
if isinstance(list_item, list): if isinstance(list_item, list):
for item in list_item: flat_list.extend(list_item)
flat_list.append(item)
else: else:
flat_list.append(list_item) flat_list.append(list_item)
else: else:

View File

@ -31,6 +31,7 @@ from esphome.const import (
KEY_TARGET_FRAMEWORK, KEY_TARGET_FRAMEWORK,
KEY_TARGET_PLATFORM, KEY_TARGET_PLATFORM,
PLATFORM_ESP32, PLATFORM_ESP32,
ThreadModel,
__version__, __version__,
) )
from esphome.core import CORE, HexInt, TimePeriod from esphome.core import CORE, HexInt, TimePeriod
@ -309,19 +310,19 @@ def _format_framework_espidf_version(
# The default/recommended arduino framework version # The default/recommended arduino framework version
# - https://github.com/espressif/arduino-esp32/releases # - https://github.com/espressif/arduino-esp32/releases
RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 1, 3) RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 2, 1)
# The platform-espressif32 version to use for arduino frameworks # The platform-espressif32 version to use for arduino frameworks
# - https://github.com/pioarduino/platform-espressif32/releases # - https://github.com/pioarduino/platform-espressif32/releases
ARDUINO_PLATFORM_VERSION = cv.Version(53, 3, 13) ARDUINO_PLATFORM_VERSION = cv.Version(54, 3, 21)
# The default/recommended esp-idf framework version # The default/recommended esp-idf framework version
# - https://github.com/espressif/esp-idf/releases # - https://github.com/espressif/esp-idf/releases
# - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf # - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf
RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(5, 3, 2) RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(5, 4, 2)
# The platformio/espressif32 version to use for esp-idf frameworks # The platformio/espressif32 version to use for esp-idf frameworks
# - https://github.com/platformio/platform-espressif32/releases # - https://github.com/platformio/platform-espressif32/releases
# - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32 # - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32
ESP_IDF_PLATFORM_VERSION = cv.Version(53, 3, 13) ESP_IDF_PLATFORM_VERSION = cv.Version(54, 3, 21)
# List based on https://registry.platformio.org/tools/platformio/framework-espidf/versions # List based on https://registry.platformio.org/tools/platformio/framework-espidf/versions
SUPPORTED_PLATFORMIO_ESP_IDF_5X = [ SUPPORTED_PLATFORMIO_ESP_IDF_5X = [
@ -356,8 +357,8 @@ SUPPORTED_PIOARDUINO_ESP_IDF_5X = [
def _arduino_check_versions(value): def _arduino_check_versions(value):
value = value.copy() value = value.copy()
lookups = { lookups = {
"dev": (cv.Version(3, 1, 3), "https://github.com/espressif/arduino-esp32.git"), "dev": (cv.Version(3, 2, 1), "https://github.com/espressif/arduino-esp32.git"),
"latest": (cv.Version(3, 1, 3), None), "latest": (cv.Version(3, 2, 1), None),
"recommended": (RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, None), "recommended": (RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, None),
} }
@ -395,8 +396,8 @@ def _arduino_check_versions(value):
def _esp_idf_check_versions(value): def _esp_idf_check_versions(value):
value = value.copy() value = value.copy()
lookups = { lookups = {
"dev": (cv.Version(5, 3, 2), "https://github.com/espressif/esp-idf.git"), "dev": (cv.Version(5, 4, 2), "https://github.com/espressif/esp-idf.git"),
"latest": (cv.Version(5, 3, 2), None), "latest": (cv.Version(5, 2, 2), None),
"recommended": (RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION, None), "recommended": (RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION, None),
} }
@ -713,6 +714,7 @@ async def to_code(config):
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{config[CONF_VARIANT]}") cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{config[CONF_VARIANT]}")
cg.add_define("ESPHOME_VARIANT", VARIANT_FRIENDLY[config[CONF_VARIANT]]) cg.add_define("ESPHOME_VARIANT", VARIANT_FRIENDLY[config[CONF_VARIANT]])
cg.add_define(ThreadModel.MULTI_ATOMICS)
cg.add_platformio_option("lib_ldf_mode", "off") cg.add_platformio_option("lib_ldf_mode", "off")
cg.add_platformio_option("lib_compat_mode", "strict") cg.add_platformio_option("lib_compat_mode", "strict")
@ -951,14 +953,16 @@ def _write_idf_component_yml():
# Called by writer.py # Called by writer.py
def copy_files(): def copy_files():
if CORE.using_arduino: if (
if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]: CORE.using_arduino
write_file_if_changed( and "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]
CORE.relative_build_path("partitions.csv"), ):
get_arduino_partition_csv( write_file_if_changed(
CORE.platformio_options.get("board_upload.flash_size") CORE.relative_build_path("partitions.csv"),
), get_arduino_partition_csv(
) CORE.platformio_options.get("board_upload.flash_size")
),
)
if CORE.using_esp_idf: if CORE.using_esp_idf:
_write_sdkconfig() _write_sdkconfig()
_write_idf_component_yml() _write_idf_component_yml()
@ -978,7 +982,7 @@ def copy_files():
__version__, __version__,
) )
for _, file in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES].items(): for file in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES].values():
if file[KEY_PATH].startswith("http"): if file[KEY_PATH].startswith("http"):
import requests import requests

View File

@ -1,77 +1,112 @@
# Source https://github.com/letscontrolit/ESPEasy/pull/3845#issuecomment-1005864664 Import("env")
# pylint: disable=E0602
Import("env") # noqa
import os import os
import json
import shutil import shutil
import pathlib
import itertools
if os.environ.get("ESPHOME_USE_SUBPROCESS") is None: def merge_factory_bin(source, target, env):
try: """
import esptool Merges all flash sections into a single .factory.bin using esptool.
except ImportError: Attempts multiple methods to detect image layout: flasher_args.json, FLASH_EXTRA_IMAGES, fallback guesses.
env.Execute("$PYTHONEXE -m pip install esptool") """
else: firmware_name = os.path.basename(env.subst("$PROGNAME")) + ".bin"
import subprocess build_dir = pathlib.Path(env.subst("$BUILD_DIR"))
from SCons.Script import ARGUMENTS firmware_path = build_dir / firmware_name
flash_size = env.BoardConfig().get("upload.flash_size", "4MB")
chip = env.BoardConfig().get("build.mcu", "esp32")
# Copy over the default sdkconfig. sections = []
from os import path flasher_args_path = build_dir / "flasher_args.json"
if path.exists("./sdkconfig.defaults"): # 1. Try flasher_args.json
os.makedirs(".temp", exist_ok=True) if flasher_args_path.exists():
shutil.copy("./sdkconfig.defaults", "./.temp/sdkconfig-esp32-idf") try:
with flasher_args_path.open() as f:
flash_data = json.load(f)
for addr, fname in sorted(flash_data["flash_files"].items(), key=lambda kv: int(kv[0], 16)):
file_path = pathlib.Path(fname)
if file_path.exists():
sections.append((addr, str(file_path)))
else:
print(f"Info: {file_path.name} not found - skipping")
except Exception as e:
print(f"Warning: Failed to parse flasher_args.json - {e}")
# 2. Try FLASH_EXTRA_IMAGES if flasher_args.json failed or was empty
if not sections:
flash_images = env.get("FLASH_EXTRA_IMAGES")
if flash_images:
print("Using FLASH_EXTRA_IMAGES from PlatformIO environment")
# flatten any nested lists
flat = list(itertools.chain.from_iterable(
x if isinstance(x, (list, tuple)) else [x] for x in flash_images
))
entries = [env.subst(x) for x in flat]
for i in range(0, len(entries) - 1, 2):
addr, fname = entries[i], entries[i + 1]
if isinstance(fname, (list, tuple)):
print(f"Warning: Skipping malformed FLASH_EXTRA_IMAGES entry: {fname}")
continue
file_path = pathlib.Path(str(fname))
if file_path.exists():
sections.append((addr, str(file_path)))
else:
print(f"Info: {file_path.name} not found — skipping")
def esp32_create_combined_bin(source, target, env): # 3. Final fallback: guess standard image locations
verbose = bool(int(ARGUMENTS.get("PIOVERBOSE", "0"))) if not sections:
if verbose: print("Fallback: guessing legacy image paths")
print("Generating combined binary for serial flashing") guesses = [
app_offset = 0x10000 ("0x0", build_dir / "bootloader" / "bootloader.bin"),
("0x8000", build_dir / "partition_table" / "partition-table.bin"),
("0xe000", build_dir / "ota_data_initial.bin"),
("0x10000", firmware_path)
]
for addr, file_path in guesses:
if file_path.exists():
sections.append((addr, str(file_path)))
else:
print(f"Info: {file_path.name} not found — skipping")
new_file_name = env.subst("$BUILD_DIR/${PROGNAME}.factory.bin") # If no valid sections found, skip merge
sections = env.subst(env.get("FLASH_EXTRA_IMAGES")) if not sections:
firmware_name = env.subst("$BUILD_DIR/${PROGNAME}.bin") print("No valid flash sections found — skipping .factory.bin creation.")
chip = env.get("BOARD_MCU") return
flash_size = env.BoardConfig().get("upload.flash_size")
output_path = firmware_path.with_suffix(".factory.bin")
cmd = [ cmd = [
"--chip", "--chip", chip,
chip,
"merge_bin", "merge_bin",
"-o", "--flash_size", flash_size,
new_file_name, "--output", str(output_path)
"--flash_size",
flash_size,
] ]
if verbose: for addr, file_path in sections:
print(" Offset | File") cmd += [addr, file_path]
for section in sections:
sect_adr, sect_file = section.split(" ", 1)
if verbose:
print(f" - {sect_adr} | {sect_file}")
cmd += [sect_adr, sect_file]
cmd += [hex(app_offset), firmware_name] print(f"Merging binaries into {output_path}")
result = env.Execute(
env.VerboseAction(
f"{env.subst('$PYTHONEXE')} -m esptool " + " ".join(cmd),
"Merging binaries with esptool"
)
)
if verbose: if result == 0:
print(f" - {hex(app_offset)} | {firmware_name}") print(f"Successfully created {output_path}")
print()
print(f"Using esptool.py arguments: {' '.join(cmd)}")
print()
if os.environ.get("ESPHOME_USE_SUBPROCESS") is None:
esptool.main(cmd)
else: else:
subprocess.run(["esptool.py", *cmd]) print(f"Error: esptool merge_bin failed with code {result}")
def esp32_copy_ota_bin(source, target, env): def esp32_copy_ota_bin(source, target, env):
"""
Copy the main firmware to a .ota.bin file for compatibility with ESPHome OTA tools.
"""
firmware_name = env.subst("$BUILD_DIR/${PROGNAME}.bin") firmware_name = env.subst("$BUILD_DIR/${PROGNAME}.bin")
new_file_name = env.subst("$BUILD_DIR/${PROGNAME}.ota.bin") new_file_name = env.subst("$BUILD_DIR/${PROGNAME}.ota.bin")
shutil.copyfile(firmware_name, new_file_name) shutil.copyfile(firmware_name, new_file_name)
print(f"Copied firmware to {new_file_name}")
# Run merge first, then ota copy second
# pylint: disable=E0602 env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", merge_factory_bin)
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_create_combined_bin) # noqa env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_copy_ota_bin)
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_copy_ota_bin) # noqa

View File

@ -19,8 +19,7 @@
#include <esp32-hal-bt.h> #include <esp32-hal-bt.h>
#endif #endif
namespace esphome { namespace esphome::esp32_ble {
namespace esp32_ble {
static const char *const TAG = "esp32_ble"; static const char *const TAG = "esp32_ble";
@ -538,7 +537,6 @@ uint64_t ble_addr_to_uint64(const esp_bd_addr_t address) {
ESP32BLE *global_ble = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) ESP32BLE *global_ble = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace esp32_ble } // namespace esphome::esp32_ble
} // namespace esphome
#endif #endif

View File

@ -21,8 +21,7 @@
#include <esp_gattc_api.h> #include <esp_gattc_api.h>
#include <esp_gatts_api.h> #include <esp_gatts_api.h>
namespace esphome { namespace esphome::esp32_ble {
namespace esp32_ble {
// Maximum number of BLE scan results to buffer // Maximum number of BLE scan results to buffer
// Sized to handle bursts of advertisements while allowing for processing delays // Sized to handle bursts of advertisements while allowing for processing delays
@ -191,7 +190,6 @@ template<typename... Ts> class BLEDisableAction : public Action<Ts...> {
void play(Ts... x) override { global_ble->disable(); } void play(Ts... x) override { global_ble->disable(); }
}; };
} // namespace esp32_ble } // namespace esphome::esp32_ble
} // namespace esphome
#endif #endif

View File

@ -8,8 +8,7 @@
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/application.h" #include "esphome/core/application.h"
namespace esphome { namespace esphome::esp32_ble {
namespace esp32_ble {
static const char *const TAG = "esp32_ble.advertising"; static const char *const TAG = "esp32_ble.advertising";
@ -160,7 +159,6 @@ void BLEAdvertising::register_raw_advertisement_callback(std::function<void(bool
this->raw_advertisements_callbacks_.push_back(std::move(callback)); this->raw_advertisements_callbacks_.push_back(std::move(callback));
} }
} // namespace esp32_ble } // namespace esphome::esp32_ble
} // namespace esphome
#endif #endif

View File

@ -10,8 +10,7 @@
#include <esp_gap_ble_api.h> #include <esp_gap_ble_api.h>
#include <esp_gatts_api.h> #include <esp_gatts_api.h>
namespace esphome { namespace esphome::esp32_ble {
namespace esp32_ble {
using raw_adv_data_t = struct { using raw_adv_data_t = struct {
uint8_t *data; uint8_t *data;
@ -55,7 +54,6 @@ class BLEAdvertising {
int8_t current_adv_index_{-1}; // -1 means standard scan response int8_t current_adv_index_{-1}; // -1 means standard scan response
}; };
} // namespace esp32_ble } // namespace esphome::esp32_ble
} // namespace esphome
#endif #endif

View File

@ -11,8 +11,7 @@
#include "ble_scan_result.h" #include "ble_scan_result.h"
namespace esphome { namespace esphome::esp32_ble {
namespace esp32_ble {
// Compile-time verification that ESP-IDF scan complete events only contain a status field // Compile-time verification that ESP-IDF scan complete events only contain a status field
// This ensures our reinterpret_cast in ble.cpp is safe // This ensures our reinterpret_cast in ble.cpp is safe
@ -395,7 +394,6 @@ static_assert(sizeof(esp_ble_sec_t) <= 73, "esp_ble_sec_t is larger than BLEScan
// BLEEvent total size: 84 bytes (80 byte union + 1 byte type + 3 bytes padding) // BLEEvent total size: 84 bytes (80 byte union + 1 byte type + 3 bytes padding)
} // namespace esp32_ble } // namespace esphome::esp32_ble
} // namespace esphome
#endif #endif

View File

@ -4,8 +4,7 @@
#include <esp_gap_ble_api.h> #include <esp_gap_ble_api.h>
namespace esphome { namespace esphome::esp32_ble {
namespace esp32_ble {
// Structure for BLE scan results - only fields we actually use // Structure for BLE scan results - only fields we actually use
struct __attribute__((packed)) BLEScanResult { struct __attribute__((packed)) BLEScanResult {
@ -18,7 +17,6 @@ struct __attribute__((packed)) BLEScanResult {
uint8_t search_evt; uint8_t search_evt;
}; // ~73 bytes vs ~400 bytes for full esp_ble_gap_cb_param_t }; // ~73 bytes vs ~400 bytes for full esp_ble_gap_cb_param_t
} // namespace esp32_ble } // namespace esphome::esp32_ble
} // namespace esphome
#endif #endif

View File

@ -7,8 +7,7 @@
#include <cinttypes> #include <cinttypes>
#include "esphome/core/log.h" #include "esphome/core/log.h"
namespace esphome { namespace esphome::esp32_ble {
namespace esp32_ble {
static const char *const TAG = "esp32_ble"; static const char *const TAG = "esp32_ble";
@ -189,7 +188,6 @@ std::string ESPBTUUID::to_string() const {
return ""; return "";
} }
} // namespace esp32_ble } // namespace esphome::esp32_ble
} // namespace esphome
#endif #endif

View File

@ -8,8 +8,7 @@
#include <string> #include <string>
#include <esp_bt_defs.h> #include <esp_bt_defs.h>
namespace esphome { namespace esphome::esp32_ble {
namespace esp32_ble {
class ESPBTUUID { class ESPBTUUID {
public: public:
@ -41,7 +40,6 @@ class ESPBTUUID {
esp_bt_uuid_t uuid_; esp_bt_uuid_t uuid_;
}; };
} // namespace esp32_ble } // namespace esphome::esp32_ble
} // namespace esphome
#endif #endif

View File

@ -140,20 +140,22 @@ VALUE_TYPES = {
def validate_char_on_write(char_config): def validate_char_on_write(char_config):
if CONF_ON_WRITE in char_config: if (
if not char_config[CONF_WRITE] and not char_config[CONF_WRITE_NO_RESPONSE]: CONF_ON_WRITE in char_config
raise cv.Invalid( and not char_config[CONF_WRITE]
f"{CONF_ON_WRITE} requires the {CONF_WRITE} or {CONF_WRITE_NO_RESPONSE} property to be set" and not char_config[CONF_WRITE_NO_RESPONSE]
) ):
raise cv.Invalid(
f"{CONF_ON_WRITE} requires the {CONF_WRITE} or {CONF_WRITE_NO_RESPONSE} property to be set"
)
return char_config return char_config
def validate_descriptor(desc_config): def validate_descriptor(desc_config):
if CONF_ON_WRITE in desc_config: if CONF_ON_WRITE in desc_config and not desc_config[CONF_WRITE]:
if not desc_config[CONF_WRITE]: raise cv.Invalid(
raise cv.Invalid( f"{CONF_ON_WRITE} requires the {CONF_WRITE} property to be set"
f"{CONF_ON_WRITE} requires the {CONF_WRITE} property to be set" )
)
if CONF_MAX_LENGTH not in desc_config: if CONF_MAX_LENGTH not in desc_config:
value = desc_config[CONF_VALUE][CONF_DATA] value = desc_config[CONF_VALUE][CONF_DATA]
if cg.is_template(value): if cg.is_template(value):

View File

@ -30,7 +30,7 @@ from esphome.const import (
CONF_SERVICE_UUID, CONF_SERVICE_UUID,
CONF_TRIGGER_ID, CONF_TRIGGER_ID,
) )
from esphome.core import CORE from esphome.core import CORE, coroutine_with_priority
from esphome.enum import StrEnum from esphome.enum import StrEnum
from esphome.types import ConfigType from esphome.types import ConfigType
@ -310,9 +310,7 @@ async def to_code(config):
for conf in config.get(CONF_ON_BLE_ADVERTISE, []): for conf in config.get(CONF_ON_BLE_ADVERTISE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
if CONF_MAC_ADDRESS in conf: if CONF_MAC_ADDRESS in conf:
addr_list = [] addr_list = [it.as_hex for it in conf[CONF_MAC_ADDRESS]]
for it in conf[CONF_MAC_ADDRESS]:
addr_list.append(it.as_hex)
cg.add(trigger.set_addresses(addr_list)) cg.add(trigger.set_addresses(addr_list))
await automation.build_automation(trigger, [(ESPBTDeviceConstRef, "x")], conf) await automation.build_automation(trigger, [(ESPBTDeviceConstRef, "x")], conf)
for conf in config.get(CONF_ON_BLE_SERVICE_DATA_ADVERTISE, []): for conf in config.get(CONF_ON_BLE_SERVICE_DATA_ADVERTISE, []):
@ -365,14 +363,22 @@ async def to_code(config):
cg.add_define("USE_OTA_STATE_CALLBACK") # To be notified when an OTA update starts cg.add_define("USE_OTA_STATE_CALLBACK") # To be notified when an OTA update starts
cg.add_define("USE_ESP32_BLE_CLIENT") cg.add_define("USE_ESP32_BLE_CLIENT")
# Add feature-specific defines based on what's needed CORE.add_job(_add_ble_features)
if BLEFeatures.ESP_BT_DEVICE in _required_features:
cg.add_define("USE_ESP32_BLE_DEVICE")
if config.get(CONF_SOFTWARE_COEXISTENCE): if config.get(CONF_SOFTWARE_COEXISTENCE):
cg.add_define("USE_ESP32_BLE_SOFTWARE_COEXISTENCE") cg.add_define("USE_ESP32_BLE_SOFTWARE_COEXISTENCE")
# This needs to be run as a job with very low priority so that all components have
# chance to call register_ble_tracker and register_client before the list is checked
# and added to the global defines list.
@coroutine_with_priority(-1000)
async def _add_ble_features():
# Add feature-specific defines based on what's needed
if BLEFeatures.ESP_BT_DEVICE in _required_features:
cg.add_define("USE_ESP32_BLE_DEVICE")
ESP32_BLE_START_SCAN_ACTION_SCHEMA = cv.Schema( ESP32_BLE_START_SCAN_ACTION_SCHEMA = cv.Schema(
{ {
cv.GenerateID(): cv.use_id(ESP32BLETracker), cv.GenerateID(): cv.use_id(ESP32BLETracker),

View File

@ -5,8 +5,7 @@
#ifdef USE_ESP32 #ifdef USE_ESP32
namespace esphome { namespace esphome::esp32_ble_tracker {
namespace esp32_ble_tracker {
#ifdef USE_ESP32_BLE_DEVICE #ifdef USE_ESP32_BLE_DEVICE
class ESPBTAdvertiseTrigger : public Trigger<const ESPBTDevice &>, public ESPBTDeviceListener { class ESPBTAdvertiseTrigger : public Trigger<const ESPBTDevice &>, public ESPBTDeviceListener {
public: public:
@ -108,7 +107,6 @@ template<typename... Ts> class ESP32BLEStopScanAction : public Action<Ts...>, pu
void play(Ts... x) override { this->parent_->stop_scan(); } void play(Ts... x) override { this->parent_->stop_scan(); }
}; };
} // namespace esp32_ble_tracker } // namespace esphome::esp32_ble_tracker
} // namespace esphome
#endif #endif

View File

@ -35,8 +35,7 @@
// bt_trace.h // bt_trace.h
#undef TAG #undef TAG
namespace esphome { namespace esphome::esp32_ble_tracker {
namespace esp32_ble_tracker {
static const char *const TAG = "esp32_ble_tracker"; static const char *const TAG = "esp32_ble_tracker";
@ -128,46 +127,53 @@ void ESP32BLETracker::loop() {
uint8_t write_idx = this->ring_write_index_.load(std::memory_order_acquire); uint8_t write_idx = this->ring_write_index_.load(std::memory_order_acquire);
while (read_idx != write_idx) { while (read_idx != write_idx) {
// Process one result at a time directly from ring buffer // Calculate how many contiguous results we can process in one batch
BLEScanResult &scan_result = this->scan_ring_buffer_[read_idx]; // If write > read: process all results from read to write
// If write <= read (wraparound): process from read to end of buffer first
size_t batch_size = (write_idx > read_idx) ? (write_idx - read_idx) : (SCAN_RESULT_BUFFER_SIZE - read_idx);
// Process the batch for raw advertisements
if (this->raw_advertisements_) { if (this->raw_advertisements_) {
for (auto *listener : this->listeners_) { for (auto *listener : this->listeners_) {
listener->parse_devices(&scan_result, 1); listener->parse_devices(&this->scan_ring_buffer_[read_idx], batch_size);
} }
for (auto *client : this->clients_) { for (auto *client : this->clients_) {
client->parse_devices(&scan_result, 1); client->parse_devices(&this->scan_ring_buffer_[read_idx], batch_size);
} }
} }
// Process individual results for parsed advertisements
if (this->parse_advertisements_) { if (this->parse_advertisements_) {
#ifdef USE_ESP32_BLE_DEVICE #ifdef USE_ESP32_BLE_DEVICE
ESPBTDevice device; for (size_t i = 0; i < batch_size; i++) {
device.parse_scan_rst(scan_result); BLEScanResult &scan_result = this->scan_ring_buffer_[read_idx + i];
ESPBTDevice device;
device.parse_scan_rst(scan_result);
bool found = false; bool found = false;
for (auto *listener : this->listeners_) { for (auto *listener : this->listeners_) {
if (listener->parse_device(device)) if (listener->parse_device(device))
found = true; found = true;
} }
for (auto *client : this->clients_) { for (auto *client : this->clients_) {
if (client->parse_device(device)) { if (client->parse_device(device)) {
found = true; found = true;
if (!connecting && client->state() == ClientState::DISCOVERED) { if (!connecting && client->state() == ClientState::DISCOVERED) {
promote_to_connecting = true; promote_to_connecting = true;
}
} }
} }
}
if (!found && !this->scan_continuous_) { if (!found && !this->scan_continuous_) {
this->print_bt_device_info(device); this->print_bt_device_info(device);
}
} }
#endif // USE_ESP32_BLE_DEVICE #endif // USE_ESP32_BLE_DEVICE
} }
// Move to next entry in ring buffer // Update read index for entire batch
read_idx = (read_idx + 1) % SCAN_RESULT_BUFFER_SIZE; read_idx = (read_idx + batch_size) % SCAN_RESULT_BUFFER_SIZE;
// Store with release to ensure reads complete before index update // Store with release to ensure reads complete before index update
this->ring_read_index_.store(read_idx, std::memory_order_release); this->ring_read_index_.store(read_idx, std::memory_order_release);
@ -875,7 +881,6 @@ bool ESPBTDevice::resolve_irk(const uint8_t *irk) const {
} }
#endif // USE_ESP32_BLE_DEVICE #endif // USE_ESP32_BLE_DEVICE
} // namespace esp32_ble_tracker } // namespace esphome::esp32_ble_tracker
} // namespace esphome
#endif // USE_ESP32 #endif // USE_ESP32

View File

@ -22,8 +22,7 @@
#include "esphome/components/esp32_ble/ble.h" #include "esphome/components/esp32_ble/ble.h"
#include "esphome/components/esp32_ble/ble_uuid.h" #include "esphome/components/esp32_ble/ble_uuid.h"
namespace esphome { namespace esphome::esp32_ble_tracker {
namespace esp32_ble_tracker {
using namespace esp32_ble; using namespace esp32_ble;
@ -321,7 +320,6 @@ class ESP32BLETracker : public Component,
// NOLINTNEXTLINE // NOLINTNEXTLINE
extern ESP32BLETracker *global_esp32_ble_tracker; extern ESP32BLETracker *global_esp32_ble_tracker;
} // namespace esp32_ble_tracker } // namespace esphome::esp32_ble_tracker
} // namespace esphome
#endif #endif

View File

@ -4,6 +4,7 @@ import logging
from esphome import pins 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
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_CHIPSET, CONF_CHIPSET,
@ -57,7 +58,6 @@ CHIPSETS = {
"SM16703": LEDStripTimings(300, 900, 900, 300, 0, 0), "SM16703": LEDStripTimings(300, 900, 900, 300, 0, 0),
} }
CONF_USE_PSRAM = "use_psram"
CONF_IS_WRGB = "is_wrgb" CONF_IS_WRGB = "is_wrgb"
CONF_BIT0_HIGH = "bit0_high" CONF_BIT0_HIGH = "bit0_high"
CONF_BIT0_LOW = "bit0_low" CONF_BIT0_LOW = "bit0_low"

View File

@ -294,9 +294,8 @@ async def to_code(config):
) )
) )
if get_esp32_variant() == VARIANT_ESP32: if get_esp32_variant() == VARIANT_ESP32 and CONF_IIR_FILTER in config:
if CONF_IIR_FILTER in config: cg.add(touch.set_iir_filter(config[CONF_IIR_FILTER]))
cg.add(touch.set_iir_filter(config[CONF_IIR_FILTER]))
if get_esp32_variant() == VARIANT_ESP32S2 or get_esp32_variant() == VARIANT_ESP32S3: if get_esp32_variant() == VARIANT_ESP32S2 or get_esp32_variant() == VARIANT_ESP32S3:
if CONF_FILTER_MODE in config: if CONF_FILTER_MODE in config:

View File

@ -16,6 +16,8 @@ namespace esp32_touch {
static const char *const TAG = "esp32_touch"; static const char *const TAG = "esp32_touch";
static const uint32_t SETUP_MODE_THRESHOLD = 0xFFFF;
void ESP32TouchComponent::setup() { void ESP32TouchComponent::setup() {
// Create queue for touch events // Create queue for touch events
// Queue size calculation: children * 4 allows for burst scenarios where ISR // Queue size calculation: children * 4 allows for burst scenarios where ISR
@ -44,7 +46,11 @@ void ESP32TouchComponent::setup() {
// Configure each touch pad // Configure each touch pad
for (auto *child : this->children_) { for (auto *child : this->children_) {
touch_pad_config(child->get_touch_pad(), child->get_threshold()); if (this->setup_mode_) {
touch_pad_config(child->get_touch_pad(), SETUP_MODE_THRESHOLD);
} else {
touch_pad_config(child->get_touch_pad(), child->get_threshold());
}
} }
// Register ISR handler // Register ISR handler
@ -114,8 +120,8 @@ void ESP32TouchComponent::loop() {
child->publish_state(new_state); child->publish_state(new_state);
// Original ESP32: ISR only fires when touched, release is detected by timeout // Original ESP32: ISR only fires when touched, release is detected by timeout
// Note: ESP32 v1 uses inverted logic - touched when value < threshold // Note: ESP32 v1 uses inverted logic - touched when value < threshold
ESP_LOGV(TAG, "Touch Pad '%s' state: ON (value: %" PRIu32 " < threshold: %" PRIu32 ")", ESP_LOGV(TAG, "Touch Pad '%s' state: %s (value: %" PRIu32 " < threshold: %" PRIu32 ")",
child->get_name().c_str(), event.value, child->get_threshold()); child->get_name().c_str(), ONOFF(new_state), event.value, child->get_threshold());
} }
break; // Exit inner loop after processing matching pad break; // Exit inner loop after processing matching pad
} }
@ -188,11 +194,6 @@ void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) {
// as any pad remains touched. This allows us to detect both new touches and // as any pad remains touched. This allows us to detect both new touches and
// continued touches, but releases must be detected by timeout in the main loop. // continued touches, but releases must be detected by timeout in the main loop.
// IMPORTANT: ESP32 v1 touch detection logic - INVERTED compared to v2!
// ESP32 v1: Touch is detected when capacitance INCREASES, causing the measured value to DECREASE
// Therefore: touched = (value < threshold)
// This is opposite to ESP32-S2/S3 v2 where touched = (value > threshold)
// Process all configured pads to check their current state // Process all configured pads to check their current state
// Note: ESP32 v1 doesn't tell us which specific pad triggered the interrupt, // Note: ESP32 v1 doesn't tell us which specific pad triggered the interrupt,
// so we must scan all configured pads to find which ones were touched // so we must scan all configured pads to find which ones were touched
@ -211,11 +212,16 @@ void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) {
} }
// Skip pads that arent in the trigger mask // Skip pads that arent in the trigger mask
bool is_touched = (mask >> pad) & 1; if (((mask >> pad) & 1) == 0) {
if (!is_touched) {
continue; continue;
} }
// IMPORTANT: ESP32 v1 touch detection logic - INVERTED compared to v2!
// ESP32 v1: Touch is detected when capacitance INCREASES, causing the measured value to DECREASE
// Therefore: touched = (value < threshold)
// This is opposite to ESP32-S2/S3 v2 where touched = (value > threshold)
bool is_touched = value < child->get_threshold();
// Always send the current state - the main loop will filter for changes // Always send the current state - the main loop will filter for changes
// We send both touched and untouched states because the ISR doesn't // We send both touched and untouched states because the ISR doesn't
// track previous state (to keep ISR fast and simple) // track previous state (to keep ISR fast and simple)

View File

@ -15,6 +15,7 @@ from esphome.const import (
KEY_TARGET_FRAMEWORK, KEY_TARGET_FRAMEWORK,
KEY_TARGET_PLATFORM, KEY_TARGET_PLATFORM,
PLATFORM_ESP8266, PLATFORM_ESP8266,
ThreadModel,
) )
from esphome.core import CORE, coroutine_with_priority from esphome.core import CORE, coroutine_with_priority
from esphome.helpers import copy_file_if_changed from esphome.helpers import copy_file_if_changed
@ -187,6 +188,7 @@ async def to_code(config):
cg.set_cpp_standard("gnu++20") cg.set_cpp_standard("gnu++20")
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
cg.add_define("ESPHOME_VARIANT", "ESP8266") cg.add_define("ESPHOME_VARIANT", "ESP8266")
cg.add_define(ThreadModel.SINGLE)
cg.add_platformio_option("extra_scripts", ["post:post_build.py"]) cg.add_platformio_option("extra_scripts", ["post:post_build.py"])
@ -243,7 +245,7 @@ async def to_code(config):
if ver <= cv.Version(2, 3, 0): if ver <= cv.Version(2, 3, 0):
# No ld script support # No ld script support
ld_script = None ld_script = None
if ver <= cv.Version(2, 4, 2): elif ver <= cv.Version(2, 4, 2):
# Old ld script path # Old ld script path
ld_script = ld_scripts[0] ld_script = ld_scripts[0]
else: else:

View File

@ -73,8 +73,7 @@ def ota_esphome_final_validate(config):
else: else:
new_ota_conf.append(ota_conf) new_ota_conf.append(ota_conf)
for port_conf in merged_ota_esphome_configs_by_port.values(): new_ota_conf.extend(merged_ota_esphome_configs_by_port.values())
new_ota_conf.append(port_conf)
full_conf[CONF_OTA] = new_ota_conf full_conf[CONF_OTA] = new_ota_conf
fv.full_config.set(full_conf) fv.full_config.set(full_conf)

View File

@ -112,7 +112,7 @@ def _is_framework_spi_polling_mode_supported():
return True return True
if cv.Version(5, 3, 0) > framework_version >= cv.Version(5, 2, 1): if cv.Version(5, 3, 0) > framework_version >= cv.Version(5, 2, 1):
return True return True
if cv.Version(5, 2, 0) > framework_version >= cv.Version(5, 1, 4): if cv.Version(5, 2, 0) > framework_version >= cv.Version(5, 1, 4): # noqa: SIM103
return True return True
return False return False
if CORE.using_arduino: if CORE.using_arduino:

View File

@ -25,6 +25,9 @@
#include "driver/gpio.h" #include "driver/gpio.h"
#include "esp_rom_gpio.h" #include "esp_rom_gpio.h"
#include "esp_rom_sys.h" #include "esp_rom_sys.h"
#include "esp_idf_version.h"
#if defined(USE_ARDUINO) || ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2)
static const char *TAG = "jl1101"; static const char *TAG = "jl1101";
#define PHY_CHECK(a, str, goto_tag, ...) \ #define PHY_CHECK(a, str, goto_tag, ...) \
@ -336,4 +339,6 @@ esp_eth_phy_t *esp_eth_phy_new_jl1101(const eth_phy_config_t *config) {
err: err:
return NULL; return NULL;
} }
#endif /* USE_ARDUINO */
#endif /* USE_ESP32 */ #endif /* USE_ESP32 */

View File

@ -11,6 +11,7 @@
#include "esp_eth_mac.h" #include "esp_eth_mac.h"
#include "esp_netif.h" #include "esp_netif.h"
#include "esp_mac.h" #include "esp_mac.h"
#include "esp_idf_version.h"
namespace esphome { namespace esphome {
namespace ethernet { namespace ethernet {
@ -153,7 +154,10 @@ class EthernetComponent : public Component {
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
extern EthernetComponent *global_eth_component; extern EthernetComponent *global_eth_component;
#if defined(USE_ARDUINO) || ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2)
extern "C" esp_eth_phy_t *esp_eth_phy_new_jl1101(const eth_phy_config_t *config); extern "C" esp_eth_phy_t *esp_eth_phy_new_jl1101(const eth_phy_config_t *config);
#endif
} // namespace ethernet } // namespace ethernet
} // namespace esphome } // namespace esphome

View File

@ -1,5 +1,97 @@
from esphome.automation import Trigger, build_automation, validate_automation
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components.esp8266 import CONF_RESTORE_FROM_FLASH, KEY_ESP8266
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
CONF_TRIGGER_ID,
PLATFORM_BK72XX,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_LN882X,
PLATFORM_RTL87XX,
)
from esphome.core import CORE
from esphome.final_validate import full_config
CODEOWNERS = ["@anatoly-savchenkov"] CODEOWNERS = ["@anatoly-savchenkov"]
factory_reset_ns = cg.esphome_ns.namespace("factory_reset") factory_reset_ns = cg.esphome_ns.namespace("factory_reset")
FactoryResetComponent = factory_reset_ns.class_("FactoryResetComponent", cg.Component)
FastBootTrigger = factory_reset_ns.class_("FastBootTrigger", Trigger, cg.Component)
CONF_MAX_DELAY = "max_delay"
CONF_RESETS_REQUIRED = "resets_required"
CONF_ON_INCREMENT = "on_increment"
def _validate(config):
if CONF_RESETS_REQUIRED in config:
return cv.only_on(
[
PLATFORM_BK72XX,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_LN882X,
PLATFORM_RTL87XX,
]
)(config)
if CONF_ON_INCREMENT in config:
raise cv.Invalid(
f"'{CONF_ON_INCREMENT}' requires a value for '{CONF_RESETS_REQUIRED}'"
)
return config
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(FactoryResetComponent),
cv.Optional(CONF_MAX_DELAY, default="10s"): cv.All(
cv.positive_time_period_seconds,
cv.Range(min=cv.TimePeriod(milliseconds=1000)),
),
cv.Optional(CONF_RESETS_REQUIRED): cv.positive_not_null_int,
cv.Optional(CONF_ON_INCREMENT): validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FastBootTrigger),
}
),
}
).extend(cv.COMPONENT_SCHEMA),
_validate,
)
def _final_validate(config):
if CORE.is_esp8266 and CONF_RESETS_REQUIRED in config:
fconfig = full_config.get()
if not fconfig.get_config_for_path([KEY_ESP8266, CONF_RESTORE_FROM_FLASH]):
raise cv.Invalid(
"'resets_required' needs 'restore_from_flash' to be enabled in the 'esp8266' configuration"
)
return config
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config):
if reset_count := config.get(CONF_RESETS_REQUIRED):
var = cg.new_Pvariable(
config[CONF_ID],
reset_count,
config[CONF_MAX_DELAY].total_milliseconds,
)
await cg.register_component(var, config)
for conf in config.get(CONF_ON_INCREMENT, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await build_automation(
trigger,
[
(cg.uint8, "x"),
(cg.uint8, "target"),
],
conf,
)

View File

@ -0,0 +1,76 @@
#include "factory_reset.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include <cinttypes>
#if !defined(USE_RP2040) && !defined(USE_HOST)
namespace esphome {
namespace factory_reset {
static const char *const TAG = "factory_reset";
static const uint32_t POWER_CYCLES_KEY = 0xFA5C0DE;
static bool was_power_cycled() {
#ifdef USE_ESP32
return esp_reset_reason() == ESP_RST_POWERON;
#endif
#ifdef USE_ESP8266
auto reset_reason = EspClass::getResetReason();
return strcasecmp(reset_reason.c_str(), "power On") == 0 || strcasecmp(reset_reason.c_str(), "external system") == 0;
#endif
#ifdef USE_LIBRETINY
auto reason = lt_get_reboot_reason();
return reason == REBOOT_REASON_POWER || reason == REBOOT_REASON_HARDWARE;
#endif
}
void FactoryResetComponent::dump_config() {
uint8_t count = 0;
this->flash_.load(&count);
ESP_LOGCONFIG(TAG, "Factory Reset by Reset:");
ESP_LOGCONFIG(TAG,
" Max interval between resets %" PRIu32 " seconds\n"
" Current count: %u\n"
" Factory reset after %u resets",
this->max_interval_ / 1000, count, this->required_count_);
}
void FactoryResetComponent::save_(uint8_t count) {
this->flash_.save(&count);
global_preferences->sync();
this->defer([count, this] { this->increment_callback_.call(count, this->required_count_); });
}
void FactoryResetComponent::setup() {
this->flash_ = global_preferences->make_preference<uint8_t>(POWER_CYCLES_KEY, true);
if (was_power_cycled()) {
uint8_t count = 0;
this->flash_.load(&count);
// this is a power on reset or external system reset
count++;
if (count == this->required_count_) {
ESP_LOGW(TAG, "Reset count reached, factory resetting");
global_preferences->reset();
// delay to allow log to be sent
delay(100); // NOLINT
App.safe_reboot(); // should not return
}
this->save_(count);
ESP_LOGD(TAG, "Power on reset detected, incremented count to %u", count);
this->set_timeout(this->max_interval_, [this]() {
ESP_LOGD(TAG, "No reset in the last %" PRIu32 " seconds, resetting count", this->max_interval_ / 1000);
this->save_(0); // reset count
});
} else {
this->save_(0); // reset count if not a power cycle
}
}
} // namespace factory_reset
} // namespace esphome
#endif // !defined(USE_RP2040) && !defined(USE_HOST)

View File

@ -0,0 +1,43 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "esphome/core/preferences.h"
#if !defined(USE_RP2040) && !defined(USE_HOST)
#ifdef USE_ESP32
#include <esp_system.h>
#endif
namespace esphome {
namespace factory_reset {
class FactoryResetComponent : public Component {
public:
FactoryResetComponent(uint8_t required_count, uint32_t max_interval)
: required_count_(required_count), max_interval_(max_interval) {}
void dump_config() override;
void setup() override;
void add_increment_callback(std::function<void(uint8_t, uint8_t)> &&callback) {
this->increment_callback_.add(std::move(callback));
}
protected:
~FactoryResetComponent() = default;
void save_(uint8_t count);
ESPPreferenceObject flash_{}; // saves the number of fast power cycles
uint8_t required_count_; // The number of boot attempts before fast boot is enabled
uint32_t max_interval_; // max interval between power cycles
CallbackManager<void(uint8_t, uint8_t)> increment_callback_{};
};
class FastBootTrigger : public Trigger<uint8_t, uint8_t> {
public:
explicit FastBootTrigger(FactoryResetComponent *parent) {
parent->add_increment_callback([this](uint8_t current, uint8_t target) { this->trigger(current, target); });
}
};
} // namespace factory_reset
} // namespace esphome
#endif // !defined(USE_RP2040) && !defined(USE_HOST)

View File

@ -2,7 +2,13 @@ from esphome import pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import fastled_base from esphome.components import fastled_base
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_CHIPSET, CONF_NUM_LEDS, CONF_PIN, CONF_RGB_ORDER from esphome.const import (
CONF_CHIPSET,
CONF_NUM_LEDS,
CONF_PIN,
CONF_RGB_ORDER,
Framework,
)
AUTO_LOAD = ["fastled_base"] AUTO_LOAD = ["fastled_base"]
@ -48,13 +54,22 @@ CONFIG_SCHEMA = cv.All(
cv.Required(CONF_PIN): pins.internal_gpio_output_pin_number, cv.Required(CONF_PIN): pins.internal_gpio_output_pin_number,
} }
), ),
_validate, cv.only_with_framework(
frameworks=Framework.ARDUINO,
suggestions={
Framework.ESP_IDF: (
"esp32_rmt_led_strip",
"light/esp32_rmt_led_strip",
)
},
),
cv.require_framework_version( cv.require_framework_version(
esp8266_arduino=cv.Version(2, 7, 4), esp8266_arduino=cv.Version(2, 7, 4),
esp32_arduino=cv.Version(99, 0, 0), esp32_arduino=cv.Version(99, 0, 0),
max_version=True, max_version=True,
extra_message="Please see note on documentation for FastLED", extra_message="Please see note on documentation for FastLED",
), ),
_validate,
) )

View File

@ -9,6 +9,7 @@ from esphome.const import (
CONF_DATA_RATE, CONF_DATA_RATE,
CONF_NUM_LEDS, CONF_NUM_LEDS,
CONF_RGB_ORDER, CONF_RGB_ORDER,
Framework,
) )
AUTO_LOAD = ["fastled_base"] AUTO_LOAD = ["fastled_base"]
@ -33,6 +34,15 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_DATA_RATE): cv.frequency, cv.Optional(CONF_DATA_RATE): cv.frequency,
} }
), ),
cv.only_with_framework(
frameworks=Framework.ARDUINO,
suggestions={
Framework.ESP_IDF: (
"spi_led_strip",
"light/spi_led_strip",
)
},
),
cv.require_framework_version( cv.require_framework_version(
esp8266_arduino=cv.Version(2, 7, 4), esp8266_arduino=cv.Version(2, 7, 4),
esp32_arduino=cv.Version(99, 0, 0), esp32_arduino=cv.Version(99, 0, 0),
@ -45,9 +55,7 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config): async def to_code(config):
var = await fastled_base.new_fastled_light(config) var = await fastled_base.new_fastled_light(config)
rgb_order = cg.RawExpression( rgb_order = cg.RawExpression(config.get(CONF_RGB_ORDER, "RGB"))
config[CONF_RGB_ORDER] if CONF_RGB_ORDER in config else "RGB"
)
data_rate = None data_rate = None
if CONF_DATA_RATE in config: if CONF_DATA_RATE in config:

View File

@ -4,7 +4,13 @@ from esphome import pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import binary_sensor from esphome.components import binary_sensor
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_NAME, CONF_NUMBER, CONF_PIN from esphome.const import (
CONF_ALLOW_OTHER_USES,
CONF_ID,
CONF_NAME,
CONF_NUMBER,
CONF_PIN,
)
from esphome.core import CORE from esphome.core import CORE
from .. import gpio_ns from .. import gpio_ns
@ -76,6 +82,18 @@ async def to_code(config):
) )
use_interrupt = False use_interrupt = False
# Check if pin is shared with other components (allow_other_uses)
# When a pin is shared, interrupts can interfere with other components
# (e.g., duty_cycle sensor) that need to monitor the pin's state changes
if use_interrupt and config[CONF_PIN].get(CONF_ALLOW_OTHER_USES, False):
_LOGGER.info(
"GPIO binary_sensor '%s': Disabling interrupts because pin %s is shared with other components. "
"The sensor will use polling mode for compatibility with other pin uses.",
config.get(CONF_NAME, config[CONF_ID]),
config[CONF_PIN][CONF_NUMBER],
)
use_interrupt = False
cg.add(var.set_use_interrupt(use_interrupt)) cg.add(var.set_use_interrupt(use_interrupt))
if use_interrupt: if use_interrupt:
cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE])) cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE]))

View File

@ -84,7 +84,6 @@ CONFIG_SCHEMA = cv.All(
) )
.extend(cv.polling_component_schema("20s")) .extend(cv.polling_component_schema("20s"))
.extend(uart.UART_DEVICE_SCHEMA), .extend(uart.UART_DEVICE_SCHEMA),
cv.only_with_arduino,
) )
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema("gps", require_rx=True) FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema("gps", require_rx=True)
@ -123,4 +122,9 @@ async def to_code(config):
cg.add(var.set_hdop_sensor(sens)) cg.add(var.set_hdop_sensor(sens))
# https://platformio.org/lib/show/1655/TinyGPSPlus # https://platformio.org/lib/show/1655/TinyGPSPlus
cg.add_library("mikalhart/TinyGPSPlus", "1.1.0") # Using fork of TinyGPSPlus patched to build on ESP-IDF
cg.add_library(
"TinyGPSPlus",
None,
"https://github.com/esphome/TinyGPSPlus.git#v1.1.0",
)

View File

@ -1,5 +1,3 @@
#ifdef USE_ARDUINO
#include "gps.h" #include "gps.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
@ -22,73 +20,76 @@ void GPS::dump_config() {
} }
void GPS::update() { void GPS::update() {
if (this->latitude_sensor_ != nullptr) if (this->latitude_sensor_ != nullptr) {
this->latitude_sensor_->publish_state(this->latitude_); this->latitude_sensor_->publish_state(this->latitude_);
}
if (this->longitude_sensor_ != nullptr) if (this->longitude_sensor_ != nullptr) {
this->longitude_sensor_->publish_state(this->longitude_); this->longitude_sensor_->publish_state(this->longitude_);
}
if (this->speed_sensor_ != nullptr) if (this->speed_sensor_ != nullptr) {
this->speed_sensor_->publish_state(this->speed_); this->speed_sensor_->publish_state(this->speed_);
}
if (this->course_sensor_ != nullptr) if (this->course_sensor_ != nullptr) {
this->course_sensor_->publish_state(this->course_); this->course_sensor_->publish_state(this->course_);
}
if (this->altitude_sensor_ != nullptr) if (this->altitude_sensor_ != nullptr) {
this->altitude_sensor_->publish_state(this->altitude_); this->altitude_sensor_->publish_state(this->altitude_);
}
if (this->satellites_sensor_ != nullptr) if (this->satellites_sensor_ != nullptr) {
this->satellites_sensor_->publish_state(this->satellites_); this->satellites_sensor_->publish_state(this->satellites_);
}
if (this->hdop_sensor_ != nullptr) if (this->hdop_sensor_ != nullptr) {
this->hdop_sensor_->publish_state(this->hdop_); this->hdop_sensor_->publish_state(this->hdop_);
}
} }
void GPS::loop() { void GPS::loop() {
while (this->available() > 0 && !this->has_time_) { while (this->available() > 0 && !this->has_time_) {
if (this->tiny_gps_.encode(this->read())) { if (!this->tiny_gps_.encode(this->read())) {
if (this->tiny_gps_.location.isUpdated()) { return;
this->latitude_ = this->tiny_gps_.location.lat(); }
this->longitude_ = this->tiny_gps_.location.lng(); if (this->tiny_gps_.location.isUpdated()) {
this->latitude_ = this->tiny_gps_.location.lat();
this->longitude_ = this->tiny_gps_.location.lng();
ESP_LOGV(TAG, "Latitude, Longitude: %.6f°, %.6f°", this->latitude_, this->longitude_);
}
ESP_LOGD(TAG, "Location:"); if (this->tiny_gps_.speed.isUpdated()) {
ESP_LOGD(TAG, " Lat: %.6f °", this->latitude_); this->speed_ = this->tiny_gps_.speed.kmph();
ESP_LOGD(TAG, " Lon: %.6f °", this->longitude_); ESP_LOGV(TAG, "Speed: %.3f km/h", this->speed_);
} }
if (this->tiny_gps_.speed.isUpdated()) { if (this->tiny_gps_.course.isUpdated()) {
this->speed_ = this->tiny_gps_.speed.kmph(); this->course_ = this->tiny_gps_.course.deg();
ESP_LOGD(TAG, "Speed: %.3f km/h", this->speed_); ESP_LOGV(TAG, "Course: %.2f°", this->course_);
} }
if (this->tiny_gps_.course.isUpdated()) { if (this->tiny_gps_.altitude.isUpdated()) {
this->course_ = this->tiny_gps_.course.deg(); this->altitude_ = this->tiny_gps_.altitude.meters();
ESP_LOGD(TAG, "Course: %.2f °", this->course_); ESP_LOGV(TAG, "Altitude: %.2f m", this->altitude_);
} }
if (this->tiny_gps_.altitude.isUpdated()) { if (this->tiny_gps_.satellites.isUpdated()) {
this->altitude_ = this->tiny_gps_.altitude.meters(); this->satellites_ = this->tiny_gps_.satellites.value();
ESP_LOGD(TAG, "Altitude: %.2f m", this->altitude_); ESP_LOGV(TAG, "Satellites: %d", this->satellites_);
} }
if (this->tiny_gps_.satellites.isUpdated()) { if (this->tiny_gps_.hdop.isUpdated()) {
this->satellites_ = this->tiny_gps_.satellites.value(); this->hdop_ = this->tiny_gps_.hdop.hdop();
ESP_LOGD(TAG, "Satellites: %d", this->satellites_); ESP_LOGV(TAG, "HDOP: %.3f", this->hdop_);
} }
if (this->tiny_gps_.hdop.isUpdated()) { for (auto *listener : this->listeners_) {
this->hdop_ = this->tiny_gps_.hdop.hdop(); listener->on_update(this->tiny_gps_);
ESP_LOGD(TAG, "HDOP: %.3f", this->hdop_);
}
for (auto *listener : this->listeners_) {
listener->on_update(this->tiny_gps_);
}
} }
} }
} }
} // namespace gps } // namespace gps
} // namespace esphome } // namespace esphome
#endif // USE_ARDUINO

View File

@ -1,10 +1,8 @@
#pragma once #pragma once
#ifdef USE_ARDUINO
#include "esphome/core/component.h"
#include "esphome/components/uart/uart.h"
#include "esphome/components/sensor/sensor.h" #include "esphome/components/sensor/sensor.h"
#include "esphome/components/uart/uart.h"
#include "esphome/core/component.h"
#include <TinyGPSPlus.h> #include <TinyGPSPlus.h>
#include <vector> #include <vector>
@ -53,8 +51,9 @@ class GPS : public PollingComponent, public uart::UARTDevice {
float speed_{NAN}; float speed_{NAN};
float course_{NAN}; float course_{NAN};
float altitude_{NAN}; float altitude_{NAN};
uint16_t satellites_{0};
float hdop_{NAN}; float hdop_{NAN};
uint16_t satellites_{0};
bool has_time_{false};
sensor::Sensor *latitude_sensor_{nullptr}; sensor::Sensor *latitude_sensor_{nullptr};
sensor::Sensor *longitude_sensor_{nullptr}; sensor::Sensor *longitude_sensor_{nullptr};
@ -64,12 +63,9 @@ class GPS : public PollingComponent, public uart::UARTDevice {
sensor::Sensor *satellites_sensor_{nullptr}; sensor::Sensor *satellites_sensor_{nullptr};
sensor::Sensor *hdop_sensor_{nullptr}; sensor::Sensor *hdop_sensor_{nullptr};
bool has_time_{false};
TinyGPSPlus tiny_gps_; TinyGPSPlus tiny_gps_;
std::vector<GPSListener *> listeners_{}; std::vector<GPSListener *> listeners_{};
}; };
} // namespace gps } // namespace gps
} // namespace esphome } // namespace esphome
#endif // USE_ARDUINO

View File

@ -1,5 +1,3 @@
#ifdef USE_ARDUINO
#include "gps_time.h" #include "gps_time.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
@ -9,12 +7,10 @@ namespace gps {
static const char *const TAG = "gps.time"; static const char *const TAG = "gps.time";
void GPSTime::from_tiny_gps_(TinyGPSPlus &tiny_gps) { void GPSTime::from_tiny_gps_(TinyGPSPlus &tiny_gps) {
if (!tiny_gps.time.isValid() || !tiny_gps.date.isValid()) if (!tiny_gps.time.isValid() || !tiny_gps.date.isValid() || !tiny_gps.time.isUpdated() ||
return; !tiny_gps.date.isUpdated() || tiny_gps.date.year() < 2025) {
if (!tiny_gps.time.isUpdated() || !tiny_gps.date.isUpdated())
return;
if (tiny_gps.date.year() < 2019)
return; return;
}
ESPTime val{}; ESPTime val{};
val.year = tiny_gps.date.year(); val.year = tiny_gps.date.year();
@ -34,5 +30,3 @@ void GPSTime::from_tiny_gps_(TinyGPSPlus &tiny_gps) {
} // namespace gps } // namespace gps
} // namespace esphome } // namespace esphome
#endif // USE_ARDUINO

View File

@ -1,10 +1,8 @@
#pragma once #pragma once
#ifdef USE_ARDUINO
#include "esphome/core/component.h"
#include "esphome/components/time/real_time_clock.h"
#include "esphome/components/gps/gps.h" #include "esphome/components/gps/gps.h"
#include "esphome/components/time/real_time_clock.h"
#include "esphome/core/component.h"
namespace esphome { namespace esphome {
namespace gps { namespace gps {
@ -13,8 +11,9 @@ class GPSTime : public time::RealTimeClock, public GPSListener {
public: public:
void update() override { this->from_tiny_gps_(this->get_tiny_gps()); }; void update() override { this->from_tiny_gps_(this->get_tiny_gps()); };
void on_update(TinyGPSPlus &tiny_gps) override { void on_update(TinyGPSPlus &tiny_gps) override {
if (!this->has_time_) if (!this->has_time_) {
this->from_tiny_gps_(tiny_gps); this->from_tiny_gps_(tiny_gps);
}
} }
protected: protected:
@ -24,5 +23,3 @@ class GPSTime : public time::RealTimeClock, public GPSListener {
} // namespace gps } // namespace gps
} // namespace esphome } // namespace esphome
#endif // USE_ARDUINO

View File

@ -116,7 +116,7 @@ GRAPH_SCHEMA = cv.Schema(
def _relocate_fields_to_subfolder(config, subfolder, subschema): def _relocate_fields_to_subfolder(config, subfolder, subschema):
fields = [k.schema for k in subschema.schema.keys()] fields = [k.schema for k in subschema.schema]
fields.remove(CONF_ID) fields.remove(CONF_ID)
if subfolder in config: if subfolder in config:
# Ensure no ambiguous fields in base of config # Ensure no ambiguous fields in base of config

View File

@ -8,6 +8,8 @@ namespace gt911 {
static const char *const TAG = "gt911.touchscreen"; static const char *const TAG = "gt911.touchscreen";
static const uint8_t PRIMARY_ADDRESS = 0x5D; // default I2C address for GT911
static const uint8_t SECONDARY_ADDRESS = 0x14; // secondary I2C address for GT911
static const uint8_t GET_TOUCH_STATE[2] = {0x81, 0x4E}; static const uint8_t GET_TOUCH_STATE[2] = {0x81, 0x4E};
static const uint8_t CLEAR_TOUCH_STATE[3] = {0x81, 0x4E, 0x00}; static const uint8_t CLEAR_TOUCH_STATE[3] = {0x81, 0x4E, 0x00};
static const uint8_t GET_TOUCHES[2] = {0x81, 0x4F}; static const uint8_t GET_TOUCHES[2] = {0x81, 0x4F};
@ -18,8 +20,7 @@ static const size_t MAX_BUTTONS = 4; // max number of buttons scanned
#define ERROR_CHECK(err) \ #define ERROR_CHECK(err) \
if ((err) != i2c::ERROR_OK) { \ if ((err) != i2c::ERROR_OK) { \
ESP_LOGE(TAG, "Failed to communicate!"); \ this->status_set_warning("Communication failure"); \
this->status_set_warning(); \
return; \ return; \
} }
@ -30,31 +31,31 @@ void GT911Touchscreen::setup() {
this->reset_pin_->setup(); this->reset_pin_->setup();
this->reset_pin_->digital_write(false); this->reset_pin_->digital_write(false);
if (this->interrupt_pin_ != nullptr) { if (this->interrupt_pin_ != nullptr) {
// The interrupt pin is used as an input during reset to select the I2C address. // temporarily set the interrupt pin to output to control address selection
this->interrupt_pin_->pin_mode(gpio::FLAG_OUTPUT); this->interrupt_pin_->pin_mode(gpio::FLAG_OUTPUT);
this->interrupt_pin_->setup();
this->interrupt_pin_->digital_write(false); this->interrupt_pin_->digital_write(false);
} }
delay(2); delay(2);
this->reset_pin_->digital_write(true); this->reset_pin_->digital_write(true);
delay(50); // NOLINT delay(50); // NOLINT
if (this->interrupt_pin_ != nullptr) { }
this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT); if (this->interrupt_pin_ != nullptr) {
this->interrupt_pin_->setup(); // set pre-configured input mode
} this->interrupt_pin_->setup();
} }
// check the configuration of the int line. // check the configuration of the int line.
uint8_t data[4]; uint8_t data[4];
err = this->write(GET_SWITCHES, 2); err = this->write(GET_SWITCHES, sizeof(GET_SWITCHES));
if (err != i2c::ERROR_OK && this->address_ == PRIMARY_ADDRESS) {
this->address_ = SECONDARY_ADDRESS;
err = this->write(GET_SWITCHES, sizeof(GET_SWITCHES));
}
if (err == i2c::ERROR_OK) { if (err == i2c::ERROR_OK) {
err = this->read(data, 1); err = this->read(data, 1);
if (err == i2c::ERROR_OK) { if (err == i2c::ERROR_OK) {
ESP_LOGD(TAG, "Read from switches: 0x%02X", data[0]); ESP_LOGD(TAG, "Read from switches at address 0x%02X: 0x%02X", this->address_, data[0]);
if (this->interrupt_pin_ != nullptr) { if (this->interrupt_pin_ != nullptr) {
// datasheet says NOT to use pullup/down on the int line.
this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT);
this->interrupt_pin_->setup();
this->attach_interrupt_(this->interrupt_pin_, this->attach_interrupt_(this->interrupt_pin_,
(data[0] & 1) ? gpio::INTERRUPT_FALLING_EDGE : gpio::INTERRUPT_RISING_EDGE); (data[0] & 1) ? gpio::INTERRUPT_FALLING_EDGE : gpio::INTERRUPT_RISING_EDGE);
} }
@ -63,7 +64,7 @@ void GT911Touchscreen::setup() {
if (this->x_raw_max_ == 0 || this->y_raw_max_ == 0) { if (this->x_raw_max_ == 0 || this->y_raw_max_ == 0) {
// no calibration? Attempt to read the max values from the touchscreen. // no calibration? Attempt to read the max values from the touchscreen.
if (err == i2c::ERROR_OK) { if (err == i2c::ERROR_OK) {
err = this->write(GET_MAX_VALUES, 2); err = this->write(GET_MAX_VALUES, sizeof(GET_MAX_VALUES));
if (err == i2c::ERROR_OK) { if (err == i2c::ERROR_OK) {
err = this->read(data, sizeof(data)); err = this->read(data, sizeof(data));
if (err == i2c::ERROR_OK) { if (err == i2c::ERROR_OK) {
@ -75,15 +76,12 @@ void GT911Touchscreen::setup() {
} }
} }
if (err != i2c::ERROR_OK) { if (err != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Failed to read calibration values from touchscreen!"); this->mark_failed("Failed to read calibration");
this->mark_failed();
return; return;
} }
} }
if (err != i2c::ERROR_OK) { if (err != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Failed to communicate!"); this->mark_failed("Failed to communicate");
this->mark_failed();
return;
} }
ESP_LOGCONFIG(TAG, "GT911 Touchscreen setup complete"); ESP_LOGCONFIG(TAG, "GT911 Touchscreen setup complete");
@ -94,7 +92,7 @@ void GT911Touchscreen::update_touches() {
uint8_t touch_state = 0; uint8_t touch_state = 0;
uint8_t data[MAX_TOUCHES + 1][8]; // 8 bytes each for each point, plus extra space for the key byte uint8_t data[MAX_TOUCHES + 1][8]; // 8 bytes each for each point, plus extra space for the key byte
err = this->write(GET_TOUCH_STATE, sizeof(GET_TOUCH_STATE), false); err = this->write(GET_TOUCH_STATE, sizeof(GET_TOUCH_STATE));
ERROR_CHECK(err); ERROR_CHECK(err);
err = this->read(&touch_state, 1); err = this->read(&touch_state, 1);
ERROR_CHECK(err); ERROR_CHECK(err);
@ -106,7 +104,7 @@ void GT911Touchscreen::update_touches() {
return; return;
} }
err = this->write(GET_TOUCHES, sizeof(GET_TOUCHES), false); err = this->write(GET_TOUCHES, sizeof(GET_TOUCHES));
ERROR_CHECK(err); ERROR_CHECK(err);
// num_of_touches is guaranteed to be 0..5. Also read the key data // num_of_touches is guaranteed to be 0..5. Also read the key data
err = this->read(data[0], sizeof(data[0]) * num_of_touches + 1); err = this->read(data[0], sizeof(data[0]) * num_of_touches + 1);
@ -132,6 +130,7 @@ void GT911Touchscreen::dump_config() {
ESP_LOGCONFIG(TAG, "GT911 Touchscreen:"); ESP_LOGCONFIG(TAG, "GT911 Touchscreen:");
LOG_I2C_DEVICE(this); LOG_I2C_DEVICE(this);
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
LOG_PIN(" Reset Pin: ", this->reset_pin_);
} }
} // namespace gt911 } // namespace gt911

View File

@ -83,18 +83,24 @@ void HomeassistantNumber::control(float value) {
this->publish_state(value); this->publish_state(value);
static constexpr auto SERVICE_NAME = StringRef::from_lit("number.set_value");
static constexpr auto ENTITY_ID_KEY = StringRef::from_lit("entity_id");
static constexpr auto VALUE_KEY = StringRef::from_lit("value");
api::HomeassistantServiceResponse resp; api::HomeassistantServiceResponse resp;
resp.service = "number.set_value"; resp.set_service(SERVICE_NAME);
api::HomeassistantServiceMap entity_id; resp.data.emplace_back();
entity_id.key = "entity_id"; auto &entity_id = resp.data.back();
entity_id.value = this->entity_id_; entity_id.set_key(ENTITY_ID_KEY);
resp.data.push_back(entity_id); entity_id.set_value(StringRef(this->entity_id_));
api::HomeassistantServiceMap entity_value; resp.data.emplace_back();
entity_value.key = "value"; auto &entity_value = resp.data.back();
entity_value.value = to_string(value); entity_value.set_key(VALUE_KEY);
resp.data.push_back(entity_value); // to_string() returns a temporary - must store it to avoid dangling reference
std::string value_str = to_string(value);
entity_value.set_value(StringRef(value_str));
api::global_api_server->send_homeassistant_service_call(resp); api::global_api_server->send_homeassistant_service_call(resp);
} }

View File

@ -40,17 +40,21 @@ void HomeassistantSwitch::write_state(bool state) {
return; return;
} }
static constexpr auto SERVICE_ON = StringRef::from_lit("homeassistant.turn_on");
static constexpr auto SERVICE_OFF = StringRef::from_lit("homeassistant.turn_off");
static constexpr auto ENTITY_ID_KEY = StringRef::from_lit("entity_id");
api::HomeassistantServiceResponse resp; api::HomeassistantServiceResponse resp;
if (state) { if (state) {
resp.service = "homeassistant.turn_on"; resp.set_service(SERVICE_ON);
} else { } else {
resp.service = "homeassistant.turn_off"; resp.set_service(SERVICE_OFF);
} }
api::HomeassistantServiceMap entity_id_kv; resp.data.emplace_back();
entity_id_kv.key = "entity_id"; auto &entity_id_kv = resp.data.back();
entity_id_kv.value = this->entity_id_; entity_id_kv.set_key(ENTITY_ID_KEY);
resp.data.push_back(entity_id_kv); entity_id_kv.set_value(StringRef(this->entity_id_));
api::global_api_server->send_homeassistant_service_call(resp); api::global_api_server->send_homeassistant_service_call(resp);
} }

View File

@ -7,6 +7,7 @@ from esphome.const import (
KEY_TARGET_FRAMEWORK, KEY_TARGET_FRAMEWORK,
KEY_TARGET_PLATFORM, KEY_TARGET_PLATFORM,
PLATFORM_HOST, PLATFORM_HOST,
ThreadModel,
) )
from esphome.core import CORE from esphome.core import CORE
@ -43,6 +44,7 @@ async def to_code(config):
cg.add_define("USE_ESPHOME_HOST_MAC_ADDRESS", config[CONF_MAC_ADDRESS].parts) cg.add_define("USE_ESPHOME_HOST_MAC_ADDRESS", config[CONF_MAC_ADDRESS].parts)
cg.add_build_flag("-std=gnu++20") cg.add_build_flag("-std=gnu++20")
cg.add_define("ESPHOME_BOARD", "host") cg.add_define("ESPHOME_BOARD", "host")
cg.add_define(ThreadModel.MULTI_ATOMICS)
cg.add_platformio_option("platform", "platformio/native") cg.add_platformio_option("platform", "platformio/native")
cg.add_platformio_option("lib_ldf_mode", "off") cg.add_platformio_option("lib_ldf_mode", "off")
cg.add_platformio_option("lib_compat_mode", "strict") cg.add_platformio_option("lib_compat_mode", "strict")

View File

@ -70,9 +70,8 @@ def validate_url(value):
def validate_ssl_verification(config): def validate_ssl_verification(config):
error_message = "" error_message = ""
if CORE.is_esp32: if CORE.is_esp32 and not CORE.using_esp_idf and config[CONF_VERIFY_SSL]:
if not CORE.using_esp_idf and config[CONF_VERIFY_SSL]: error_message = "ESPHome supports certificate verification only via ESP-IDF"
error_message = "ESPHome supports certificate verification only via ESP-IDF"
if CORE.is_rp2040 and config[CONF_VERIFY_SSL]: if CORE.is_rp2040 and config[CONF_VERIFY_SSL]:
error_message = "ESPHome does not support certificate verification on RP2040" error_message = "ESPHome does not support certificate verification on RP2040"

View File

@ -157,8 +157,8 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(std::string url, std::str
container->status_code = esp_http_client_get_status_code(client); container->status_code = esp_http_client_get_status_code(client);
container->feed_wdt(); container->feed_wdt();
container->set_response_headers(user_data.response_headers); container->set_response_headers(user_data.response_headers);
container->duration_ms = millis() - start;
if (is_success(container->status_code)) { if (is_success(container->status_code)) {
container->duration_ms = millis() - start;
return container; return container;
} }
@ -191,8 +191,8 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(std::string url, std::str
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);
container->feed_wdt(); container->feed_wdt();
container->duration_ms = millis() - start;
if (is_success(container->status_code)) { if (is_success(container->status_code)) {
container->duration_ms = millis() - start;
return container; return container;
} }

View File

@ -66,11 +66,10 @@ PROTOCOL_NAMES = {
def _validate(config): def _validate(config):
for conf, models in SUPPORTED_OPTIONS.items(): for conf, models in SUPPORTED_OPTIONS.items():
if conf in config: if conf in config and config[CONF_MODEL] not in models:
if config[CONF_MODEL] not in models: raise cv.Invalid(
raise cv.Invalid( f"{conf} is only available on {' and '.join(models)}, not {config[CONF_MODEL]}"
f"{conf} is only available on {' and '.join(models)}, not {config[CONF_MODEL]}" )
)
return config return config

View File

@ -94,7 +94,7 @@ class I2CBus {
protected: protected:
/// @brief Scans the I2C bus for devices. Devices presence is kept in an array of std::pair /// @brief Scans the I2C bus for devices. Devices presence is kept in an array of std::pair
/// that contains the address and the corresponding bool presence flag. /// that contains the address and the corresponding bool presence flag.
void i2c_scan_() { virtual void i2c_scan() {
for (uint8_t address = 8; address < 120; address++) { for (uint8_t address = 8; address < 120; address++) {
auto err = writev(address, nullptr, 0); auto err = writev(address, nullptr, 0);
if (err == ERROR_OK) { if (err == ERROR_OK) {

View File

@ -42,7 +42,7 @@ void ArduinoI2CBus::setup() {
this->initialized_ = true; this->initialized_ = true;
if (this->scan_) { if (this->scan_) {
ESP_LOGV(TAG, "Scanning bus for active devices"); ESP_LOGV(TAG, "Scanning bus for active devices");
this->i2c_scan_(); this->i2c_scan();
} }
} }

View File

@ -1,13 +1,13 @@
#ifdef USE_ESP_IDF #ifdef USE_ESP_IDF
#include "i2c_bus_esp_idf.h" #include "i2c_bus_esp_idf.h"
#include <driver/gpio.h>
#include <cinttypes> #include <cinttypes>
#include <cstring> #include <cstring>
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include <driver/gpio.h>
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 3, 0) #if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 3, 0)
#define SOC_HP_I2C_NUM SOC_I2C_NUM #define SOC_HP_I2C_NUM SOC_I2C_NUM
@ -78,7 +78,7 @@ void IDFI2CBus::setup() {
if (this->scan_) { if (this->scan_) {
ESP_LOGV(TAG, "Scanning for devices"); ESP_LOGV(TAG, "Scanning for devices");
this->i2c_scan_(); this->i2c_scan();
} }
#else #else
#if SOC_HP_I2C_NUM > 1 #if SOC_HP_I2C_NUM > 1
@ -125,7 +125,7 @@ void IDFI2CBus::setup() {
initialized_ = true; initialized_ = true;
if (this->scan_) { if (this->scan_) {
ESP_LOGV(TAG, "Scanning bus for active devices"); ESP_LOGV(TAG, "Scanning bus for active devices");
this->i2c_scan_(); this->i2c_scan();
} }
#endif #endif
} }
@ -167,6 +167,17 @@ void IDFI2CBus::dump_config() {
} }
} }
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2)
void IDFI2CBus::i2c_scan() {
for (uint8_t address = 8; address < 120; address++) {
auto err = i2c_master_probe(this->bus_, address, 20);
if (err == ESP_OK) {
this->scan_results_.emplace_back(address, true);
}
}
}
#endif
ErrorCode IDFI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt) { ErrorCode IDFI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt) {
// logging is only enabled with vv level, if warnings are shown the caller // logging is only enabled with vv level, if warnings are shown the caller
// should log them // should log them

View File

@ -2,9 +2,9 @@
#ifdef USE_ESP_IDF #ifdef USE_ESP_IDF
#include "esp_idf_version.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "i2c_bus.h" #include "i2c_bus.h"
#include "esp_idf_version.h"
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2)
#include <driver/i2c_master.h> #include <driver/i2c_master.h>
#else #else
@ -46,6 +46,7 @@ class IDFI2CBus : public InternalI2CBus, public Component {
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2)
i2c_master_dev_handle_t dev_; i2c_master_dev_handle_t dev_;
i2c_master_bus_handle_t bus_; i2c_master_bus_handle_t bus_;
void i2c_scan() override;
#endif #endif
i2c_port_t port_; i2c_port_t port_;
uint8_t sda_pin_; uint8_t sda_pin_;

View File

@ -1,6 +1,6 @@
from esphome import pins from esphome import pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components.esp32 import get_esp32_variant from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant
from esphome.components.esp32.const import ( from esphome.components.esp32.const import (
VARIANT_ESP32, VARIANT_ESP32,
VARIANT_ESP32C3, VARIANT_ESP32C3,
@ -243,10 +243,7 @@ def _final_validate(_):
def use_legacy(): def use_legacy():
if CORE.using_esp_idf: return not (CORE.using_esp_idf and not _use_legacy_driver)
if not _use_legacy_driver:
return False
return True
FINAL_VALIDATE_SCHEMA = _final_validate FINAL_VALIDATE_SCHEMA = _final_validate
@ -258,6 +255,10 @@ async def to_code(config):
if use_legacy(): if use_legacy():
cg.add_define("USE_I2S_LEGACY") cg.add_define("USE_I2S_LEGACY")
# Helps avoid callbacks being skipped due to processor load
if CORE.using_esp_idf:
add_idf_sdkconfig_option("CONFIG_I2S_ISR_IRAM_SAFE", True)
cg.add(var.set_lrclk_pin(config[CONF_I2S_LRCLK_PIN])) cg.add(var.set_lrclk_pin(config[CONF_I2S_LRCLK_PIN]))
if CONF_I2S_BCLK_PIN in config: if CONF_I2S_BCLK_PIN in config:
cg.add(var.set_bclk_pin(config[CONF_I2S_BCLK_PIN])) cg.add(var.set_bclk_pin(config[CONF_I2S_BCLK_PIN]))

View File

@ -44,9 +44,8 @@ PDM_VARIANTS = [esp32.const.VARIANT_ESP32, esp32.const.VARIANT_ESP32S3]
def _validate_esp32_variant(config): def _validate_esp32_variant(config):
variant = esp32.get_esp32_variant() variant = esp32.get_esp32_variant()
if config[CONF_ADC_TYPE] == "external": if config[CONF_ADC_TYPE] == "external":
if config[CONF_PDM]: if config[CONF_PDM] and variant not in PDM_VARIANTS:
if variant not in PDM_VARIANTS: raise cv.Invalid(f"{variant} does not support PDM")
raise cv.Invalid(f"{variant} does not support PDM")
return config return config
if config[CONF_ADC_TYPE] == "internal": if config[CONF_ADC_TYPE] == "internal":
if variant not in INTERNAL_ADC_VARIANTS: if variant not in INTERNAL_ADC_VARIANTS:
@ -122,9 +121,8 @@ CONFIG_SCHEMA = cv.All(
def _final_validate(config): def _final_validate(config):
if not use_legacy(): if not use_legacy() and config[CONF_ADC_TYPE] == "internal":
if config[CONF_ADC_TYPE] == "internal": raise cv.Invalid("Internal ADC is only compatible with legacy i2s driver.")
raise cv.Invalid("Internal ADC is only compatible with legacy i2s driver.")
FINAL_VALIDATE_SCHEMA = _final_validate FINAL_VALIDATE_SCHEMA = _final_validate

View File

@ -9,6 +9,7 @@
#endif #endif
#include "esphome/components/audio/audio.h" #include "esphome/components/audio/audio.h"
#include "esphome/components/audio/audio_transfer_buffer.h"
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
@ -19,72 +20,33 @@
namespace esphome { namespace esphome {
namespace i2s_audio { namespace i2s_audio {
static const uint8_t DMA_BUFFER_DURATION_MS = 15; static const uint32_t DMA_BUFFER_DURATION_MS = 15;
static const size_t DMA_BUFFERS_COUNT = 4; static const size_t DMA_BUFFERS_COUNT = 4;
static const size_t TASK_DELAY_MS = DMA_BUFFER_DURATION_MS * DMA_BUFFERS_COUNT / 2;
static const size_t TASK_STACK_SIZE = 4096; static const size_t TASK_STACK_SIZE = 4096;
static const ssize_t TASK_PRIORITY = 23; static const ssize_t TASK_PRIORITY = 19;
static const size_t I2S_EVENT_QUEUE_COUNT = DMA_BUFFERS_COUNT + 1; static const size_t I2S_EVENT_QUEUE_COUNT = DMA_BUFFERS_COUNT + 1;
static const char *const TAG = "i2s_audio.speaker"; static const char *const TAG = "i2s_audio.speaker";
enum SpeakerEventGroupBits : uint32_t { enum SpeakerEventGroupBits : uint32_t {
COMMAND_START = (1 << 0), // starts the speaker task COMMAND_START = (1 << 0), // indicates loop should start speaker task
COMMAND_STOP = (1 << 1), // stops the speaker task COMMAND_STOP = (1 << 1), // stops the speaker task
COMMAND_STOP_GRACEFULLY = (1 << 2), // Stops the speaker task once all data has been written COMMAND_STOP_GRACEFULLY = (1 << 2), // Stops the speaker task once all data has been written
STATE_STARTING = (1 << 10),
STATE_RUNNING = (1 << 11), TASK_STARTING = (1 << 10),
STATE_STOPPING = (1 << 12), TASK_RUNNING = (1 << 11),
STATE_STOPPED = (1 << 13), TASK_STOPPING = (1 << 12),
ERR_TASK_FAILED_TO_START = (1 << 14), TASK_STOPPED = (1 << 13),
ERR_ESP_INVALID_STATE = (1 << 15),
ERR_ESP_NOT_SUPPORTED = (1 << 16),
ERR_ESP_INVALID_ARG = (1 << 17),
ERR_ESP_INVALID_SIZE = (1 << 18),
ERR_ESP_NO_MEM = (1 << 19), ERR_ESP_NO_MEM = (1 << 19),
ERR_ESP_FAIL = (1 << 20),
ALL_ERR_ESP_BITS = ERR_ESP_INVALID_STATE | ERR_ESP_NOT_SUPPORTED | ERR_ESP_INVALID_ARG | ERR_ESP_INVALID_SIZE | WARN_DROPPED_EVENT = (1 << 20),
ERR_ESP_NO_MEM | ERR_ESP_FAIL,
ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits
}; };
// Translates a SpeakerEventGroupBits ERR_ESP bit to the coressponding esp_err_t
static esp_err_t err_bit_to_esp_err(uint32_t bit) {
switch (bit) {
case SpeakerEventGroupBits::ERR_ESP_INVALID_STATE:
return ESP_ERR_INVALID_STATE;
case SpeakerEventGroupBits::ERR_ESP_INVALID_ARG:
return ESP_ERR_INVALID_ARG;
case SpeakerEventGroupBits::ERR_ESP_INVALID_SIZE:
return ESP_ERR_INVALID_SIZE;
case SpeakerEventGroupBits::ERR_ESP_NO_MEM:
return ESP_ERR_NO_MEM;
case SpeakerEventGroupBits::ERR_ESP_NOT_SUPPORTED:
return ESP_ERR_NOT_SUPPORTED;
default:
return ESP_FAIL;
}
}
/// @brief Multiplies the input array of Q15 numbers by a Q15 constant factor
///
/// Based on `dsps_mulc_s16_ansi` from the esp-dsp library:
/// https://github.com/espressif/esp-dsp/blob/master/modules/math/mulc/fixed/dsps_mulc_s16_ansi.c
/// (accessed on 2024-09-30).
/// @param input Array of Q15 numbers
/// @param output Array of Q15 numbers
/// @param len Length of array
/// @param c Q15 constant factor
static void q15_multiplication(const int16_t *input, int16_t *output, size_t len, int16_t c) {
for (int i = 0; i < len; i++) {
int32_t acc = (int32_t) input[i] * (int32_t) c;
output[i] = (int16_t) (acc >> 15);
}
}
// Lists the Q15 fixed point scaling factor for volume reduction. // Lists the Q15 fixed point scaling factor for volume reduction.
// Has 100 values representing silence and a reduction [49, 48.5, ... 0.5, 0] dB. // Has 100 values representing silence and a reduction [49, 48.5, ... 0.5, 0] dB.
// dB to PCM scaling factor formula: floating_point_scale_factor = 2^(-db/6.014) // dB to PCM scaling factor formula: floating_point_scale_factor = 2^(-db/6.014)
@ -132,51 +94,80 @@ void I2SAudioSpeaker::dump_config() {
void I2SAudioSpeaker::loop() { void I2SAudioSpeaker::loop() {
uint32_t event_group_bits = xEventGroupGetBits(this->event_group_); uint32_t event_group_bits = xEventGroupGetBits(this->event_group_);
if (event_group_bits & SpeakerEventGroupBits::STATE_STARTING) { if ((event_group_bits & SpeakerEventGroupBits::COMMAND_START) && (this->state_ == speaker::STATE_STOPPED)) {
ESP_LOGD(TAG, "Starting");
this->state_ = speaker::STATE_STARTING; this->state_ = speaker::STATE_STARTING;
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::STATE_STARTING); xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::COMMAND_START);
} }
if (event_group_bits & SpeakerEventGroupBits::STATE_RUNNING) {
// Handle the task's state
if (event_group_bits & SpeakerEventGroupBits::TASK_STARTING) {
ESP_LOGD(TAG, "Starting");
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::TASK_STARTING);
}
if (event_group_bits & SpeakerEventGroupBits::TASK_RUNNING) {
ESP_LOGD(TAG, "Started"); ESP_LOGD(TAG, "Started");
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::TASK_RUNNING);
this->state_ = speaker::STATE_RUNNING; this->state_ = speaker::STATE_RUNNING;
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::STATE_RUNNING);
this->status_clear_warning();
this->status_clear_error();
} }
if (event_group_bits & SpeakerEventGroupBits::STATE_STOPPING) { if (event_group_bits & SpeakerEventGroupBits::TASK_STOPPING) {
ESP_LOGD(TAG, "Stopping"); ESP_LOGD(TAG, "Stopping");
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::TASK_STOPPING);
this->state_ = speaker::STATE_STOPPING; this->state_ = speaker::STATE_STOPPING;
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::STATE_STOPPING);
} }
if (event_group_bits & SpeakerEventGroupBits::STATE_STOPPED) { if (event_group_bits & SpeakerEventGroupBits::TASK_STOPPED) {
if (!this->task_created_) { ESP_LOGD(TAG, "Stopped");
ESP_LOGD(TAG, "Stopped");
this->state_ = speaker::STATE_STOPPED; vTaskDelete(this->speaker_task_handle_);
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ALL_BITS); this->speaker_task_handle_ = nullptr;
this->speaker_task_handle_ = nullptr;
} this->stop_i2s_driver_();
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ALL_BITS);
this->status_clear_error();
this->state_ = speaker::STATE_STOPPED;
} }
if (event_group_bits & SpeakerEventGroupBits::ERR_TASK_FAILED_TO_START) { // Log any errors encounted by the task
this->status_set_error("Failed to start task"); if (event_group_bits & SpeakerEventGroupBits::ERR_ESP_NO_MEM) {
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ERR_TASK_FAILED_TO_START); ESP_LOGE(TAG, "Not enough memory");
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_NO_MEM);
} }
if (event_group_bits & SpeakerEventGroupBits::ALL_ERR_ESP_BITS) { // Warn if any playback timestamp events are dropped, which drastically reduces synced playback accuracy
uint32_t error_bits = event_group_bits & SpeakerEventGroupBits::ALL_ERR_ESP_BITS; if (event_group_bits & SpeakerEventGroupBits::WARN_DROPPED_EVENT) {
ESP_LOGW(TAG, "Writing failed: %s", esp_err_to_name(err_bit_to_esp_err(error_bits))); ESP_LOGW(TAG, "Event dropped, synchronized playback accuracy is reduced");
this->status_set_warning(); xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::WARN_DROPPED_EVENT);
} }
if (event_group_bits & SpeakerEventGroupBits::ERR_ESP_NOT_SUPPORTED) { // Handle the speaker's state
this->status_set_error("Failed to adjust bus to match incoming audio"); switch (this->state_) {
ESP_LOGE(TAG, "Incompatible audio format: sample rate = %" PRIu32 ", channels = %u, bits per sample = %u", case speaker::STATE_STARTING:
this->audio_stream_info_.get_sample_rate(), this->audio_stream_info_.get_channels(), if (this->status_has_error()) {
this->audio_stream_info_.get_bits_per_sample()); break;
} }
xEventGroupClearBits(this->event_group_, ALL_ERR_ESP_BITS); if (this->start_i2s_driver_(this->audio_stream_info_) != ESP_OK) {
ESP_LOGE(TAG, "Driver failed to start; retrying in 1 second");
this->status_momentary_error("driver-faiure", 1000);
break;
}
if (this->speaker_task_handle_ == nullptr) {
xTaskCreate(I2SAudioSpeaker::speaker_task, "speaker_task", TASK_STACK_SIZE, (void *) this, TASK_PRIORITY,
&this->speaker_task_handle_);
if (this->speaker_task_handle_ == nullptr) {
ESP_LOGE(TAG, "Task failed to start, retrying in 1 second");
this->status_momentary_error("task-failure", 1000);
this->stop_i2s_driver_(); // Stops the driver to return the lock; will be reloaded in next attempt
}
}
break;
case speaker::STATE_RUNNING: // Intentional fallthrough
case speaker::STATE_STOPPING: // Intentional fallthrough
case speaker::STATE_STOPPED:
break;
}
} }
void I2SAudioSpeaker::set_volume(float volume) { void I2SAudioSpeaker::set_volume(float volume) {
@ -227,83 +218,76 @@ size_t I2SAudioSpeaker::play(const uint8_t *data, size_t length, TickType_t tick
this->start(); this->start();
} }
if ((this->state_ != speaker::STATE_RUNNING) || (this->audio_ring_buffer_.use_count() != 1)) { if (this->state_ != speaker::STATE_RUNNING) {
// Unable to write data to a running speaker, so delay the max amount of time so it can get ready // Unable to write data to a running speaker, so delay the max amount of time so it can get ready
vTaskDelay(ticks_to_wait); vTaskDelay(ticks_to_wait);
ticks_to_wait = 0; ticks_to_wait = 0;
} }
size_t bytes_written = 0; size_t bytes_written = 0;
if ((this->state_ == speaker::STATE_RUNNING) && (this->audio_ring_buffer_.use_count() == 1)) { if (this->state_ == speaker::STATE_RUNNING) {
// Only one owner of the ring buffer (the speaker task), so the ring buffer is allocated and no other components are std::shared_ptr<RingBuffer> temp_ring_buffer = this->audio_ring_buffer_.lock();
// attempting to write to it. if (temp_ring_buffer.use_count() == 2) {
// Only the speaker task and this temp_ring_buffer own the ring buffer, so its safe to write to
// Temporarily share ownership of the ring buffer so it won't be deallocated while writing bytes_written = temp_ring_buffer->write_without_replacement((void *) data, length, ticks_to_wait);
std::shared_ptr<RingBuffer> temp_ring_buffer = this->audio_ring_buffer_; }
bytes_written = temp_ring_buffer->write_without_replacement((void *) data, length, ticks_to_wait);
} }
return bytes_written; return bytes_written;
} }
bool I2SAudioSpeaker::has_buffered_data() const { bool I2SAudioSpeaker::has_buffered_data() const {
if (this->audio_ring_buffer_ != nullptr) { if (this->audio_ring_buffer_.use_count() > 0) {
return this->audio_ring_buffer_->available() > 0; std::shared_ptr<RingBuffer> temp_ring_buffer = this->audio_ring_buffer_.lock();
return temp_ring_buffer->available() > 0;
} }
return false; return false;
} }
void I2SAudioSpeaker::speaker_task(void *params) { void I2SAudioSpeaker::speaker_task(void *params) {
I2SAudioSpeaker *this_speaker = (I2SAudioSpeaker *) params; I2SAudioSpeaker *this_speaker = (I2SAudioSpeaker *) params;
this_speaker->task_created_ = true;
uint32_t event_group_bits = xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::TASK_STARTING);
xEventGroupWaitBits(this_speaker->event_group_,
SpeakerEventGroupBits::COMMAND_START | SpeakerEventGroupBits::COMMAND_STOP |
SpeakerEventGroupBits::COMMAND_STOP_GRACEFULLY, // Bit message to read
pdTRUE, // Clear the bits on exit
pdFALSE, // Don't wait for all the bits,
portMAX_DELAY); // Block indefinitely until a bit is set
if (event_group_bits & (SpeakerEventGroupBits::COMMAND_STOP | SpeakerEventGroupBits::COMMAND_STOP_GRACEFULLY)) {
// Received a stop signal before the task was requested to start
this_speaker->delete_task_(0);
}
xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::STATE_STARTING);
audio::AudioStreamInfo audio_stream_info = this_speaker->audio_stream_info_;
const uint32_t dma_buffers_duration_ms = DMA_BUFFER_DURATION_MS * DMA_BUFFERS_COUNT; const uint32_t dma_buffers_duration_ms = DMA_BUFFER_DURATION_MS * DMA_BUFFERS_COUNT;
// Ensure ring buffer duration is at least the duration of all DMA buffers // Ensure ring buffer duration is at least the duration of all DMA buffers
const uint32_t ring_buffer_duration = std::max(dma_buffers_duration_ms, this_speaker->buffer_duration_ms_); const uint32_t ring_buffer_duration = std::max(dma_buffers_duration_ms, this_speaker->buffer_duration_ms_);
// The DMA buffers may have more bits per sample, so calculate buffer sizes based in the input audio stream info // The DMA buffers may have more bits per sample, so calculate buffer sizes based in the input audio stream info
const size_t data_buffer_size = audio_stream_info.ms_to_bytes(dma_buffers_duration_ms); const size_t ring_buffer_size = this_speaker->current_stream_info_.ms_to_bytes(ring_buffer_duration);
const size_t ring_buffer_size = audio_stream_info.ms_to_bytes(ring_buffer_duration);
const size_t single_dma_buffer_input_size = data_buffer_size / DMA_BUFFERS_COUNT; const uint32_t frames_to_fill_single_dma_buffer =
this_speaker->current_stream_info_.ms_to_frames(DMA_BUFFER_DURATION_MS);
const size_t bytes_to_fill_single_dma_buffer =
this_speaker->current_stream_info_.frames_to_bytes(frames_to_fill_single_dma_buffer);
if (this_speaker->send_esp_err_to_event_group_(this_speaker->allocate_buffers_(data_buffer_size, ring_buffer_size))) { bool successful_setup = false;
// Failed to allocate buffers std::unique_ptr<audio::AudioSourceTransferBuffer> transfer_buffer =
xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::ERR_ESP_NO_MEM); audio::AudioSourceTransferBuffer::create(bytes_to_fill_single_dma_buffer);
this_speaker->delete_task_(data_buffer_size);
if (transfer_buffer != nullptr) {
std::shared_ptr<RingBuffer> temp_ring_buffer = RingBuffer::create(ring_buffer_size);
if (temp_ring_buffer.use_count() == 1) {
transfer_buffer->set_source(temp_ring_buffer);
this_speaker->audio_ring_buffer_ = temp_ring_buffer;
successful_setup = true;
}
} }
if (!this_speaker->send_esp_err_to_event_group_(this_speaker->start_i2s_driver_(audio_stream_info))) { if (!successful_setup) {
xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::STATE_RUNNING); xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::ERR_ESP_NO_MEM);
} else {
bool stop_gracefully = false; bool stop_gracefully = false;
bool tx_dma_underflow = true;
uint32_t frames_written = 0;
uint32_t last_data_received_time = millis(); uint32_t last_data_received_time = millis();
bool tx_dma_underflow = false;
this_speaker->accumulated_frames_written_ = 0; xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::TASK_RUNNING);
// Keep looping if paused, there is no timeout configured, or data was received more recently than the configured
// timeout
while (this_speaker->pause_state_ || !this_speaker->timeout_.has_value() || while (this_speaker->pause_state_ || !this_speaker->timeout_.has_value() ||
(millis() - last_data_received_time) <= this_speaker->timeout_.value()) { (millis() - last_data_received_time) <= this_speaker->timeout_.value()) {
event_group_bits = xEventGroupGetBits(this_speaker->event_group_); uint32_t event_group_bits = xEventGroupGetBits(this_speaker->event_group_);
if (event_group_bits & SpeakerEventGroupBits::COMMAND_STOP) { if (event_group_bits & SpeakerEventGroupBits::COMMAND_STOP) {
xEventGroupClearBits(this_speaker->event_group_, SpeakerEventGroupBits::COMMAND_STOP); xEventGroupClearBits(this_speaker->event_group_, SpeakerEventGroupBits::COMMAND_STOP);
@ -314,7 +298,7 @@ void I2SAudioSpeaker::speaker_task(void *params) {
stop_gracefully = true; stop_gracefully = true;
} }
if (this_speaker->audio_stream_info_ != audio_stream_info) { if (this_speaker->audio_stream_info_ != this_speaker->current_stream_info_) {
// Audio stream info changed, stop the speaker task so it will restart with the proper settings. // Audio stream info changed, stop the speaker task so it will restart with the proper settings.
break; break;
} }
@ -326,36 +310,75 @@ void I2SAudioSpeaker::speaker_task(void *params) {
} }
} }
#else #else
bool overflow; int64_t write_timestamp;
while (xQueueReceive(this_speaker->i2s_event_queue_, &overflow, 0)) { while (xQueueReceive(this_speaker->i2s_event_queue_, &write_timestamp, 0)) {
if (overflow) { // Receives timing events from the I2S on_sent callback. If actual audio data was sent in this event, it passes
// on the timing info via the audio_output_callback.
uint32_t frames_sent = frames_to_fill_single_dma_buffer;
if (frames_to_fill_single_dma_buffer > frames_written) {
tx_dma_underflow = true; tx_dma_underflow = true;
frames_sent = frames_written;
const uint32_t frames_zeroed = frames_to_fill_single_dma_buffer - frames_written;
write_timestamp -= this_speaker->current_stream_info_.frames_to_microseconds(frames_zeroed);
} else {
tx_dma_underflow = false;
}
frames_written -= frames_sent;
if (frames_sent > 0) {
this_speaker->audio_output_callback_(frames_sent, write_timestamp);
} }
} }
#endif #endif
if (this_speaker->pause_state_) { if (this_speaker->pause_state_) {
// Pause state is accessed atomically, so thread safe // Pause state is accessed atomically, so thread safe
// Delay so the task can yields, then skip transferring audio data // Delay so the task yields, then skip transferring audio data
delay(TASK_DELAY_MS); vTaskDelay(pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS));
continue; continue;
} }
size_t bytes_read = this_speaker->audio_ring_buffer_->read((void *) this_speaker->data_buffer_, data_buffer_size, // Wait half the duration of the data already written to the DMA buffers for new audio data
pdMS_TO_TICKS(TASK_DELAY_MS)); // The millisecond helper modifies the frames_written variable, so use the microsecond helper and divide by 1000
const uint32_t read_delay =
(this_speaker->current_stream_info_.frames_to_microseconds(frames_written) / 1000) / 2;
uint8_t *new_data = transfer_buffer->get_buffer_end(); // track start of any newly copied bytes
size_t bytes_read = transfer_buffer->transfer_data_from_source(pdMS_TO_TICKS(read_delay));
if (bytes_read > 0) { if (bytes_read > 0) {
if ((audio_stream_info.get_bits_per_sample() == 16) && (this_speaker->q15_volume_factor_ < INT16_MAX)) { if (this_speaker->q15_volume_factor_ < INT16_MAX) {
// Scale samples by the volume factor in place // Apply the software volume adjustment by unpacking the sample into a Q31 fixed-point number, shifting it,
q15_multiplication((int16_t *) this_speaker->data_buffer_, (int16_t *) this_speaker->data_buffer_, // multiplying by the volume factor, and packing the sample back into the original bytes per sample.
bytes_read / sizeof(int16_t), this_speaker->q15_volume_factor_);
const size_t bytes_per_sample = this_speaker->current_stream_info_.samples_to_bytes(1);
const uint32_t len = bytes_read / bytes_per_sample;
// Use Q16 for samples with 1 or 2 bytes: shifted_sample * gain_factor is Q16 * Q15 -> Q31
int32_t shift = 15; // Q31 -> Q16
int32_t gain_factor = this_speaker->q15_volume_factor_; // Q15
if (bytes_per_sample >= 3) {
// Use Q23 for samples with 3 or 4 bytes: shifted_sample * gain_factor is Q23 * Q8 -> Q31
shift = 8; // Q31 -> Q23
gain_factor >>= 7; // Q15 -> Q8
}
for (uint32_t i = 0; i < len; ++i) {
int32_t sample =
audio::unpack_audio_sample_to_q31(&new_data[i * bytes_per_sample], bytes_per_sample); // Q31
sample >>= shift;
sample *= gain_factor; // Q31
audio::pack_q31_as_audio_sample(sample, &new_data[i * bytes_per_sample], bytes_per_sample);
}
} }
#ifdef USE_ESP32_VARIANT_ESP32 #ifdef USE_ESP32_VARIANT_ESP32
// For ESP32 8/16 bit mono mode samples need to be switched. // For ESP32 8/16 bit mono mode samples need to be switched.
if (audio_stream_info.get_channels() == 1 && audio_stream_info.get_bits_per_sample() <= 16) { if (this_speaker->current_stream_info_.get_channels() == 1 &&
this_speaker->current_stream_info_.get_bits_per_sample() <= 16) {
size_t len = bytes_read / sizeof(int16_t); size_t len = bytes_read / sizeof(int16_t);
int16_t *tmp_buf = (int16_t *) this_speaker->data_buffer_; int16_t *tmp_buf = (int16_t *) new_data;
for (int i = 0; i < len; i += 2) { for (int i = 0; i < len; i += 2) {
int16_t tmp = tmp_buf[i]; int16_t tmp = tmp_buf[i];
tmp_buf[i] = tmp_buf[i + 1]; tmp_buf[i] = tmp_buf[i + 1];
@ -363,62 +386,87 @@ void I2SAudioSpeaker::speaker_task(void *params) {
} }
} }
#endif #endif
// Write the audio data to a single DMA buffer at a time to reduce latency for the audio duration played }
// callback.
const uint32_t batches = (bytes_read + single_dma_buffer_input_size - 1) / single_dma_buffer_input_size;
for (uint32_t i = 0; i < batches; ++i) { if (transfer_buffer->available() == 0) {
size_t bytes_written = 0;
size_t bytes_to_write = std::min(single_dma_buffer_input_size, bytes_read);
#ifdef USE_I2S_LEGACY
if (audio_stream_info.get_bits_per_sample() == (uint8_t) this_speaker->bits_per_sample_) {
i2s_write(this_speaker->parent_->get_port(), this_speaker->data_buffer_ + i * single_dma_buffer_input_size,
bytes_to_write, &bytes_written, pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS * 5));
} else if (audio_stream_info.get_bits_per_sample() < (uint8_t) this_speaker->bits_per_sample_) {
i2s_write_expand(this_speaker->parent_->get_port(),
this_speaker->data_buffer_ + i * single_dma_buffer_input_size, bytes_to_write,
audio_stream_info.get_bits_per_sample(), this_speaker->bits_per_sample_, &bytes_written,
pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS * 5));
}
#else
i2s_channel_write(this_speaker->tx_handle_, this_speaker->data_buffer_ + i * single_dma_buffer_input_size,
bytes_to_write, &bytes_written, pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS * 5));
#endif
int64_t now = esp_timer_get_time();
if (bytes_written != bytes_to_write) {
xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::ERR_ESP_INVALID_SIZE);
}
bytes_read -= bytes_written;
this_speaker->audio_output_callback_(audio_stream_info.bytes_to_frames(bytes_written),
now + dma_buffers_duration_ms * 1000);
tx_dma_underflow = false;
last_data_received_time = millis();
}
} else {
// No data received
if (stop_gracefully && tx_dma_underflow) { if (stop_gracefully && tx_dma_underflow) {
break; break;
} }
vTaskDelay(pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS / 2));
} else {
size_t bytes_written = 0;
#ifdef USE_I2S_LEGACY
if (this_speaker->current_stream_info_.get_bits_per_sample() == (uint8_t) this_speaker->bits_per_sample_) {
i2s_write(this_speaker->parent_->get_port(), transfer_buffer->get_buffer_start(),
transfer_buffer->available(), &bytes_written, pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS));
} else if (this_speaker->current_stream_info_.get_bits_per_sample() <
(uint8_t) this_speaker->bits_per_sample_) {
i2s_write_expand(this_speaker->parent_->get_port(), transfer_buffer->get_buffer_start(),
transfer_buffer->available(), this_speaker->current_stream_info_.get_bits_per_sample(),
this_speaker->bits_per_sample_, &bytes_written, pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS));
}
#else
if (tx_dma_underflow) {
// Temporarily disable channel and callback to reset the I2S driver's internal DMA buffer queue so timing
// callbacks are accurate. Preload the data.
i2s_channel_disable(this_speaker->tx_handle_);
const i2s_event_callbacks_t callbacks = {
.on_sent = nullptr,
};
i2s_channel_register_event_callback(this_speaker->tx_handle_, &callbacks, this_speaker);
i2s_channel_preload_data(this_speaker->tx_handle_, transfer_buffer->get_buffer_start(),
transfer_buffer->available(), &bytes_written);
} else {
// Audio is already playing, use regular I2S write to add to the DMA buffers
i2s_channel_write(this_speaker->tx_handle_, transfer_buffer->get_buffer_start(), transfer_buffer->available(),
&bytes_written, DMA_BUFFER_DURATION_MS);
}
#endif
if (bytes_written > 0) {
last_data_received_time = millis();
frames_written += this_speaker->current_stream_info_.bytes_to_frames(bytes_written);
transfer_buffer->decrease_buffer_length(bytes_written);
if (tx_dma_underflow) {
tx_dma_underflow = false;
#ifndef USE_I2S_LEGACY
// Reset the event queue timestamps
// Enable the on_sent callback to accurately track the timestamps of played audio
// Enable the I2S channel to start sending the preloaded audio
xQueueReset(this_speaker->i2s_event_queue_);
const i2s_event_callbacks_t callbacks = {
.on_sent = i2s_on_sent_cb,
};
i2s_channel_register_event_callback(this_speaker->tx_handle_, &callbacks, this_speaker);
i2s_channel_enable(this_speaker->tx_handle_);
#endif
}
#ifdef USE_I2S_LEGACY
// The legacy driver doesn't easily support the callback approach for timestamps, so fall back to a direct but
// less accurate approach.
this_speaker->audio_output_callback_(this_speaker->current_stream_info_.bytes_to_frames(bytes_written),
esp_timer_get_time() + dma_buffers_duration_ms * 1000);
#endif
}
} }
} }
xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::STATE_STOPPING);
#ifdef USE_I2S_LEGACY
i2s_driver_uninstall(this_speaker->parent_->get_port());
#else
i2s_channel_disable(this_speaker->tx_handle_);
i2s_del_channel(this_speaker->tx_handle_);
#endif
this_speaker->parent_->unlock();
} }
this_speaker->delete_task_(data_buffer_size); xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::TASK_STOPPING);
if (transfer_buffer != nullptr) {
transfer_buffer.reset();
}
xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::TASK_STOPPED);
while (true) {
// Continuously delay until the loop method deletes the task
vTaskDelay(pdMS_TO_TICKS(10));
}
} }
void I2SAudioSpeaker::start() { void I2SAudioSpeaker::start() {
@ -427,16 +475,7 @@ void I2SAudioSpeaker::start() {
if ((this->state_ == speaker::STATE_STARTING) || (this->state_ == speaker::STATE_RUNNING)) if ((this->state_ == speaker::STATE_STARTING) || (this->state_ == speaker::STATE_RUNNING))
return; return;
if (!this->task_created_ && (this->speaker_task_handle_ == nullptr)) { xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::COMMAND_START);
xTaskCreate(I2SAudioSpeaker::speaker_task, "speaker_task", TASK_STACK_SIZE, (void *) this, TASK_PRIORITY,
&this->speaker_task_handle_);
if (this->speaker_task_handle_ != nullptr) {
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::COMMAND_START);
} else {
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_TASK_FAILED_TO_START);
}
}
} }
void I2SAudioSpeaker::stop() { this->stop_(false); } void I2SAudioSpeaker::stop() { this->stop_(false); }
@ -456,61 +495,16 @@ void I2SAudioSpeaker::stop_(bool wait_on_empty) {
} }
} }
bool I2SAudioSpeaker::send_esp_err_to_event_group_(esp_err_t err) {
switch (err) {
case ESP_OK:
return false;
case ESP_ERR_INVALID_STATE:
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_INVALID_STATE);
return true;
case ESP_ERR_INVALID_ARG:
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_INVALID_ARG);
return true;
case ESP_ERR_INVALID_SIZE:
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_INVALID_SIZE);
return true;
case ESP_ERR_NO_MEM:
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_NO_MEM);
return true;
case ESP_ERR_NOT_SUPPORTED:
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_NOT_SUPPORTED);
return true;
default:
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_FAIL);
return true;
}
}
esp_err_t I2SAudioSpeaker::allocate_buffers_(size_t data_buffer_size, size_t ring_buffer_size) {
if (this->data_buffer_ == nullptr) {
// Allocate data buffer for temporarily storing audio from the ring buffer before writing to the I2S bus
RAMAllocator<uint8_t> allocator;
this->data_buffer_ = allocator.allocate(data_buffer_size);
}
if (this->data_buffer_ == nullptr) {
return ESP_ERR_NO_MEM;
}
if (this->audio_ring_buffer_.use_count() == 0) {
// Allocate ring buffer. Uses a shared_ptr to ensure it isn't improperly deallocated.
this->audio_ring_buffer_ = RingBuffer::create(ring_buffer_size);
}
if (this->audio_ring_buffer_ == nullptr) {
return ESP_ERR_NO_MEM;
}
return ESP_OK;
}
esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_stream_info) { esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_stream_info) {
this->current_stream_info_ = audio_stream_info; // store the stream info settings the driver will use
#ifdef USE_I2S_LEGACY #ifdef USE_I2S_LEGACY
if ((this->i2s_mode_ & I2S_MODE_SLAVE) && (this->sample_rate_ != audio_stream_info.get_sample_rate())) { // NOLINT if ((this->i2s_mode_ & I2S_MODE_SLAVE) && (this->sample_rate_ != audio_stream_info.get_sample_rate())) { // NOLINT
#else #else
if ((this->i2s_role_ & I2S_ROLE_SLAVE) && (this->sample_rate_ != audio_stream_info.get_sample_rate())) { // NOLINT if ((this->i2s_role_ & I2S_ROLE_SLAVE) && (this->sample_rate_ != audio_stream_info.get_sample_rate())) { // NOLINT
#endif #endif
// Can't reconfigure I2S bus, so the sample rate must match the configured value // Can't reconfigure I2S bus, so the sample rate must match the configured value
ESP_LOGE(TAG, "Audio stream settings are not compatible with this I2S configuration");
return ESP_ERR_NOT_SUPPORTED; return ESP_ERR_NOT_SUPPORTED;
} }
@ -521,10 +515,12 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_strea
(i2s_slot_bit_width_t) audio_stream_info.get_bits_per_sample() > this->slot_bit_width_) { (i2s_slot_bit_width_t) audio_stream_info.get_bits_per_sample() > this->slot_bit_width_) {
#endif #endif
// Currently can't handle the case when the incoming audio has more bits per sample than the configured value // Currently can't handle the case when the incoming audio has more bits per sample than the configured value
ESP_LOGE(TAG, "Audio streams with more bits per sample than the I2S speaker's configuration is not supported");
return ESP_ERR_NOT_SUPPORTED; return ESP_ERR_NOT_SUPPORTED;
} }
if (!this->parent_->try_lock()) { if (!this->parent_->try_lock()) {
ESP_LOGE(TAG, "Parent I2S bus not free");
return ESP_ERR_INVALID_STATE; return ESP_ERR_INVALID_STATE;
} }
@ -575,6 +571,7 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_strea
esp_err_t err = esp_err_t err =
i2s_driver_install(this->parent_->get_port(), &config, I2S_EVENT_QUEUE_COUNT, &this->i2s_event_queue_); i2s_driver_install(this->parent_->get_port(), &config, I2S_EVENT_QUEUE_COUNT, &this->i2s_event_queue_);
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to install I2S legacy driver");
// Failed to install the driver, so unlock the I2S port // Failed to install the driver, so unlock the I2S port
this->parent_->unlock(); this->parent_->unlock();
return err; return err;
@ -595,6 +592,7 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_strea
if (err != ESP_OK) { if (err != ESP_OK) {
// Failed to set the data out pin, so uninstall the driver and unlock the I2S port // Failed to set the data out pin, so uninstall the driver and unlock the I2S port
ESP_LOGE(TAG, "Failed to set the data out pin");
i2s_driver_uninstall(this->parent_->get_port()); i2s_driver_uninstall(this->parent_->get_port());
this->parent_->unlock(); this->parent_->unlock();
} }
@ -605,10 +603,12 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_strea
.dma_desc_num = DMA_BUFFERS_COUNT, .dma_desc_num = DMA_BUFFERS_COUNT,
.dma_frame_num = dma_buffer_length, .dma_frame_num = dma_buffer_length,
.auto_clear = true, .auto_clear = true,
.intr_priority = 3,
}; };
/* Allocate a new TX channel and get the handle of this channel */ /* Allocate a new TX channel and get the handle of this channel */
esp_err_t err = i2s_new_channel(&chan_cfg, &this->tx_handle_, NULL); esp_err_t err = i2s_new_channel(&chan_cfg, &this->tx_handle_, NULL);
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to allocate new I2S channel");
this->parent_->unlock(); this->parent_->unlock();
return err; return err;
} }
@ -652,7 +652,11 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_strea
// per sample causes the audio to play too fast. Setting the ws_width to the configured slot bit width seems to // per sample causes the audio to play too fast. Setting the ws_width to the configured slot bit width seems to
// make it play at the correct speed while sending more bits per slot. // make it play at the correct speed while sending more bits per slot.
if (this->slot_bit_width_ != I2S_SLOT_BIT_WIDTH_AUTO) { if (this->slot_bit_width_ != I2S_SLOT_BIT_WIDTH_AUTO) {
std_slot_cfg.ws_width = static_cast<uint32_t>(this->slot_bit_width_); uint32_t configured_bit_width = static_cast<uint32_t>(this->slot_bit_width_);
std_slot_cfg.ws_width = configured_bit_width;
if (configured_bit_width > 16) {
std_slot_cfg.msb_right = false;
}
} }
#else #else
std_slot_cfg.slot_bit_width = this->slot_bit_width_; std_slot_cfg.slot_bit_width = this->slot_bit_width_;
@ -670,54 +674,56 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_strea
err = i2s_channel_init_std_mode(this->tx_handle_, &std_cfg); err = i2s_channel_init_std_mode(this->tx_handle_, &std_cfg);
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to initialize channel");
i2s_del_channel(this->tx_handle_); i2s_del_channel(this->tx_handle_);
this->tx_handle_ = nullptr;
this->parent_->unlock(); this->parent_->unlock();
return err; return err;
} }
if (this->i2s_event_queue_ == nullptr) { if (this->i2s_event_queue_ == nullptr) {
this->i2s_event_queue_ = xQueueCreate(1, sizeof(bool)); this->i2s_event_queue_ = xQueueCreate(I2S_EVENT_QUEUE_COUNT, sizeof(int64_t));
} }
const i2s_event_callbacks_t callbacks = {
.on_send_q_ovf = i2s_overflow_cb,
};
i2s_channel_register_event_callback(this->tx_handle_, &callbacks, this);
/* Before reading data, start the TX channel first */
i2s_channel_enable(this->tx_handle_); i2s_channel_enable(this->tx_handle_);
if (err != ESP_OK) {
i2s_del_channel(this->tx_handle_);
this->parent_->unlock();
}
#endif #endif
return err; return err;
} }
void I2SAudioSpeaker::delete_task_(size_t buffer_size) { #ifndef USE_I2S_LEGACY
this->audio_ring_buffer_.reset(); // Releases ownership of the shared_ptr bool IRAM_ATTR I2SAudioSpeaker::i2s_on_sent_cb(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx) {
int64_t now = esp_timer_get_time();
if (this->data_buffer_ != nullptr) { BaseType_t need_yield1 = pdFALSE;
RAMAllocator<uint8_t> allocator; BaseType_t need_yield2 = pdFALSE;
allocator.deallocate(this->data_buffer_, buffer_size); BaseType_t need_yield3 = pdFALSE;
this->data_buffer_ = nullptr;
I2SAudioSpeaker *this_speaker = (I2SAudioSpeaker *) user_ctx;
if (xQueueIsQueueFullFromISR(this_speaker->i2s_event_queue_)) {
// Queue is full, so discard the oldest event and set the warning flag to inform the user
int64_t dummy;
xQueueReceiveFromISR(this_speaker->i2s_event_queue_, &dummy, &need_yield1);
xEventGroupSetBitsFromISR(this_speaker->event_group_, SpeakerEventGroupBits::WARN_DROPPED_EVENT, &need_yield2);
} }
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::STATE_STOPPED); xQueueSendToBackFromISR(this_speaker->i2s_event_queue_, &now, &need_yield3);
this->task_created_ = false; return need_yield1 | need_yield2 | need_yield3;
vTaskDelete(nullptr);
}
#ifndef USE_I2S_LEGACY
bool IRAM_ATTR I2SAudioSpeaker::i2s_overflow_cb(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx) {
I2SAudioSpeaker *this_speaker = (I2SAudioSpeaker *) user_ctx;
bool overflow = true;
xQueueOverwrite(this_speaker->i2s_event_queue_, &overflow);
return false;
} }
#endif #endif
void I2SAudioSpeaker::stop_i2s_driver_() {
#ifdef USE_I2S_LEGACY
i2s_driver_uninstall(this->parent_->get_port());
#else
i2s_channel_disable(this->tx_handle_);
i2s_del_channel(this->tx_handle_);
this->tx_handle_ = nullptr;
#endif
this->parent_->unlock();
}
} // namespace i2s_audio } // namespace i2s_audio
} // namespace esphome } // namespace esphome

View File

@ -72,70 +72,57 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp
protected: protected:
/// @brief Function for the FreeRTOS task handling audio output. /// @brief Function for the FreeRTOS task handling audio output.
/// After receiving the COMMAND_START signal, allocates space for the buffers, starts the I2S driver, and reads /// Allocates space for the buffers, reads audio from the ring buffer and writes audio to the I2S port. Stops
/// audio from the ring buffer and writes audio to the I2S port. Stops immmiately after receiving the COMMAND_STOP /// immmiately after receiving the COMMAND_STOP signal and stops only after the ring buffer is empty after receiving
/// signal and stops only after the ring buffer is empty after receiving the COMMAND_STOP_GRACEFULLY signal. Stops if /// the COMMAND_STOP_GRACEFULLY signal. Stops if the ring buffer hasn't read data for more than timeout_ milliseconds.
/// the ring buffer hasn't read data for more than timeout_ milliseconds. When stopping, it deallocates the buffers, /// When stopping, it deallocates the buffers. It communicates its state and any errors via ``event_group_``.
/// stops the I2S driver, unlocks the I2S port, and deletes the task. It communicates the state and any errors via
/// event_group_.
/// @param params I2SAudioSpeaker component /// @param params I2SAudioSpeaker component
static void speaker_task(void *params); static void speaker_task(void *params);
/// @brief Sends a stop command to the speaker task via event_group_. /// @brief Sends a stop command to the speaker task via ``event_group_``.
/// @param wait_on_empty If false, sends the COMMAND_STOP signal. If true, sends the COMMAND_STOP_GRACEFULLY signal. /// @param wait_on_empty If false, sends the COMMAND_STOP signal. If true, sends the COMMAND_STOP_GRACEFULLY signal.
void stop_(bool wait_on_empty); void stop_(bool wait_on_empty);
/// @brief Sets the corresponding ERR_ESP event group bits.
/// @param err esp_err_t error code.
/// @return True if an ERR_ESP bit is set and false if err == ESP_OK
bool send_esp_err_to_event_group_(esp_err_t err);
#ifndef USE_I2S_LEGACY #ifndef USE_I2S_LEGACY
static bool i2s_overflow_cb(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx); /// @brief Callback function used to send playback timestamps the to the speaker task.
/// @param handle (i2s_chan_handle_t)
/// @param event (i2s_event_data_t)
/// @param user_ctx (void*) User context pointer that the callback accesses
/// @return True if a higher priority task was interrupted
static bool i2s_on_sent_cb(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx);
#endif #endif
/// @brief Allocates the data buffer and ring buffer
/// @param data_buffer_size Number of bytes to allocate for the data buffer.
/// @param ring_buffer_size Number of bytes to allocate for the ring buffer.
/// @return ESP_ERR_NO_MEM if either buffer fails to allocate
/// ESP_OK if successful
esp_err_t allocate_buffers_(size_t data_buffer_size, size_t ring_buffer_size);
/// @brief Starts the ESP32 I2S driver. /// @brief Starts the ESP32 I2S driver.
/// Attempts to lock the I2S port, starts the I2S driver using the passed in stream information, and sets the data out /// Attempts to lock the I2S port, starts the I2S driver using the passed in stream information, and sets the data out
/// pin. If it fails, it will unlock the I2S port and uninstall the driver, if necessary. /// pin. If it fails, it will unlock the I2S port and uninstalls the driver, if necessary.
/// @param audio_stream_info Stream information for the I2S driver. /// @param audio_stream_info Stream information for the I2S driver.
/// @return ESP_ERR_NOT_ALLOWED if the I2S port can't play the incoming audio stream. /// @return ESP_ERR_NOT_ALLOWED if the I2S port can't play the incoming audio stream.
/// ESP_ERR_INVALID_STATE if the I2S port is already locked. /// ESP_ERR_INVALID_STATE if the I2S port is already locked.
/// ESP_ERR_INVALID_ARG if nstalling the driver or setting the data outpin fails due to a parameter error. /// ESP_ERR_INVALID_ARG if installing the driver or setting the data outpin fails due to a parameter error.
/// ESP_ERR_NO_MEM if the driver fails to install due to a memory allocation error. /// ESP_ERR_NO_MEM if the driver fails to install due to a memory allocation error.
/// ESP_FAIL if setting the data out pin fails due to an IO error ESP_OK if successful /// ESP_FAIL if setting the data out pin fails due to an IO error
/// ESP_OK if successful
esp_err_t start_i2s_driver_(audio::AudioStreamInfo &audio_stream_info); esp_err_t start_i2s_driver_(audio::AudioStreamInfo &audio_stream_info);
/// @brief Deletes the speaker's task. /// @brief Stops the I2S driver and unlocks the I2S port
/// Deallocates the data_buffer_ and audio_ring_buffer_, if necessary, and deletes the task. Should only be called by void stop_i2s_driver_();
/// the speaker_task itself.
/// @param buffer_size The allocated size of the data_buffer_.
void delete_task_(size_t buffer_size);
TaskHandle_t speaker_task_handle_{nullptr}; TaskHandle_t speaker_task_handle_{nullptr};
EventGroupHandle_t event_group_{nullptr}; EventGroupHandle_t event_group_{nullptr};
QueueHandle_t i2s_event_queue_; QueueHandle_t i2s_event_queue_;
uint8_t *data_buffer_; std::weak_ptr<RingBuffer> audio_ring_buffer_;
std::shared_ptr<RingBuffer> audio_ring_buffer_;
uint32_t buffer_duration_ms_; uint32_t buffer_duration_ms_;
optional<uint32_t> timeout_; optional<uint32_t> timeout_;
bool task_created_{false};
bool pause_state_{false}; bool pause_state_{false};
int16_t q15_volume_factor_{INT16_MAX}; int16_t q15_volume_factor_{INT16_MAX};
size_t bytes_written_{0}; audio::AudioStreamInfo current_stream_info_; // The currently loaded driver's stream info
#ifdef USE_I2S_LEGACY #ifdef USE_I2S_LEGACY
#if SOC_I2S_SUPPORTS_DAC #if SOC_I2S_SUPPORTS_DAC
@ -148,8 +135,6 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp
std::string i2s_comm_fmt_; std::string i2s_comm_fmt_;
i2s_chan_handle_t tx_handle_; i2s_chan_handle_t tx_handle_;
#endif #endif
uint32_t accumulated_frames_written_{0};
}; };
} // namespace i2s_audio } // namespace i2s_audio

View File

@ -138,9 +138,10 @@ def _validate(config):
]: ]:
raise cv.Invalid("Selected model can't run on ESP8266.") raise cv.Invalid("Selected model can't run on ESP8266.")
if model == "CUSTOM": if model == "CUSTOM" and (
if CONF_INIT_SEQUENCE not in config or CONF_DIMENSIONS not in config: CONF_INIT_SEQUENCE not in config or CONF_DIMENSIONS not in config
raise cv.Invalid("CUSTOM model requires init_sequence and dimensions") ):
raise cv.Invalid("CUSTOM model requires init_sequence and dimensions")
return config return config

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