mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +00:00 
			
		
		
		
	Merge branch 'dev' into revert-9439-lib_compat_mode_fix
This commit is contained in:
		| @@ -1 +1 @@ | ||||
| 90bb12a42dfe2a13378fb292fd67a5fd503b689bcec2be034be366758a5f69c6 | ||||
| 4df2fc55e977ba821978fac5f1e721ce2338e23647050b7005b4c801b1770739 | ||||
|   | ||||
							
								
								
									
										826
									
								
								.github/workflows/auto-label-pr.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										826
									
								
								.github/workflows/auto-label-pr.yml
									
									
									
									
										vendored
									
									
								
							| @@ -11,51 +11,10 @@ permissions: | ||||
|   contents: read | ||||
|  | ||||
| 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 | ||||
|   MAX_LABELS: 15 | ||||
|   TOO_BIG_THRESHOLD: 1000 | ||||
|   COMPONENT_LABEL_THRESHOLD: 10 | ||||
|  | ||||
| jobs: | ||||
|   label: | ||||
| @@ -65,24 +24,6 @@ jobs: | ||||
|       - name: Checkout | ||||
|         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 | ||||
|         id: generate-token | ||||
|         uses: actions/create-github-app-token@v2 | ||||
| @@ -97,73 +38,466 @@ jobs: | ||||
|           script: | | ||||
|             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 pr_number = context.issue.number; | ||||
|  | ||||
|             // Get current labels | ||||
|             // Get current labels and PR data | ||||
|             const { data: currentLabelsData } = await github.rest.issues.listLabelsOnIssue({ | ||||
|               owner, | ||||
|               repo, | ||||
|               issue_number: pr_number | ||||
|             }); | ||||
|             const currentLabels = currentLabelsData.map(label => label.name); | ||||
|  | ||||
|             // Define managed labels that this workflow controls | ||||
|             const managedLabels = currentLabels.filter(label => | ||||
|               label.startsWith('component: ') || | ||||
|               [ | ||||
|                 '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) | ||||
|               label.startsWith('component: ') || MANAGED_LABELS.includes(label) | ||||
|             ); | ||||
|  | ||||
|             // Check for mega-PR early - if present, skip most automatic labeling | ||||
|             const isMegaPR = currentLabels.includes('mega-pr'); | ||||
|  | ||||
|             // Get all PR files with automatic pagination | ||||
|             const prFiles = await github.paginate( | ||||
|               github.rest.pulls.listFiles, | ||||
|               { | ||||
|                 owner, | ||||
|                 repo, | ||||
|                 pull_number: pr_number | ||||
|               } | ||||
|             ); | ||||
|  | ||||
|             // Calculate data from PR files | ||||
|             const changedFiles = prFiles.map(file => file.filename); | ||||
|             const totalChanges = prFiles.reduce((sum, file) => sum + (file.additions || 0) + (file.deletions || 0), 0); | ||||
|  | ||||
|             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('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 | ||||
|             const targetPlatforms = `${{ env.TARGET_PLATFORMS }}`.split('\n').filter(p => p.trim().length > 0).map(p => p.trim()); | ||||
|             const platformComponents = `${{ env.PLATFORM_COMPONENTS }}`.split('\n').filter(p => p.trim().length > 0).map(p => p.trim()); | ||||
|             const smallPrThreshold = parseInt('${{ env.SMALL_PR_THRESHOLD }}'); | ||||
|             const maxLabels = parseInt('${{ env.MAX_LABELS }}'); | ||||
|             // Strategy: Merge branch detection | ||||
|             async function detectMergeBranch() { | ||||
|               const labels = new Set(); | ||||
|               const baseRef = context.payload.pull_request.base.ref; | ||||
|  | ||||
|             // Strategy: Merge to release or beta branch | ||||
|             const baseRef = context.payload.pull_request.base.ref; | ||||
|             if (baseRef !== 'dev') { | ||||
|               if (baseRef === 'release') { | ||||
|                 labels.add('merging-to-release'); | ||||
|               } else if (baseRef === 'beta') { | ||||
|                 labels.add('merging-to-beta'); | ||||
|               } | ||||
|  | ||||
|               // When targeting non-dev branches, only use merge warning labels | ||||
|               const finalLabels = Array.from(labels); | ||||
|               return labels; | ||||
|             } | ||||
|  | ||||
|             // Strategy: Component and platform labeling | ||||
|             async function detectComponentPlatforms(apiData) { | ||||
|               const labels = new Set(); | ||||
|               const componentRegex = /^esphome\/components\/([^\/]+)\//; | ||||
|               const targetPlatformRegex = new RegExp(`^esphome\/components\/(${apiData.targetPlatforms.join('|')})/`); | ||||
|  | ||||
|               for (const file of changedFiles) { | ||||
|                 const componentMatch = file.match(componentRegex); | ||||
|                 if (componentMatch) { | ||||
|                   labels.add(`component: ${componentMatch[1]}`); | ||||
|                 } | ||||
|  | ||||
|                 const platformMatch = file.match(targetPlatformRegex); | ||||
|                 if (platformMatch) { | ||||
|                   labels.add(`platform: ${platformMatch[1]}`); | ||||
|                 } | ||||
|               } | ||||
|  | ||||
|               return labels; | ||||
|             } | ||||
|  | ||||
|             // Strategy: New component detection | ||||
|             async function detectNewComponents() { | ||||
|               const labels = new Set(); | ||||
|               const addedFiles = prFiles.filter(file => file.status === 'added').map(file => file.filename); | ||||
|  | ||||
|               for (const file of addedFiles) { | ||||
|                 const componentMatch = file.match(/^esphome\/components\/([^\/]+)\/__init__\.py$/); | ||||
|                 if (componentMatch) { | ||||
|                   try { | ||||
|                     const content = fs.readFileSync(file, 'utf8'); | ||||
|                     if (content.includes('IS_TARGET_PLATFORM = True')) { | ||||
|                       labels.add('new-target-platform'); | ||||
|                     } | ||||
|                   } catch (error) { | ||||
|                     console.log(`Failed to read content of ${file}:`, error.message); | ||||
|                   } | ||||
|                   labels.add('new-component'); | ||||
|                 } | ||||
|               } | ||||
|  | ||||
|               return labels; | ||||
|             } | ||||
|  | ||||
|             // Strategy: New platform detection | ||||
|             async function detectNewPlatforms(apiData) { | ||||
|               const labels = new Set(); | ||||
|               const addedFiles = prFiles.filter(file => file.status === 'added').map(file => file.filename); | ||||
|  | ||||
|               for (const file of addedFiles) { | ||||
|                 const platformFileMatch = file.match(/^esphome\/components\/([^\/]+)\/([^\/]+)\.py$/); | ||||
|                 if (platformFileMatch) { | ||||
|                   const [, component, platform] = platformFileMatch; | ||||
|                   if (apiData.platformComponents.includes(platform)) { | ||||
|                     labels.add('new-platform'); | ||||
|                   } | ||||
|                 } | ||||
|  | ||||
|                 const platformDirMatch = file.match(/^esphome\/components\/([^\/]+)\/([^\/]+)\/__init__\.py$/); | ||||
|                 if (platformDirMatch) { | ||||
|                   const [, component, platform] = platformDirMatch; | ||||
|                   if (apiData.platformComponents.includes(platform)) { | ||||
|                     labels.add('new-platform'); | ||||
|                   } | ||||
|                 } | ||||
|               } | ||||
|  | ||||
|               return labels; | ||||
|             } | ||||
|  | ||||
|             // Strategy: Core files detection | ||||
|             async function detectCoreChanges() { | ||||
|               const labels = new Set(); | ||||
|               const coreFiles = changedFiles.filter(file => | ||||
|                 file.startsWith('esphome/core/') || | ||||
|                 (file.startsWith('esphome/') && file.split('/').length === 2) | ||||
|               ); | ||||
|  | ||||
|               if (coreFiles.length > 0) { | ||||
|                 labels.add('core'); | ||||
|               } | ||||
|  | ||||
|               return labels; | ||||
|             } | ||||
|  | ||||
|             // Strategy: PR size detection | ||||
|             async function detectPRSize() { | ||||
|               const labels = new Set(); | ||||
|               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(', ')); | ||||
|  | ||||
|               // Add new labels | ||||
|               // Apply labels | ||||
|               if (finalLabels.length > 0) { | ||||
|                 console.log(`Adding labels: ${finalLabels.join(', ')}`); | ||||
|                 await github.rest.issues.addLabels({ | ||||
|                   owner, | ||||
|                   repo, | ||||
| @@ -172,13 +506,9 @@ jobs: | ||||
|                 }); | ||||
|               } | ||||
|  | ||||
|               // Remove old managed labels that are no longer needed | ||||
|               const labelsToRemove = managedLabels.filter(label => | ||||
|                 !finalLabels.includes(label) | ||||
|               ); | ||||
|  | ||||
|               // Remove old managed labels | ||||
|               const labelsToRemove = managedLabels.filter(label => !finalLabels.includes(label)); | ||||
|               for (const label of labelsToRemove) { | ||||
|                 console.log(`Removing label: ${label}`); | ||||
|                 try { | ||||
|                   await github.rest.issues.removeLabel({ | ||||
|                     owner, | ||||
| @@ -191,235 +521,78 @@ jobs: | ||||
|                 } | ||||
|               } | ||||
|  | ||||
|               return; // Exit early, don't process other strategies | ||||
|               return; | ||||
|             } | ||||
|  | ||||
|             // Strategy: Component and Platform labeling | ||||
|             const componentRegex = /^esphome\/components\/([^\/]+)\//; | ||||
|             const targetPlatformRegex = new RegExp(`^esphome\/components\/(${targetPlatforms.join('|')})/`); | ||||
|             // Run all strategies | ||||
|             const [ | ||||
|               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) { | ||||
|               // Check for component changes | ||||
|               const componentMatch = file.match(componentRegex); | ||||
|               if (componentMatch) { | ||||
|                 const component = componentMatch[1]; | ||||
|                 labels.add(`component: ${component}`); | ||||
|               } | ||||
|             // Combine all labels | ||||
|             const allLabels = new Set([ | ||||
|               ...branchLabels, | ||||
|               ...componentLabels, | ||||
|               ...newComponentLabels, | ||||
|               ...newPlatformLabels, | ||||
|               ...coreLabels, | ||||
|               ...sizeLabels, | ||||
|               ...dashboardLabels, | ||||
|               ...actionsLabels, | ||||
|               ...codeOwnerLabels, | ||||
|               ...testLabels | ||||
|             ]); | ||||
|  | ||||
|               // Check for target platform changes | ||||
|               const platformMatch = file.match(targetPlatformRegex); | ||||
|               if (platformMatch) { | ||||
|                 const targetPlatform = platformMatch[1]; | ||||
|                 labels.add(`platform: ${targetPlatform}`); | ||||
|             // Detect requirements based on all other labels | ||||
|             const requirementLabels = await detectRequirements(allLabels); | ||||
|             for (const label of requirementLabels) { | ||||
|               allLabels.add(label); | ||||
|             } | ||||
|  | ||||
|             let finalLabels = Array.from(allLabels); | ||||
|  | ||||
|             // For mega-PRs, exclude component labels if there are too many | ||||
|             if (isMegaPR) { | ||||
|               const componentLabels = finalLabels.filter(label => label.startsWith('component: ')); | ||||
|               if (componentLabels.length > COMPONENT_LABEL_THRESHOLD) { | ||||
|                 finalLabels = finalLabels.filter(label => !label.startsWith('component: ')); | ||||
|                 console.log(`Mega-PR detected - excluding ${componentLabels.length} component labels (threshold: ${COMPONENT_LABEL_THRESHOLD})`); | ||||
|               } | ||||
|             } | ||||
|  | ||||
|             // Get PR files for new component/platform detection | ||||
|             const { data: prFiles } = await github.rest.pulls.listFiles({ | ||||
|               owner, | ||||
|               repo, | ||||
|               pull_number: pr_number | ||||
|             }); | ||||
|             // Handle too many labels (only for non-mega PRs) | ||||
|             const tooManyLabels = finalLabels.length > MAX_LABELS; | ||||
|  | ||||
|             const addedFiles = prFiles.filter(file => file.status === 'added').map(file => file.filename); | ||||
|  | ||||
|             // 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'); | ||||
|                 } | ||||
|               } | ||||
|             if (tooManyLabels && !isMegaPR && !finalLabels.includes('too-big')) { | ||||
|               finalLabels = ['too-big']; | ||||
|             } | ||||
|  | ||||
|             // 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: '.github/CODEOWNERS', | ||||
|                 ref: context.payload.pull_request.head.sha | ||||
|               }); | ||||
|  | ||||
|               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(', ')); | ||||
|  | ||||
|             // Don't set more than max labels | ||||
|             if (finalLabels.length > maxLabels) { | ||||
|               const originalLength = finalLabels.length; | ||||
|               console.log(`Not setting ${originalLength} labels because out of range`); | ||||
|               finalLabels = ['too-big']; | ||||
|             // Handle reviews | ||||
|             await handleReviews(finalLabels); | ||||
|  | ||||
|               // Request changes on the PR | ||||
|               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 | ||||
|             // Apply labels | ||||
|             if (finalLabels.length > 0) { | ||||
|               console.log(`Adding labels: ${finalLabels.join(', ')}`); | ||||
|               await github.rest.issues.addLabels({ | ||||
| @@ -430,11 +603,8 @@ jobs: | ||||
|               }); | ||||
|             } | ||||
|  | ||||
|             // Remove old managed labels that are no longer needed | ||||
|             const labelsToRemove = managedLabels.filter(label => | ||||
|               !finalLabels.includes(label) | ||||
|             ); | ||||
|  | ||||
|             // Remove old managed labels | ||||
|             const labelsToRemove = managedLabels.filter(label => !finalLabels.includes(label)); | ||||
|             for (const label of labelsToRemove) { | ||||
|               console.log(`Removing label: ${label}`); | ||||
|               try { | ||||
|   | ||||
							
								
								
									
										324
									
								
								.github/workflows/codeowner-review-request.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										324
									
								
								.github/workflows/codeowner-review-request.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,324 @@ | ||||
| # This workflow automatically requests reviews from codeowners when: | ||||
| # 1. A PR is opened, reopened, or synchronized (updated) | ||||
| # 2. A PR is marked as ready for review | ||||
| # | ||||
| # It reads the CODEOWNERS file and matches all changed files in the PR against | ||||
| # the codeowner patterns, then requests reviews from the appropriate owners | ||||
| # while avoiding duplicate requests for users who have already been requested | ||||
| # or have already reviewed the PR. | ||||
|  | ||||
| name: Request Codeowner Reviews | ||||
|  | ||||
| on: | ||||
|   # Needs to be pull_request_target to get write permissions | ||||
|   pull_request_target: | ||||
|     types: [opened, reopened, synchronize, ready_for_review] | ||||
|  | ||||
| permissions: | ||||
|   pull-requests: write | ||||
|   contents: read | ||||
|  | ||||
| jobs: | ||||
|   request-codeowner-reviews: | ||||
|     name: Run | ||||
|     if: ${{ !github.event.pull_request.draft }} | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Request reviews from component codeowners | ||||
|         uses: actions/github-script@v7.0.1 | ||||
|         with: | ||||
|           script: | | ||||
|             const owner = context.repo.owner; | ||||
|             const repo = context.repo.repo; | ||||
|             const pr_number = context.payload.pull_request.number; | ||||
|  | ||||
|             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 { | ||||
|               // Get the list of changed files in this PR | ||||
|               const { data: files } = await github.rest.pulls.listFiles({ | ||||
|                 owner, | ||||
|                 repo, | ||||
|                 pull_number: pr_number | ||||
|               }); | ||||
|  | ||||
|               const changedFiles = files.map(file => file.filename); | ||||
|               console.log(`Found ${changedFiles.length} changed files`); | ||||
|  | ||||
|               if (changedFiles.length === 0) { | ||||
|                 console.log('No changed files found, skipping codeowner review requests'); | ||||
|                 return; | ||||
|               } | ||||
|  | ||||
|               // Fetch CODEOWNERS file from root | ||||
|               const { data: codeownersFile } = await github.rest.repos.getContent({ | ||||
|                 owner, | ||||
|                 repo, | ||||
|                 path: 'CODEOWNERS', | ||||
|                 ref: context.payload.pull_request.base.sha | ||||
|               }); | ||||
|               const codeownersContent = Buffer.from(codeownersFile.content, 'base64').toString('utf8'); | ||||
|  | ||||
|               // Parse CODEOWNERS file to extract all patterns and their owners | ||||
|               const codeownersLines = codeownersContent.split('\n') | ||||
|                 .map(line => line.trim()) | ||||
|                 .filter(line => line && !line.startsWith('#')); | ||||
|  | ||||
|               const codeownersPatterns = []; | ||||
|  | ||||
|               // Convert CODEOWNERS pattern to regex (robust glob handling) | ||||
|               function globToRegex(pattern) { | ||||
|                 // Escape regex special characters except for glob wildcards | ||||
|                 let regexStr = pattern | ||||
|                   .replace(/([.+^=!:${}()|[\]\\])/g, '\\$1') // escape regex chars | ||||
|                   .replace(/\*\*/g, '.*') // globstar | ||||
|                   .replace(/\*/g, '[^/]*') // single star | ||||
|                   .replace(/\?/g, '.'); // question mark | ||||
|                 return new RegExp('^' + regexStr + '$'); | ||||
|               } | ||||
|  | ||||
|               // Helper function to create comment body | ||||
|               function createCommentBody(reviewersList, teamsList, matchedFileCount, isSuccessful = true) { | ||||
|                 const reviewerMentions = reviewersList.map(r => `@${r}`); | ||||
|                 const teamMentions = teamsList.map(t => `@${owner}/${t}`); | ||||
|                 const allMentions = [...reviewerMentions, ...teamMentions].join(', '); | ||||
|  | ||||
|                 if (isSuccessful) { | ||||
|                   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 { | ||||
|                   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._`; | ||||
|                 } | ||||
|               } | ||||
|  | ||||
|               for (const line of codeownersLines) { | ||||
|                 const parts = line.split(/\s+/); | ||||
|                 if (parts.length < 2) continue; | ||||
|  | ||||
|                 const pattern = parts[0]; | ||||
|                 const owners = parts.slice(1); | ||||
|  | ||||
|                 // Use robust glob-to-regex conversion | ||||
|                 const regex = globToRegex(pattern); | ||||
|                 codeownersPatterns.push({ pattern, regex, owners }); | ||||
|               } | ||||
|  | ||||
|               console.log(`Parsed ${codeownersPatterns.length} codeowner patterns`); | ||||
|  | ||||
|               // Match changed files against CODEOWNERS patterns | ||||
|               const matchedOwners = new Set(); | ||||
|               const matchedTeams = new Set(); | ||||
|               const fileMatches = new Map(); // Track which files matched which patterns | ||||
|  | ||||
|               for (const file of changedFiles) { | ||||
|                 for (const { pattern, regex, owners } of codeownersPatterns) { | ||||
|                   if (regex.test(file)) { | ||||
|                     console.log(`File '${file}' matches pattern '${pattern}' with owners: ${owners.join(', ')}`); | ||||
|  | ||||
|                     if (!fileMatches.has(file)) { | ||||
|                       fileMatches.set(file, []); | ||||
|                     } | ||||
|                     fileMatches.get(file).push({ pattern, owners }); | ||||
|  | ||||
|                     // Add owners to the appropriate set (remove @ prefix) | ||||
|                     for (const owner of owners) { | ||||
|                       const cleanOwner = owner.startsWith('@') ? owner.slice(1) : owner; | ||||
|                       if (cleanOwner.includes('/')) { | ||||
|                         // Team mention (org/team-name) | ||||
|                         const teamName = cleanOwner.split('/')[1]; | ||||
|                         matchedTeams.add(teamName); | ||||
|                       } else { | ||||
|                         // Individual user | ||||
|                         matchedOwners.add(cleanOwner); | ||||
|                       } | ||||
|                     } | ||||
|                   } | ||||
|                 } | ||||
|               } | ||||
|  | ||||
|               if (matchedOwners.size === 0 && matchedTeams.size === 0) { | ||||
|                 console.log('No codeowners found for any changed files'); | ||||
|                 return; | ||||
|               } | ||||
|  | ||||
|               // Remove the PR author from reviewers | ||||
|               const prAuthor = context.payload.pull_request.user.login; | ||||
|               matchedOwners.delete(prAuthor); | ||||
|  | ||||
|               // Get current reviewers to avoid duplicate requests (but still mention them) | ||||
|               const { data: prData } = await github.rest.pulls.get({ | ||||
|                 owner, | ||||
|                 repo, | ||||
|                 pull_number: pr_number | ||||
|               }); | ||||
|  | ||||
|               const currentReviewers = new Set(); | ||||
|               const currentTeams = new Set(); | ||||
|  | ||||
|               if (prData.requested_reviewers) { | ||||
|                 prData.requested_reviewers.forEach(reviewer => { | ||||
|                   currentReviewers.add(reviewer.login); | ||||
|                 }); | ||||
|               } | ||||
|  | ||||
|               if (prData.requested_teams) { | ||||
|                 prData.requested_teams.forEach(team => { | ||||
|                   currentTeams.add(team.slug); | ||||
|                 }); | ||||
|               } | ||||
|  | ||||
|               // Check for completed reviews to avoid re-requesting users who have already reviewed | ||||
|               const { data: reviews } = await github.rest.pulls.listReviews({ | ||||
|                 owner, | ||||
|                 repo, | ||||
|                 pull_number: pr_number | ||||
|               }); | ||||
|  | ||||
|               const reviewedUsers = new Set(); | ||||
|               reviews.forEach(review => { | ||||
|                 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) | ||||
|               reviewedUsers.forEach(reviewer => { | ||||
|                 matchedOwners.delete(reviewer); | ||||
|               }); | ||||
|  | ||||
|               // For teams, we'll still remove already requested teams to avoid API errors | ||||
|               currentTeams.forEach(team => { | ||||
|                 matchedTeams.delete(team); | ||||
|               }); | ||||
|  | ||||
|               const reviewersList = Array.from(matchedOwners); | ||||
|               const teamsList = Array.from(matchedTeams); | ||||
|  | ||||
|               if (reviewersList.length === 0 && teamsList.length === 0) { | ||||
|                 console.log('No eligible reviewers found (all may already be requested, reviewed, or previously pinged)'); | ||||
|                 return; | ||||
|               } | ||||
|  | ||||
|               const totalReviewers = reviewersList.length + teamsList.length; | ||||
|               console.log(`Requesting reviews from ${reviewersList.length} users and ${teamsList.length} teams for ${fileMatches.size} matched files`); | ||||
|  | ||||
|               // Request reviews | ||||
|               try { | ||||
|                 const requestParams = { | ||||
|                   owner, | ||||
|                   repo, | ||||
|                   pull_number: pr_number | ||||
|                 }; | ||||
|  | ||||
|                 // Filter out users who are already requested reviewers for the API call | ||||
|                 const newReviewers = reviewersList.filter(reviewer => !currentReviewers.has(reviewer)); | ||||
|                 const newTeams = teamsList.filter(team => !currentTeams.has(team)); | ||||
|  | ||||
|                 if (newReviewers.length > 0) { | ||||
|                   requestParams.reviewers = newReviewers; | ||||
|                 } | ||||
|  | ||||
|                 if (newTeams.length > 0) { | ||||
|                   requestParams.team_reviewers = newTeams; | ||||
|                 } | ||||
|  | ||||
|                 // Only make the API call if there are new reviewers to request | ||||
|                 if (newReviewers.length > 0 || newTeams.length > 0) { | ||||
|                   await github.rest.pulls.requestReviewers(requestParams); | ||||
|                   console.log(`Successfully requested reviews from ${newReviewers.length} new users and ${newTeams.length} new teams`); | ||||
|                 } else { | ||||
|                   console.log('All codeowners are already requested reviewers or have reviewed'); | ||||
|                 } | ||||
|  | ||||
|                 // Only add a comment if there are new codeowners to mention (not previously pinged) | ||||
|                 if (reviewersList.length > 0 || teamsList.length > 0) { | ||||
|                   const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, true); | ||||
|  | ||||
|                   await github.rest.issues.createComment({ | ||||
|                     owner, | ||||
|                     repo, | ||||
|                     issue_number: pr_number, | ||||
|                     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) { | ||||
|                 if (error.status === 422) { | ||||
|                   console.log('Some reviewers may already be requested or unavailable:', error.message); | ||||
|  | ||||
|                   // Only try to add a comment if there are new codeowners to mention | ||||
|                   if (reviewersList.length > 0 || teamsList.length > 0) { | ||||
|                     const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, false); | ||||
|  | ||||
|                     try { | ||||
|                       await github.rest.issues.createComment({ | ||||
|                         owner, | ||||
|                         repo, | ||||
|                         issue_number: pr_number, | ||||
|                         body: commentBody | ||||
|                       }); | ||||
|                       console.log(`Added fallback comment mentioning ${reviewersList.length} users and ${teamsList.length} teams`); | ||||
|                     } catch (commentError) { | ||||
|                       console.log('Failed to add comment:', commentError.message); | ||||
|                     } | ||||
|                   } else { | ||||
|                     console.log('No new codeowners to mention in fallback comment'); | ||||
|                   } | ||||
|                 } else { | ||||
|                   throw error; | ||||
|                 } | ||||
|               } | ||||
|  | ||||
|             } catch (error) { | ||||
|               console.log('Failed to process codeowner review requests:', error.message); | ||||
|               console.error(error); | ||||
|             } | ||||
							
								
								
									
										26
									
								
								.github/workflows/external-component-bot.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										26
									
								
								.github/workflows/external-component-bot.yml
									
									
									
									
										vendored
									
									
								
							| @@ -61,7 +61,8 @@ jobs: | ||||
|             } | ||||
|  | ||||
|             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; | ||||
|                 if (esphomeChanges.length === 1) { | ||||
|                     commentBody = generateExternalComponentInstructions(prNumber, componentChanges, owner, repo); | ||||
| @@ -71,14 +72,23 @@ jobs: | ||||
|                 commentBody += `\n\n---\n(Added by the PR bot)\n\n${commentMarker}`; | ||||
|  | ||||
|                 // Check for existing bot comment | ||||
|                 const comments = await github.rest.issues.listComments({ | ||||
|                     owner: owner, | ||||
|                     repo: repo, | ||||
|                     issue_number: prNumber, | ||||
|                 }); | ||||
|                 const comments = await github.paginate( | ||||
|                     github.rest.issues.listComments, | ||||
|                     { | ||||
|                         owner: owner, | ||||
|                         repo: repo, | ||||
|                         issue_number: prNumber, | ||||
|                         per_page: 100, | ||||
|                     } | ||||
|                 ); | ||||
|  | ||||
|                 const botComment = comments.data.find(comment => | ||||
|                     comment.body.includes(commentMarker) | ||||
|                 const sorted = comments.sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at)); | ||||
|  | ||||
|                 const botComment = sorted.find(comment => | ||||
|                     ( | ||||
|                       comment.body.includes(commentMarker) || | ||||
|                       comment.body.includes(legacyCommentMarker) | ||||
|                     ) && comment.user.type === "Bot" | ||||
|                 ); | ||||
|  | ||||
|                 if (botComment && botComment.body === commentBody) { | ||||
|   | ||||
							
								
								
									
										163
									
								
								.github/workflows/issue-codeowner-notify.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										163
									
								
								.github/workflows/issue-codeowner-notify.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,163 @@ | ||||
| # This workflow automatically notifies codeowners when an issue is labeled with component labels. | ||||
| # It reads the CODEOWNERS file to find the maintainers for the labeled components | ||||
| # and posts a comment mentioning them to ensure they're aware of the issue. | ||||
|  | ||||
| name: Notify Issue Codeowners | ||||
|  | ||||
| on: | ||||
|   issues: | ||||
|     types: [labeled] | ||||
|  | ||||
| permissions: | ||||
|   issues: write | ||||
|   contents: read | ||||
|  | ||||
| jobs: | ||||
|   notify-codeowners: | ||||
|     name: Run | ||||
|     if: ${{ startsWith(github.event.label.name, format('component{0} ', ':')) }} | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Notify codeowners for component issues | ||||
|         uses: actions/github-script@v7.0.1 | ||||
|         with: | ||||
|           script: | | ||||
|             const owner = context.repo.owner; | ||||
|             const repo = context.repo.repo; | ||||
|             const issue_number = context.payload.issue.number; | ||||
|             const labelName = context.payload.label.name; | ||||
|  | ||||
|             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 | ||||
|             const componentName = labelName.replace('component: ', ''); | ||||
|             console.log(`Component: ${componentName}`); | ||||
|  | ||||
|             try { | ||||
|               // Fetch CODEOWNERS file from root | ||||
|               const { data: codeownersFile } = await github.rest.repos.getContent({ | ||||
|                 owner, | ||||
|                 repo, | ||||
|                 path: 'CODEOWNERS' | ||||
|               }); | ||||
|               const codeownersContent = Buffer.from(codeownersFile.content, 'base64').toString('utf8'); | ||||
|  | ||||
|               // Parse CODEOWNERS file to extract component mappings | ||||
|               const codeownersLines = codeownersContent.split('\n') | ||||
|                 .map(line => line.trim()) | ||||
|                 .filter(line => line && !line.startsWith('#')); | ||||
|  | ||||
|               let componentOwners = null; | ||||
|  | ||||
|               for (const line of codeownersLines) { | ||||
|                 const parts = line.split(/\s+/); | ||||
|                 if (parts.length < 2) continue; | ||||
|  | ||||
|                 const pattern = parts[0]; | ||||
|                 const owners = parts.slice(1); | ||||
|  | ||||
|                 // Look for component patterns: esphome/components/{component}/* | ||||
|                 const componentMatch = pattern.match(/^esphome\/components\/([^\/]+)\/\*$/); | ||||
|                 if (componentMatch && componentMatch[1] === componentName) { | ||||
|                   componentOwners = owners; | ||||
|                   break; | ||||
|                 } | ||||
|               } | ||||
|  | ||||
|               if (!componentOwners) { | ||||
|                 console.log(`No codeowners found for component: ${componentName}`); | ||||
|                 return; | ||||
|               } | ||||
|  | ||||
|               console.log(`Found codeowners for '${componentName}': ${componentOwners.join(', ')}`); | ||||
|  | ||||
|               // Separate users and teams | ||||
|               const userOwners = []; | ||||
|               const teamOwners = []; | ||||
|  | ||||
|               for (const owner of componentOwners) { | ||||
|                 const cleanOwner = owner.startsWith('@') ? owner.slice(1) : owner; | ||||
|                 if (cleanOwner.includes('/')) { | ||||
|                   // Team mention (org/team-name) | ||||
|                   teamOwners.push(`@${cleanOwner}`); | ||||
|                 } else { | ||||
|                   // Individual user | ||||
|                   userOwners.push(`@${cleanOwner}`); | ||||
|                 } | ||||
|               } | ||||
|  | ||||
|               // Remove issue author from mentions to avoid self-notification | ||||
|               const issueAuthor = context.payload.issue.user.login; | ||||
|               const filteredUserOwners = userOwners.filter(mention => | ||||
|                 mention !== `@${issueAuthor}` | ||||
|               ); | ||||
|  | ||||
|               // 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) { | ||||
|                 console.log('No new codeowners to notify (all previously pinged or issue author is the only codeowner)'); | ||||
|                 return; | ||||
|               } | ||||
|  | ||||
|               // Create comment body | ||||
|               const mentionString = allMentions.join(', '); | ||||
|               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 | ||||
|               await github.rest.issues.createComment({ | ||||
|                 owner, | ||||
|                 repo, | ||||
|                 issue_number: issue_number, | ||||
|                 body: commentBody | ||||
|               }); | ||||
|  | ||||
|               console.log(`Successfully notified new codeowners: ${mentionString}`); | ||||
|  | ||||
|             } catch (error) { | ||||
|               console.log('Failed to process codeowner notifications:', error.message); | ||||
|               console.error(error); | ||||
|             } | ||||
| @@ -11,7 +11,7 @@ ci: | ||||
| repos: | ||||
|   - repo: https://github.com/astral-sh/ruff-pre-commit | ||||
|     # Ruff version. | ||||
|     rev: v0.12.4 | ||||
|     rev: v0.12.5 | ||||
|     hooks: | ||||
|       # Run the linter. | ||||
|       - id: ruff | ||||
|   | ||||
| @@ -9,6 +9,7 @@ | ||||
| pyproject.toml @esphome/core | ||||
| esphome/*.py @esphome/core | ||||
| esphome/core/* @esphome/core | ||||
| .github/** @esphome/core | ||||
|  | ||||
| # Integrations | ||||
| esphome/components/a01nyub/* @MrSuicideParrot | ||||
| @@ -245,6 +246,7 @@ esphome/components/lcd_menu/* @numo68 | ||||
| esphome/components/ld2410/* @regevbr @sebcaps | ||||
| esphome/components/ld2420/* @descipher | ||||
| esphome/components/ld2450/* @hareeshmu | ||||
| esphome/components/ld24xx/* @kbx81 | ||||
| esphome/components/ledc/* @OttoWinter | ||||
| esphome/components/libretiny/* @kuba2k2 | ||||
| esphome/components/libretiny_pwm/* @kuba2k2 | ||||
| @@ -292,6 +294,7 @@ esphome/components/microphone/* @jesserockz @kahrendt | ||||
| esphome/components/mics_4514/* @jesserockz | ||||
| esphome/components/midea/* @dudanov | ||||
| esphome/components/midea_ir/* @dudanov | ||||
| esphome/components/mipi_dsi/* @clydebarrow | ||||
| esphome/components/mipi_spi/* @clydebarrow | ||||
| esphome/components/mitsubishi/* @RubyBailey | ||||
| esphome/components/mixer/speaker/* @kahrendt | ||||
|   | ||||
| @@ -2,6 +2,7 @@ | ||||
| import argparse | ||||
| from datetime import datetime | ||||
| import functools | ||||
| import getpass | ||||
| import importlib | ||||
| import logging | ||||
| import os | ||||
| @@ -34,6 +35,7 @@ from esphome.const import ( | ||||
|     CONF_PORT, | ||||
|     CONF_SUBSTITUTIONS, | ||||
|     CONF_TOPIC, | ||||
|     ENV_NOGITIGNORE, | ||||
|     PLATFORM_ESP32, | ||||
|     PLATFORM_ESP8266, | ||||
|     PLATFORM_RP2040, | ||||
| @@ -88,9 +90,9 @@ def choose_prompt(options, purpose: str = None): | ||||
| def choose_upload_log_host( | ||||
|     default, check_default, show_ota, show_mqtt, show_api, purpose: str = None | ||||
| ): | ||||
|     options = [] | ||||
|     for port in get_serial_ports(): | ||||
|         options.append((f"{port.path} ({port.description})", port.path)) | ||||
|     options = [ | ||||
|         (f"{port.path} ({port.description})", port.path) for port in get_serial_ports() | ||||
|     ] | ||||
|     if default == "SERIAL": | ||||
|         return choose_prompt(options, purpose=purpose) | ||||
|     if (show_ota and "ota" in CORE.config) or (show_api and "api" in CORE.config): | ||||
| @@ -118,9 +120,7 @@ def mqtt_logging_enabled(mqtt_config): | ||||
|         return False | ||||
|     if CONF_TOPIC not in log_topic: | ||||
|         return False | ||||
|     if log_topic.get(CONF_LEVEL, None) == "NONE": | ||||
|         return False | ||||
|     return True | ||||
|     return log_topic.get(CONF_LEVEL, None) != "NONE" | ||||
|  | ||||
|  | ||||
| def get_port_type(port): | ||||
| @@ -209,6 +209,9 @@ def wrap_to_code(name, comp): | ||||
|  | ||||
|  | ||||
| def write_cpp(config): | ||||
|     if not get_bool_env(ENV_NOGITIGNORE): | ||||
|         writer.write_gitignore() | ||||
|  | ||||
|     generate_cpp_contents(config) | ||||
|     return write_cpp_file() | ||||
|  | ||||
| @@ -225,10 +228,13 @@ def generate_cpp_contents(config): | ||||
|  | ||||
|  | ||||
| def write_cpp_file(): | ||||
|     writer.write_platformio_project() | ||||
|  | ||||
|     code_s = indent(CORE.cpp_main_section) | ||||
|     writer.write_cpp(code_s) | ||||
|  | ||||
|     from esphome.build_gen import platformio | ||||
|  | ||||
|     platformio.write_project() | ||||
|  | ||||
|     return 0 | ||||
|  | ||||
|  | ||||
| @@ -330,7 +336,7 @@ def check_permissions(port): | ||||
|             raise EsphomeError( | ||||
|                 "You do not have read or write permission on the selected serial port. " | ||||
|                 "To resolve this issue, you can add your user to the dialout group " | ||||
|                 f"by running the following command: sudo usermod -a -G dialout {os.getlogin()}. " | ||||
|                 f"by running the following command: sudo usermod -a -G dialout {getpass.getuser()}. " | ||||
|                 "You will need to log out & back in or reboot to activate the new group access." | ||||
|             ) | ||||
|  | ||||
|   | ||||
							
								
								
									
										0
									
								
								esphome/build_gen/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								esphome/build_gen/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										102
									
								
								esphome/build_gen/platformio.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								esphome/build_gen/platformio.py
									
									
									
									
									
										Normal 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) | ||||
| @@ -7,7 +7,6 @@ namespace a4988 { | ||||
| static const char *const TAG = "a4988.stepper"; | ||||
|  | ||||
| void A4988::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
|   if (this->sleep_pin_ != nullptr) { | ||||
|     this->sleep_pin_->setup(); | ||||
|     this->sleep_pin_->digital_write(false); | ||||
|   | ||||
| @@ -7,8 +7,6 @@ namespace absolute_humidity { | ||||
| static const char *const TAG = "absolute_humidity.sensor"; | ||||
|  | ||||
| void AbsoluteHumidityComponent::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->get_name().c_str()); | ||||
|  | ||||
|   ESP_LOGD(TAG, "  Added callback for temperature '%s'", this->temperature_sensor_->get_name().c_str()); | ||||
|   this->temperature_sensor_->add_on_state_callback([this](float state) { this->temperature_callback_(state); }); | ||||
|   if (this->temperature_sensor_->has_state()) { | ||||
|   | ||||
| @@ -37,7 +37,6 @@ const LogString *adc_unit_to_str(adc_unit_t unit) { | ||||
| } | ||||
|  | ||||
| void ADCSensor::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->get_name().c_str()); | ||||
|   // Check if another sensor already initialized this ADC unit | ||||
|   if (ADCSensor::shared_adc_handles[this->adc_unit_] == nullptr) { | ||||
|     adc_oneshot_unit_init_cfg_t init_config = {};  // Zero initialize | ||||
|   | ||||
| @@ -17,7 +17,6 @@ namespace adc { | ||||
| static const char *const TAG = "adc.esp8266"; | ||||
|  | ||||
| void ADCSensor::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->get_name().c_str()); | ||||
| #ifndef USE_ADC_SENSOR_VCC | ||||
|   this->pin_->setup(); | ||||
| #endif | ||||
|   | ||||
| @@ -9,7 +9,6 @@ namespace adc { | ||||
| static const char *const TAG = "adc.libretiny"; | ||||
|  | ||||
| void ADCSensor::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->get_name().c_str()); | ||||
| #ifndef USE_ADC_SENSOR_VCC | ||||
|   this->pin_->setup(); | ||||
| #endif  // !USE_ADC_SENSOR_VCC | ||||
|   | ||||
| @@ -14,7 +14,6 @@ namespace adc { | ||||
| static const char *const TAG = "adc.rp2040"; | ||||
|  | ||||
| void ADCSensor::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->get_name().c_str()); | ||||
|   static bool initialized = false; | ||||
|   if (!initialized) { | ||||
|     adc_init(); | ||||
|   | ||||
| @@ -8,10 +8,7 @@ static const char *const TAG = "adc128s102"; | ||||
|  | ||||
| float ADC128S102::get_setup_priority() const { return setup_priority::HARDWARE; } | ||||
|  | ||||
| void ADC128S102::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
|   this->spi_setup(); | ||||
| } | ||||
| void ADC128S102::setup() { this->spi_setup(); } | ||||
|  | ||||
| void ADC128S102::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "ADC128S102:"); | ||||
|   | ||||
| @@ -10,7 +10,6 @@ static const uint8_t ADS1115_REGISTER_CONVERSION = 0x00; | ||||
| static const uint8_t ADS1115_REGISTER_CONFIG = 0x01; | ||||
|  | ||||
| void ADS1115Component::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
|   uint16_t value; | ||||
|   if (!this->read_byte_16(ADS1115_REGISTER_CONVERSION, &value)) { | ||||
|     this->mark_failed(); | ||||
|   | ||||
| @@ -9,7 +9,6 @@ static const char *const TAG = "ads1118"; | ||||
| static const uint8_t ADS1118_DATA_RATE_860_SPS = 0b111; | ||||
|  | ||||
| void ADS1118::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
|   this->spi_setup(); | ||||
|  | ||||
|   this->config_ = 0; | ||||
|   | ||||
| @@ -24,8 +24,6 @@ static const uint16_t ZP_CURRENT = 0x0000; | ||||
| static const uint16_t ZP_DEFAULT = 0xFFFF; | ||||
|  | ||||
| void AGS10Component::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
|  | ||||
|   auto version = this->read_version_(); | ||||
|   if (version) { | ||||
|     ESP_LOGD(TAG, "AGS10 Sensor Version: 0x%02X", *version); | ||||
| @@ -45,8 +43,6 @@ void AGS10Component::setup() { | ||||
|   } else { | ||||
|     ESP_LOGE(TAG, "AGS10 Sensor Resistance: unknown"); | ||||
|   } | ||||
|  | ||||
|   ESP_LOGD(TAG, "Sensor initialized"); | ||||
| } | ||||
|  | ||||
| void AGS10Component::update() { | ||||
|   | ||||
| @@ -38,8 +38,6 @@ static const uint8_t AHT10_STATUS_BUSY = 0x80; | ||||
| static const float AHT10_DIVISOR = 1048576.0f;  // 2^20, used for temperature and humidity calculations | ||||
|  | ||||
| void AHT10Component::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
|  | ||||
|   if (this->write(AHT10_SOFTRESET_CMD, sizeof(AHT10_SOFTRESET_CMD)) != i2c::ERROR_OK) { | ||||
|     ESP_LOGE(TAG, "Reset failed"); | ||||
|   } | ||||
| @@ -80,8 +78,6 @@ void AHT10Component::setup() { | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   ESP_LOGV(TAG, "Initialization complete"); | ||||
| } | ||||
|  | ||||
| void AHT10Component::restart_read_() { | ||||
|   | ||||
| @@ -17,8 +17,6 @@ static const char *const TAG = "aic3204"; | ||||
|   } | ||||
|  | ||||
| void AIC3204::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
|  | ||||
|   // Set register page to 0 | ||||
|   ERROR_CHECK(this->write_byte(AIC3204_PAGE_CTRL, 0x00), "Set page 0 failed"); | ||||
|   // Initiate SW reset (PLL is powered off as part of reset) | ||||
|   | ||||
| @@ -90,8 +90,6 @@ bool AM2315C::convert_(uint8_t *data, float &humidity, float &temperature) { | ||||
| } | ||||
|  | ||||
| void AM2315C::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
|  | ||||
|   // get status | ||||
|   uint8_t status = 0; | ||||
|   if (this->read(&status, 1) != i2c::ERROR_OK) { | ||||
|   | ||||
| @@ -34,7 +34,6 @@ void AM2320Component::update() { | ||||
|   this->status_clear_warning(); | ||||
| } | ||||
| void AM2320Component::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
|   uint8_t data[8]; | ||||
|   data[0] = 0; | ||||
|   data[1] = 4; | ||||
|   | ||||
| @@ -54,8 +54,6 @@ enum {  // APDS9306 registers | ||||
|   } | ||||
|  | ||||
| void APDS9306::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
|  | ||||
|   uint8_t id; | ||||
|   if (!this->read_byte(APDS9306_PART_ID, &id)) {  // Part ID register | ||||
|     this->error_code_ = COMMUNICATION_FAILED; | ||||
| @@ -86,8 +84,6 @@ void APDS9306::setup() { | ||||
|  | ||||
|   // Set to active mode | ||||
|   APDS9306_WRITE_BYTE(APDS9306_MAIN_CTRL, 0x02); | ||||
|  | ||||
|   ESP_LOGCONFIG(TAG, "APDS9306 setup complete"); | ||||
| } | ||||
|  | ||||
| void APDS9306::dump_config() { | ||||
|   | ||||
| @@ -15,7 +15,6 @@ static const char *const TAG = "apds9960"; | ||||
| #define APDS9960_WRITE_BYTE(reg, value) APDS9960_ERROR_CHECK(this->write_byte(reg, value)); | ||||
|  | ||||
| void APDS9960::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
|   uint8_t id; | ||||
|   if (!this->read_byte(0x92, &id)) {  // ID register | ||||
|     this->error_code_ = COMMUNICATION_FAILED; | ||||
|   | ||||
| @@ -53,6 +53,8 @@ SERVICE_ARG_NATIVE_TYPES = { | ||||
| CONF_ENCRYPTION = "encryption" | ||||
| CONF_BATCH_DELAY = "batch_delay" | ||||
| CONF_CUSTOM_SERVICES = "custom_services" | ||||
| CONF_HOMEASSISTANT_SERVICES = "homeassistant_services" | ||||
| CONF_HOMEASSISTANT_STATES = "homeassistant_states" | ||||
|  | ||||
|  | ||||
| def validate_encryption_key(value): | ||||
| @@ -118,6 +120,8 @@ CONFIG_SCHEMA = cv.All( | ||||
|                 cv.Range(max=cv.TimePeriod(milliseconds=65535)), | ||||
|             ), | ||||
|             cv.Optional(CONF_CUSTOM_SERVICES, default=False): cv.boolean, | ||||
|             cv.Optional(CONF_HOMEASSISTANT_SERVICES, default=False): cv.boolean, | ||||
|             cv.Optional(CONF_HOMEASSISTANT_STATES, default=False): cv.boolean, | ||||
|             cv.Optional(CONF_ON_CLIENT_CONNECTED): automation.validate_automation( | ||||
|                 single=True | ||||
|             ), | ||||
| @@ -146,6 +150,12 @@ async def to_code(config): | ||||
|     if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]: | ||||
|         cg.add_define("USE_API_SERVICES") | ||||
|  | ||||
|     if config[CONF_HOMEASSISTANT_SERVICES]: | ||||
|         cg.add_define("USE_API_HOMEASSISTANT_SERVICES") | ||||
|  | ||||
|     if config[CONF_HOMEASSISTANT_STATES]: | ||||
|         cg.add_define("USE_API_HOMEASSISTANT_STATES") | ||||
|  | ||||
|     if actions := config.get(CONF_ACTIONS, []): | ||||
|         for conf in actions: | ||||
|             template_args = [] | ||||
| @@ -235,6 +245,7 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All( | ||||
|     HOMEASSISTANT_ACTION_ACTION_SCHEMA, | ||||
| ) | ||||
| async def homeassistant_service_to_code(config, action_id, template_arg, args): | ||||
|     cg.add_define("USE_API_HOMEASSISTANT_SERVICES") | ||||
|     serv = await cg.get_variable(config[CONF_ID]) | ||||
|     var = cg.new_Pvariable(action_id, template_arg, serv, False) | ||||
|     templ = await cg.templatable(config[CONF_ACTION], args, None) | ||||
| @@ -278,6 +289,7 @@ HOMEASSISTANT_EVENT_ACTION_SCHEMA = cv.Schema( | ||||
|     HOMEASSISTANT_EVENT_ACTION_SCHEMA, | ||||
| ) | ||||
| async def homeassistant_event_to_code(config, action_id, template_arg, args): | ||||
|     cg.add_define("USE_API_HOMEASSISTANT_SERVICES") | ||||
|     serv = await cg.get_variable(config[CONF_ID]) | ||||
|     var = cg.new_Pvariable(action_id, template_arg, serv, True) | ||||
|     templ = await cg.templatable(config[CONF_EVENT], args, None) | ||||
| @@ -323,9 +335,10 @@ async def api_connected_to_code(config, condition_id, template_arg, args): | ||||
|  | ||||
|  | ||||
| def FILTER_SOURCE_FILES() -> list[str]: | ||||
|     """Filter out api_pb2_dump.cpp when proto message dumping is not enabled | ||||
|     and user_services.cpp when no services are defined.""" | ||||
|     files_to_filter = [] | ||||
|     """Filter out api_pb2_dump.cpp when proto message dumping is not enabled, | ||||
|     user_services.cpp when no services are defined, and protocol-specific | ||||
|     implementations based on encryption configuration.""" | ||||
|     files_to_filter: list[str] = [] | ||||
|  | ||||
|     # 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 | ||||
| @@ -341,4 +354,16 @@ def FILTER_SOURCE_FILES() -> list[str]: | ||||
|     if config and not config.get(CONF_ACTIONS) and not config[CONF_CUSTOM_SERVICES]: | ||||
|         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 | ||||
|   | ||||
| @@ -203,7 +203,7 @@ message DeviceInfoResponse { | ||||
|   option (id) = 10; | ||||
|   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()" | ||||
|   string name = 2; | ||||
| @@ -230,14 +230,16 @@ message DeviceInfoResponse { | ||||
|  | ||||
|   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"]; | ||||
|  | ||||
|   string manufacturer = 12; | ||||
|  | ||||
|   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"]; | ||||
|  | ||||
|   string suggested_area = 16 [(field_ifdef) = "USE_AREAS"]; | ||||
| @@ -337,7 +339,9 @@ message ListEntitiesCoverResponse { | ||||
|   uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"]; | ||||
| } | ||||
|  | ||||
| // Deprecated in API version 1.1 | ||||
| enum LegacyCoverState { | ||||
|   option deprecated = true; | ||||
|   LEGACY_COVER_STATE_OPEN = 0; | ||||
|   LEGACY_COVER_STATE_CLOSED = 1; | ||||
| } | ||||
| @@ -356,7 +360,8 @@ message CoverStateResponse { | ||||
|   fixed32 key = 1; | ||||
|   // legacy: state has been removed in 1.13 | ||||
|   // 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 tilt = 4; | ||||
| @@ -364,7 +369,9 @@ message CoverStateResponse { | ||||
|   uint32 device_id = 6 [(field_ifdef) = "USE_DEVICES"]; | ||||
| } | ||||
|  | ||||
| // Deprecated in API version 1.1 | ||||
| enum LegacyCoverCommand { | ||||
|   option deprecated = true; | ||||
|   LEGACY_COVER_COMMAND_OPEN = 0; | ||||
|   LEGACY_COVER_COMMAND_CLOSE = 1; | ||||
|   LEGACY_COVER_COMMAND_STOP = 2; | ||||
| @@ -380,8 +387,10 @@ message CoverCommandRequest { | ||||
|  | ||||
|   // legacy: command has been removed in 1.13 | ||||
|   // clients/servers must still send/accept it until the next protocol change | ||||
|   bool has_legacy_command = 2; | ||||
|   LegacyCoverCommand legacy_command = 3; | ||||
|   // Deprecated in API version 1.1 | ||||
|   bool has_legacy_command = 2 [deprecated=true]; | ||||
|   // Deprecated in API version 1.1 | ||||
|   LegacyCoverCommand legacy_command = 3 [deprecated=true]; | ||||
|  | ||||
|   bool has_position = 4; | ||||
|   float position = 5; | ||||
| @@ -413,7 +422,9 @@ message ListEntitiesFanResponse { | ||||
|   repeated string supported_preset_modes = 12; | ||||
|   uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"]; | ||||
| } | ||||
| // Deprecated in API version 1.6 - only used in deprecated fields | ||||
| enum FanSpeed { | ||||
|   option deprecated = true; | ||||
|   FAN_SPEED_LOW = 0; | ||||
|   FAN_SPEED_MEDIUM = 1; | ||||
|   FAN_SPEED_HIGH = 2; | ||||
| @@ -432,7 +443,8 @@ message FanStateResponse { | ||||
|   fixed32 key = 1; | ||||
|   bool state = 2; | ||||
|   bool oscillating = 3; | ||||
|   FanSpeed speed = 4 [deprecated = true]; | ||||
|   // Deprecated in API version 1.6 | ||||
|   FanSpeed speed = 4 [deprecated=true]; | ||||
|   FanDirection direction = 5; | ||||
|   int32 speed_level = 6; | ||||
|   string preset_mode = 7; | ||||
| @@ -448,8 +460,10 @@ message FanCommandRequest { | ||||
|   fixed32 key = 1; | ||||
|   bool has_state = 2; | ||||
|   bool state = 3; | ||||
|   bool has_speed = 4 [deprecated = true]; | ||||
|   FanSpeed speed = 5 [deprecated = true]; | ||||
|   // Deprecated in API version 1.6 | ||||
|   bool has_speed = 4 [deprecated=true]; | ||||
|   // Deprecated in API version 1.6 | ||||
|   FanSpeed speed = 5 [deprecated=true]; | ||||
|   bool has_oscillating = 6; | ||||
|   bool oscillating = 7; | ||||
|   bool has_direction = 8; | ||||
| @@ -488,9 +502,13 @@ message ListEntitiesLightResponse { | ||||
|  | ||||
|   repeated ColorMode supported_color_modes = 12; | ||||
|   // 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]; | ||||
|   // Deprecated in API version 1.6 | ||||
|   bool legacy_supports_rgb = 6 [deprecated=true]; | ||||
|   // Deprecated in API version 1.6 | ||||
|   bool legacy_supports_white_value = 7 [deprecated=true]; | ||||
|   // Deprecated in API version 1.6 | ||||
|   bool legacy_supports_color_temperature = 8 [deprecated=true]; | ||||
|   float min_mireds = 9; | ||||
|   float max_mireds = 10; | ||||
| @@ -567,7 +585,9 @@ enum SensorStateClass { | ||||
|   STATE_CLASS_TOTAL = 3; | ||||
| } | ||||
|  | ||||
| // Deprecated in API version 1.5 | ||||
| enum SensorLastResetType { | ||||
|   option deprecated = true; | ||||
|   LAST_RESET_NONE = 0; | ||||
|   LAST_RESET_NEVER = 1; | ||||
|   LAST_RESET_AUTO = 2; | ||||
| @@ -591,7 +611,8 @@ message ListEntitiesSensorResponse { | ||||
|   string device_class = 9; | ||||
|   SensorStateClass state_class = 10; | ||||
|   // 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; | ||||
|   EntityCategory entity_category = 13; | ||||
|   uint32 device_id = 14 [(field_ifdef) = "USE_DEVICES"]; | ||||
| @@ -711,7 +732,6 @@ message SubscribeLogsResponse { | ||||
|  | ||||
|   LogLevel level = 1; | ||||
|   bytes message = 3; | ||||
|   bool send_failed = 4; | ||||
| } | ||||
|  | ||||
| // ==================== NOISE ENCRYPTION ==================== | ||||
| @@ -735,17 +755,19 @@ message NoiseEncryptionSetKeyResponse { | ||||
| message SubscribeHomeassistantServicesRequest { | ||||
|   option (id) = 34; | ||||
|   option (source) = SOURCE_CLIENT; | ||||
|   option (ifdef) = "USE_API_HOMEASSISTANT_SERVICES"; | ||||
| } | ||||
|  | ||||
| message HomeassistantServiceMap { | ||||
|   string key = 1; | ||||
|   string value = 2; | ||||
|   string value = 2 [(no_zero_copy) = true]; | ||||
| } | ||||
|  | ||||
| message HomeassistantServiceResponse { | ||||
|   option (id) = 35; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (no_delay) = true; | ||||
|   option (ifdef) = "USE_API_HOMEASSISTANT_SERVICES"; | ||||
|  | ||||
|   string service = 1; | ||||
|   repeated HomeassistantServiceMap data = 2; | ||||
| @@ -761,11 +783,13 @@ message HomeassistantServiceResponse { | ||||
| message SubscribeHomeAssistantStatesRequest { | ||||
|   option (id) = 38; | ||||
|   option (source) = SOURCE_CLIENT; | ||||
|   option (ifdef) = "USE_API_HOMEASSISTANT_STATES"; | ||||
| } | ||||
|  | ||||
| message SubscribeHomeAssistantStateResponse { | ||||
|   option (id) = 39; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_API_HOMEASSISTANT_STATES"; | ||||
|   string entity_id = 1; | ||||
|   string attribute = 2; | ||||
|   bool once = 3; | ||||
| @@ -775,6 +799,7 @@ message HomeAssistantStateResponse { | ||||
|   option (id) = 40; | ||||
|   option (source) = SOURCE_CLIENT; | ||||
|   option (no_delay) = true; | ||||
|   option (ifdef) = "USE_API_HOMEASSISTANT_STATES"; | ||||
|  | ||||
|   string entity_id = 1; | ||||
|   string state = 2; | ||||
| @@ -947,7 +972,8 @@ message ListEntitiesClimateResponse { | ||||
|   float visual_target_temperature_step = 10; | ||||
|   // for older peer versions - in new system this | ||||
|   // 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; | ||||
|   repeated ClimateFanMode supported_fan_modes = 13; | ||||
|   repeated ClimateSwingMode supported_swing_modes = 14; | ||||
| @@ -978,7 +1004,8 @@ message ClimateStateResponse { | ||||
|   float target_temperature_low = 5; | ||||
|   float target_temperature_high = 6; | ||||
|   // 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; | ||||
|   ClimateFanMode fan_mode = 9; | ||||
|   ClimateSwingMode swing_mode = 10; | ||||
| @@ -1006,8 +1033,10 @@ message ClimateCommandRequest { | ||||
|   bool has_target_temperature_high = 8; | ||||
|   float target_temperature_high = 9; | ||||
|   // legacy, for older peers, newer ones should use CLIMATE_PRESET_AWAY in preset | ||||
|   bool unused_has_legacy_away = 10; | ||||
|   bool unused_legacy_away = 11; | ||||
|   // Deprecated in API version 1.5 | ||||
|   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; | ||||
|   ClimateFanMode fan_mode = 13; | ||||
|   bool has_swing_mode = 14; | ||||
| @@ -1354,12 +1383,17 @@ message SubscribeBluetoothLEAdvertisementsRequest { | ||||
|   uint32 flags = 1; | ||||
| } | ||||
|  | ||||
| // Deprecated - only used by deprecated BluetoothLEAdvertisementResponse | ||||
| message BluetoothServiceData { | ||||
|   option deprecated = true; | ||||
|   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 | ||||
| } | ||||
| // Removed in ESPHome 2025.8.0 - use BluetoothLERawAdvertisementsResponse instead | ||||
| message BluetoothLEAdvertisementResponse { | ||||
|   option deprecated = true; | ||||
|   option (id) = 67; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_BLUETOOTH_PROXY"; | ||||
| @@ -1381,7 +1415,7 @@ message BluetoothLERawAdvertisement { | ||||
|   sint32 rssi = 2; | ||||
|   uint32 address_type = 3; | ||||
|  | ||||
|   bytes data = 4; | ||||
|   bytes data = 4 [(fixed_array_size) = 62]; | ||||
| } | ||||
|  | ||||
| message BluetoothLERawAdvertisementsResponse { | ||||
| @@ -1434,19 +1468,19 @@ message BluetoothGATTGetServicesRequest { | ||||
| } | ||||
|  | ||||
| message BluetoothGATTDescriptor { | ||||
|   repeated uint64 uuid = 1; | ||||
|   repeated uint64 uuid = 1 [(fixed_array_size) = 2]; | ||||
|   uint32 handle = 2; | ||||
| } | ||||
|  | ||||
| message BluetoothGATTCharacteristic { | ||||
|   repeated uint64 uuid = 1; | ||||
|   repeated uint64 uuid = 1 [(fixed_array_size) = 2]; | ||||
|   uint32 handle = 2; | ||||
|   uint32 properties = 3; | ||||
|   repeated BluetoothGATTDescriptor descriptors = 4; | ||||
| } | ||||
|  | ||||
| message BluetoothGATTService { | ||||
|   repeated uint64 uuid = 1; | ||||
|   repeated uint64 uuid = 1 [(fixed_array_size) = 2]; | ||||
|   uint32 handle = 2; | ||||
|   repeated BluetoothGATTCharacteristic characteristics = 3; | ||||
| } | ||||
| @@ -1457,7 +1491,7 @@ message BluetoothGATTGetServicesResponse { | ||||
|   option (ifdef) = "USE_BLUETOOTH_PROXY"; | ||||
|  | ||||
|   uint64 address = 1; | ||||
|   repeated BluetoothGATTService services = 2; | ||||
|   repeated BluetoothGATTService services = 2 [(fixed_array_size) = 1]; | ||||
| } | ||||
|  | ||||
| message BluetoothGATTGetServicesDoneResponse { | ||||
|   | ||||
| @@ -1,5 +1,11 @@ | ||||
| #include "api_connection.h" | ||||
| #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 <cinttypes> | ||||
| #include <utility> | ||||
| @@ -25,8 +31,7 @@ | ||||
| #include "esphome/components/voice_assistant/voice_assistant.h" | ||||
| #endif | ||||
|  | ||||
| namespace esphome { | ||||
| namespace api { | ||||
| namespace esphome::api { | ||||
|  | ||||
| // 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. | ||||
| @@ -79,14 +84,16 @@ APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *pa | ||||
| #if defined(USE_API_PLAINTEXT) && defined(USE_API_NOISE) | ||||
|   auto noise_ctx = parent->get_noise_ctx(); | ||||
|   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 { | ||||
|     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) | ||||
|   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) | ||||
|   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 | ||||
| #error "No frame helper defined" | ||||
| #endif | ||||
| @@ -109,9 +116,8 @@ void APIConnection::start() { | ||||
|              errno); | ||||
|     return; | ||||
|   } | ||||
|   this->client_info_ = helper_->getpeername(); | ||||
|   this->client_peername_ = this->client_info_; | ||||
|   this->helper_->set_log_info(this->client_info_); | ||||
|   this->client_info_.peername = helper_->getpeername(); | ||||
|   this->client_info_.name = this->client_info_.peername; | ||||
| } | ||||
|  | ||||
| APIConnection::~APIConnection() { | ||||
| @@ -218,24 +224,16 @@ void APIConnection::loop() { | ||||
|   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()); | ||||
|     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); | ||||
|     // fixed32 key = 1; | ||||
|     buffer.encode_fixed32(1, camera::Camera::instance()->get_object_id_hash()); | ||||
|     // bytes data = 2; | ||||
|     buffer.encode_bytes(2, this->image_reader_->peek_data_buffer(), to_send); | ||||
|     // bool done = 3; | ||||
|     buffer.encode_bool(3, done); | ||||
|     CameraImageResponse msg; | ||||
|     msg.key = camera::Camera::instance()->get_object_id_hash(); | ||||
|     msg.set_data(this->image_reader_->peek_data_buffer(), to_send); | ||||
|     msg.done = done; | ||||
| #ifdef USE_DEVICES | ||||
|     msg.device_id = camera::Camera::instance()->get_device_id(); | ||||
| #endif | ||||
|  | ||||
|     bool success = this->send_buffer(buffer, CameraImageResponse::MESSAGE_TYPE); | ||||
|  | ||||
|     if (success) { | ||||
|     if (this->send_message_(msg, CameraImageResponse::MESSAGE_TYPE)) { | ||||
|       this->image_reader_->consume_data(to_send); | ||||
|       if (done) { | ||||
|         this->image_reader_->return_image(); | ||||
| @@ -244,31 +242,21 @@ void APIConnection::loop() { | ||||
|   } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_API_HOMEASSISTANT_STATES | ||||
|   if (state_subs_at_ >= 0) { | ||||
|     const auto &subs = this->parent_->get_state_subs(); | ||||
|     if (state_subs_at_ < static_cast<int>(subs.size())) { | ||||
|       auto &it = subs[state_subs_at_]; | ||||
|       SubscribeHomeAssistantStateResponse resp; | ||||
|       resp.entity_id = it.entity_id; | ||||
|       resp.attribute = it.attribute.value(); | ||||
|       resp.once = it.once; | ||||
|       if (this->send_message(resp, SubscribeHomeAssistantStateResponse::MESSAGE_TYPE)) { | ||||
|         state_subs_at_++; | ||||
|       } | ||||
|     } else { | ||||
|       state_subs_at_ = -1; | ||||
|     } | ||||
|     this->process_state_subscriptions_(); | ||||
|   } | ||||
| #endif | ||||
| } | ||||
|  | ||||
| DisconnectResponse APIConnection::disconnect(const DisconnectRequest &msg) { | ||||
| bool APIConnection::send_disconnect_response(const DisconnectRequest &msg) { | ||||
|   // remote initiated disconnect_client | ||||
|   // don't close yet, we still need to send the disconnect response | ||||
|   // close will happen on next loop | ||||
|   ESP_LOGD(TAG, "%s disconnected", this->get_client_combined_info().c_str()); | ||||
|   this->flags_.next_close = true; | ||||
|   DisconnectResponse resp; | ||||
|   return resp; | ||||
|   return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE); | ||||
| } | ||||
| void APIConnection::on_disconnect_response(const DisconnectResponse &value) { | ||||
|   this->helper_->close(); | ||||
| @@ -345,7 +333,7 @@ uint16_t APIConnection::try_send_binary_sensor_info(EntityBase *entity, APIConne | ||||
|                                                     bool is_single) { | ||||
|   auto *binary_sensor = static_cast<binary_sensor::BinarySensor *>(entity); | ||||
|   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(); | ||||
|   return fill_and_encode_entity_info(binary_sensor, msg, ListEntitiesBinarySensorResponse::MESSAGE_TYPE, conn, | ||||
|                                      remaining_size, is_single); | ||||
| @@ -362,8 +350,6 @@ uint16_t APIConnection::try_send_cover_state(EntityBase *entity, APIConnection * | ||||
|   auto *cover = static_cast<cover::Cover *>(entity); | ||||
|   CoverStateResponse msg; | ||||
|   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; | ||||
|   if (traits.get_supports_tilt()) | ||||
|     msg.tilt = cover->tilt; | ||||
| @@ -379,25 +365,12 @@ uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *c | ||||
|   msg.supports_position = traits.get_supports_position(); | ||||
|   msg.supports_tilt = traits.get_supports_tilt(); | ||||
|   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, | ||||
|                                      is_single); | ||||
| } | ||||
| void APIConnection::cover_command(const CoverCommandRequest &msg) { | ||||
|   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) | ||||
|     call.set_position(msg.position); | ||||
|   if (msg.has_tilt) | ||||
| @@ -427,7 +400,7 @@ uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *co | ||||
|   if (traits.supports_direction()) | ||||
|     msg.direction = static_cast<enums::FanDirection>(fan->direction); | ||||
|   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); | ||||
| } | ||||
| uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, | ||||
| @@ -484,8 +457,11 @@ uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection * | ||||
|   resp.color_temperature = values.get_color_temperature(); | ||||
|   resp.cold_white = values.get_cold_white(); | ||||
|   resp.warm_white = values.get_warm_white(); | ||||
|   if (light->supports_effects()) | ||||
|     resp.effect = light->get_effect_name(); | ||||
|   if (light->supports_effects()) { | ||||
|     // 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); | ||||
| } | ||||
| uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, | ||||
| @@ -495,14 +471,8 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c | ||||
|   auto traits = light->get_traits(); | ||||
|   for (auto mode : traits.get_supported_color_modes()) | ||||
|     msg.supported_color_modes.push_back(static_cast<enums::ColorMode>(mode)); | ||||
|   msg.legacy_supports_brightness = traits.supports_color_capability(light::ColorCapability::BRIGHTNESS); | ||||
|   msg.legacy_supports_rgb = traits.supports_color_capability(light::ColorCapability::RGB); | ||||
|   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) { | ||||
|   if (traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) || | ||||
|       traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE)) { | ||||
|     msg.min_mireds = traits.get_min_mireds(); | ||||
|     msg.max_mireds = traits.get_max_mireds(); | ||||
|   } | ||||
| @@ -567,10 +537,10 @@ uint16_t APIConnection::try_send_sensor_info(EntityBase *entity, APIConnection * | ||||
|                                              bool is_single) { | ||||
|   auto *sensor = static_cast<sensor::Sensor *>(entity); | ||||
|   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.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()); | ||||
|   return fill_and_encode_entity_info(sensor, msg, ListEntitiesSensorResponse::MESSAGE_TYPE, conn, remaining_size, | ||||
|                                      is_single); | ||||
| @@ -597,7 +567,7 @@ uint16_t APIConnection::try_send_switch_info(EntityBase *entity, APIConnection * | ||||
|   auto *a_switch = static_cast<switch_::Switch *>(entity); | ||||
|   ListEntitiesSwitchResponse msg; | ||||
|   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, | ||||
|                                      is_single); | ||||
| } | ||||
| @@ -622,7 +592,7 @@ uint16_t APIConnection::try_send_text_sensor_state(EntityBase *entity, APIConnec | ||||
|                                                    bool is_single) { | ||||
|   auto *text_sensor = static_cast<text_sensor::TextSensor *>(entity); | ||||
|   TextSensorStateResponse resp; | ||||
|   resp.state = text_sensor->state; | ||||
|   resp.set_state(StringRef(text_sensor->state)); | ||||
|   resp.missing_state = !text_sensor->has_state(); | ||||
|   return fill_and_encode_entity_state(text_sensor, resp, TextSensorStateResponse::MESSAGE_TYPE, conn, remaining_size, | ||||
|                                       is_single); | ||||
| @@ -631,7 +601,7 @@ uint16_t APIConnection::try_send_text_sensor_info(EntityBase *entity, APIConnect | ||||
|                                                   bool is_single) { | ||||
|   auto *text_sensor = static_cast<text_sensor::TextSensor *>(entity); | ||||
|   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, | ||||
|                                      remaining_size, is_single); | ||||
| } | ||||
| @@ -659,13 +629,15 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection | ||||
|   } | ||||
|   if (traits.get_supports_fan_modes() && climate->fan_mode.has_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()) | ||||
|     resp.custom_fan_mode = climate->custom_fan_mode.value(); | ||||
|   if (!traits.get_supported_custom_fan_modes().empty() && climate->custom_fan_mode.has_value()) { | ||||
|     resp.set_custom_fan_mode(StringRef(climate->custom_fan_mode.value())); | ||||
|   } | ||||
|   if (traits.get_supports_presets() && climate->preset.has_value()) { | ||||
|     resp.preset = static_cast<enums::ClimatePreset>(climate->preset.value()); | ||||
|   } | ||||
|   if (!traits.get_supported_custom_presets().empty() && climate->custom_preset.has_value()) | ||||
|     resp.custom_preset = climate->custom_preset.value(); | ||||
|   if (!traits.get_supported_custom_presets().empty() && climate->custom_preset.has_value()) { | ||||
|     resp.set_custom_preset(StringRef(climate->custom_preset.value())); | ||||
|   } | ||||
|   if (traits.get_supports_swing_modes()) | ||||
|     resp.swing_mode = static_cast<enums::ClimateSwingMode>(climate->swing_mode); | ||||
|   if (traits.get_supports_current_humidity()) | ||||
| @@ -692,7 +664,6 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection | ||||
|   msg.visual_current_temperature_step = traits.get_visual_current_temperature_step(); | ||||
|   msg.visual_min_humidity = traits.get_visual_min_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(); | ||||
|   for (auto fan_mode : traits.get_supported_fan_modes()) | ||||
|     msg.supported_fan_modes.push_back(static_cast<enums::ClimateFanMode>(fan_mode)); | ||||
| @@ -752,9 +723,9 @@ uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection * | ||||
|                                              bool is_single) { | ||||
|   auto *number = static_cast<number::Number *>(entity); | ||||
|   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.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.max_value = number->traits.get_max_value(); | ||||
|   msg.step = number->traits.get_step(); | ||||
| @@ -867,7 +838,7 @@ uint16_t APIConnection::try_send_text_state(EntityBase *entity, APIConnection *c | ||||
|                                             bool is_single) { | ||||
|   auto *text = static_cast<text::Text *>(entity); | ||||
|   TextStateResponse resp; | ||||
|   resp.state = text->state; | ||||
|   resp.set_state(StringRef(text->state)); | ||||
|   resp.missing_state = !text->has_state(); | ||||
|   return fill_and_encode_entity_state(text, resp, TextStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); | ||||
| } | ||||
| @@ -879,7 +850,7 @@ uint16_t APIConnection::try_send_text_info(EntityBase *entity, APIConnection *co | ||||
|   msg.mode = static_cast<enums::TextMode>(text->traits.get_mode()); | ||||
|   msg.min_length = text->traits.get_min_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, | ||||
|                                      is_single); | ||||
| } | ||||
| @@ -900,7 +871,7 @@ uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection | ||||
|                                               bool is_single) { | ||||
|   auto *select = static_cast<select::Select *>(entity); | ||||
|   SelectStateResponse resp; | ||||
|   resp.state = select->state; | ||||
|   resp.set_state(StringRef(select->state)); | ||||
|   resp.missing_state = !select->has_state(); | ||||
|   return fill_and_encode_entity_state(select, resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); | ||||
| } | ||||
| @@ -926,7 +897,7 @@ uint16_t APIConnection::try_send_button_info(EntityBase *entity, APIConnection * | ||||
|                                              bool is_single) { | ||||
|   auto *button = static_cast<button::Button *>(entity); | ||||
|   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, | ||||
|                                      is_single); | ||||
| } | ||||
| @@ -995,7 +966,7 @@ uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *c | ||||
|   auto *valve = static_cast<valve::Valve *>(entity); | ||||
|   ListEntitiesValveResponse msg; | ||||
|   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.supports_position = traits.get_supports_position(); | ||||
|   msg.supports_stop = traits.get_supports_stop(); | ||||
| @@ -1037,13 +1008,13 @@ uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnec | ||||
|   auto traits = media_player->get_traits(); | ||||
|   msg.supports_pause = traits.get_supports_pause(); | ||||
|   for (auto &supported_format : traits.get_supported_formats()) { | ||||
|     MediaPlayerSupportedFormat media_format; | ||||
|     media_format.format = supported_format.format; | ||||
|     msg.supported_formats.emplace_back(); | ||||
|     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.num_channels = supported_format.num_channels; | ||||
|     media_format.purpose = static_cast<enums::MediaPlayerFormatPurpose>(supported_format.purpose); | ||||
|     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, | ||||
|                                      remaining_size, is_single); | ||||
| @@ -1106,6 +1077,12 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) { | ||||
| } | ||||
| #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 | ||||
| void APIConnection::subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) { | ||||
|   bluetooth_proxy::global_bluetooth_proxy->subscribe_api_connection(this, msg.flags); | ||||
| @@ -1113,21 +1090,6 @@ void APIConnection::subscribe_bluetooth_le_advertisements(const SubscribeBluetoo | ||||
| void APIConnection::unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) { | ||||
|   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) { | ||||
|   bluetooth_proxy::global_bluetooth_proxy->bluetooth_device_request(msg); | ||||
| } | ||||
| @@ -1151,12 +1113,12 @@ void APIConnection::bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &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) { | ||||
|   BluetoothConnectionsFreeResponse resp; | ||||
|   resp.free = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_connections_free(); | ||||
|   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) { | ||||
| @@ -1217,28 +1179,27 @@ void APIConnection::on_voice_assistant_announce_request(const VoiceAssistantAnno | ||||
|   } | ||||
| } | ||||
|  | ||||
| VoiceAssistantConfigurationResponse APIConnection::voice_assistant_get_configuration( | ||||
|     const VoiceAssistantConfigurationRequest &msg) { | ||||
| bool APIConnection::send_voice_assistant_get_configuration_response(const VoiceAssistantConfigurationRequest &msg) { | ||||
|   VoiceAssistantConfigurationResponse resp; | ||||
|   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(); | ||||
|   for (auto &wake_word : config.available_wake_words) { | ||||
|     VoiceAssistantWakeWord resp_wake_word; | ||||
|     resp_wake_word.id = wake_word.id; | ||||
|     resp_wake_word.wake_word = wake_word.wake_word; | ||||
|     resp.available_wake_words.emplace_back(); | ||||
|     auto &resp_wake_word = resp.available_wake_words.back(); | ||||
|     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) { | ||||
|       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) { | ||||
|     resp.active_wake_words.push_back(wake_word_id); | ||||
|   } | ||||
|   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) { | ||||
| @@ -1311,7 +1272,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, | ||||
|                                                 uint32_t remaining_size, bool is_single) { | ||||
|   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); | ||||
| } | ||||
|  | ||||
| @@ -1319,7 +1280,7 @@ uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *c | ||||
|                                             bool is_single) { | ||||
|   auto *event = static_cast<event::Event *>(entity); | ||||
|   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()) | ||||
|     msg.event_types.push_back(event_type); | ||||
|   return fill_and_encode_entity_info(event, msg, ListEntitiesEventResponse::MESSAGE_TYPE, conn, remaining_size, | ||||
| @@ -1343,11 +1304,11 @@ uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection | ||||
|       resp.has_progress = true; | ||||
|       resp.progress = update->update_info.progress; | ||||
|     } | ||||
|     resp.current_version = update->update_info.current_version; | ||||
|     resp.latest_version = update->update_info.latest_version; | ||||
|     resp.title = update->update_info.title; | ||||
|     resp.release_summary = update->update_info.summary; | ||||
|     resp.release_url = update->update_info.release_url; | ||||
|     resp.set_current_version(StringRef(update->update_info.current_version)); | ||||
|     resp.set_latest_version(StringRef(update->update_info.latest_version)); | ||||
|     resp.set_title(StringRef(update->update_info.title)); | ||||
|     resp.set_release_summary(StringRef(update->update_info.summary)); | ||||
|     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); | ||||
| } | ||||
| @@ -1355,7 +1316,7 @@ uint16_t APIConnection::try_send_update_info(EntityBase *entity, APIConnection * | ||||
|                                              bool is_single) { | ||||
|   auto *update = static_cast<update::UpdateEntity *>(entity); | ||||
|   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, | ||||
|                                      is_single); | ||||
| } | ||||
| @@ -1380,26 +1341,10 @@ void APIConnection::update_command(const UpdateCommandRequest &msg) { | ||||
| #endif | ||||
|  | ||||
| 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 | ||||
|   uint32_t msg_size = 0; | ||||
|  | ||||
|   // Add size for level field (field ID 1, varint 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); | ||||
|   SubscribeLogsResponse msg; | ||||
|   msg.level = static_cast<enums::LogLevel>(level); | ||||
|   msg.set_message(reinterpret_cast<const uint8_t *>(line), message_len); | ||||
|   return this->send_message_(msg, SubscribeLogsResponse::MESSAGE_TYPE); | ||||
| } | ||||
|  | ||||
| void APIConnection::complete_authentication_() { | ||||
| @@ -1411,7 +1356,7 @@ void APIConnection::complete_authentication_() { | ||||
|   this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::AUTHENTICATED); | ||||
|   ESP_LOGD(TAG, "%s connected", this->get_client_combined_info().c_str()); | ||||
| #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 | ||||
| #ifdef USE_HOMEASSISTANT_TIME | ||||
|   if (homeassistant::global_homeassistant_time != nullptr) { | ||||
| @@ -1420,20 +1365,21 @@ void APIConnection::complete_authentication_() { | ||||
| #endif | ||||
| } | ||||
|  | ||||
| HelloResponse APIConnection::hello(const HelloRequest &msg) { | ||||
|   this->client_info_ = msg.client_info; | ||||
|   this->client_peername_ = this->helper_->getpeername(); | ||||
|   this->helper_->set_log_info(this->get_client_combined_info()); | ||||
| bool APIConnection::send_hello_response(const HelloRequest &msg) { | ||||
|   this->client_info_.name = msg.client_info; | ||||
|   this->client_info_.peername = this->helper_->getpeername(); | ||||
|   this->client_api_version_major_ = msg.api_version_major; | ||||
|   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(), | ||||
|            this->client_peername_.c_str(), this->client_api_version_major_, this->client_api_version_minor_); | ||||
|   ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->client_info_.name.c_str(), | ||||
|            this->client_info_.peername.c_str(), this->client_api_version_major_, this->client_api_version_minor_); | ||||
|  | ||||
|   HelloResponse resp; | ||||
|   resp.api_version_major = 1; | ||||
|   resp.api_version_minor = 10; | ||||
|   resp.server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")"; | ||||
|   resp.name = App.get_name(); | ||||
|   // Temporary string for concatenation - will be valid during send_message call | ||||
|   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 | ||||
|   // Password required - wait for authentication | ||||
| @@ -1443,9 +1389,9 @@ HelloResponse APIConnection::hello(const HelloRequest &msg) { | ||||
|   this->complete_authentication_(); | ||||
| #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; | ||||
| #ifdef USE_API_PASSWORD | ||||
|   correct = this->parent_->check_password(msg.password); | ||||
| @@ -1457,54 +1403,73 @@ ConnectResponse APIConnection::connect(const ConnectRequest &msg) { | ||||
|   if (correct) { | ||||
|     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{}; | ||||
| #ifdef USE_API_PASSWORD | ||||
|   resp.uses_password = true; | ||||
| #else | ||||
|   resp.uses_password = false; | ||||
| #endif | ||||
|   resp.name = App.get_name(); | ||||
|   resp.friendly_name = App.get_friendly_name(); | ||||
|   resp.set_name(StringRef(App.get_name())); | ||||
|   resp.set_friendly_name(StringRef(App.get_friendly_name())); | ||||
| #ifdef USE_AREAS | ||||
|   resp.suggested_area = App.get_area(); | ||||
|   resp.set_suggested_area(StringRef(App.get_area())); | ||||
| #endif | ||||
|   resp.mac_address = get_mac_address_pretty(); | ||||
|   resp.esphome_version = ESPHOME_VERSION; | ||||
|   resp.compilation_time = App.get_compilation_time(); | ||||
|   // mac_address must store temporary string - will be valid during send_message call | ||||
|   std::string mac_address = get_mac_address_pretty(); | ||||
|   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) | ||||
|   resp.manufacturer = "Espressif"; | ||||
|   static constexpr auto MANUFACTURER = StringRef::from_lit("Espressif"); | ||||
| #elif defined(USE_RP2040) | ||||
|   resp.manufacturer = "Raspberry Pi"; | ||||
|   static constexpr auto MANUFACTURER = StringRef::from_lit("Raspberry Pi"); | ||||
| #elif defined(USE_BK72XX) | ||||
|   resp.manufacturer = "Beken"; | ||||
|   static constexpr auto MANUFACTURER = StringRef::from_lit("Beken"); | ||||
| #elif defined(USE_LN882X) | ||||
|   resp.manufacturer = "Lightning"; | ||||
|   static constexpr auto MANUFACTURER = StringRef::from_lit("Lightning"); | ||||
| #elif defined(USE_RTL87XX) | ||||
|   resp.manufacturer = "Realtek"; | ||||
|   static constexpr auto MANUFACTURER = StringRef::from_lit("Realtek"); | ||||
| #elif defined(USE_HOST) | ||||
|   resp.manufacturer = "Host"; | ||||
|   static constexpr auto MANUFACTURER = StringRef::from_lit("Host"); | ||||
| #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 | ||||
|   resp.has_deep_sleep = deep_sleep::global_has_deep_sleep; | ||||
| #endif | ||||
| #ifdef ESPHOME_PROJECT_NAME | ||||
|   resp.project_name = ESPHOME_PROJECT_NAME; | ||||
|   resp.project_version = ESPHOME_PROJECT_VERSION; | ||||
|   static constexpr auto PROJECT_NAME = StringRef::from_lit(ESPHOME_PROJECT_NAME); | ||||
|   static constexpr auto PROJECT_VERSION = StringRef::from_lit(ESPHOME_PROJECT_VERSION); | ||||
|   resp.set_project_name(PROJECT_NAME); | ||||
|   resp.set_project_version(PROJECT_VERSION); | ||||
| #endif | ||||
| #ifdef USE_WEBSERVER | ||||
|   resp.webserver_port = USE_WEBSERVER_PORT; | ||||
| #endif | ||||
| #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_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 | ||||
| #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(); | ||||
| #endif | ||||
| #ifdef USE_API_NOISE | ||||
| @@ -1512,23 +1477,26 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { | ||||
| #endif | ||||
| #ifdef USE_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.name = device->get_name(); | ||||
|     device_info.set_name(StringRef(device->get_name())); | ||||
|     device_info.area_id = device->get_area_id(); | ||||
|     resp.devices.push_back(device_info); | ||||
|   } | ||||
| #endif | ||||
| #ifdef USE_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.name = area->get_name(); | ||||
|     resp.areas.push_back(area_info); | ||||
|     area_info.set_name(StringRef(area->get_name())); | ||||
|   } | ||||
| #endif | ||||
|   return resp; | ||||
|  | ||||
|   return this->send_message(resp, DeviceInfoResponse::MESSAGE_TYPE); | ||||
| } | ||||
|  | ||||
| #ifdef USE_API_HOMEASSISTANT_STATES | ||||
| void APIConnection::on_home_assistant_state_response(const HomeAssistantStateResponse &msg) { | ||||
|   for (auto &it : this->parent_->get_state_subs()) { | ||||
|     if (it.entity_id == msg.entity_id && it.attribute.value() == msg.attribute) { | ||||
| @@ -1536,6 +1504,7 @@ void APIConnection::on_home_assistant_state_response(const HomeAssistantStateRes | ||||
|     } | ||||
|   } | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_API_SERVICES | ||||
| void APIConnection::execute_service(const ExecuteServiceRequest &msg) { | ||||
|   bool found = false; | ||||
| @@ -1550,28 +1519,27 @@ void APIConnection::execute_service(const ExecuteServiceRequest &msg) { | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_API_NOISE | ||||
| NoiseEncryptionSetKeyResponse APIConnection::noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) { | ||||
|   psk_t psk{}; | ||||
| bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) { | ||||
|   NoiseEncryptionSetKeyResponse resp; | ||||
|   resp.success = false; | ||||
|  | ||||
|   psk_t psk{}; | ||||
|   if (base64_decode(msg.key, psk.data(), msg.key.size()) != psk.size()) { | ||||
|     ESP_LOGW(TAG, "Invalid encryption key length"); | ||||
|     resp.success = false; | ||||
|     return resp; | ||||
|   } | ||||
|  | ||||
|   if (!this->parent_->save_noise_psk(psk, true)) { | ||||
|   } else if (!this->parent_->save_noise_psk(psk, true)) { | ||||
|     ESP_LOGW(TAG, "Failed to save encryption key"); | ||||
|     resp.success = false; | ||||
|     return resp; | ||||
|   } else { | ||||
|     resp.success = true; | ||||
|   } | ||||
|  | ||||
|   resp.success = true; | ||||
|   return resp; | ||||
|   return this->send_message(resp, NoiseEncryptionSetKeyResponse::MESSAGE_TYPE); | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_API_HOMEASSISTANT_STATES | ||||
| void APIConnection::subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) { | ||||
|   state_subs_at_ = 0; | ||||
| } | ||||
| #endif | ||||
| bool APIConnection::try_to_clear_buffer(bool log_out_of_space) { | ||||
|   if (this->flags_.remove) | ||||
|     return false; | ||||
| @@ -1609,10 +1577,12 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) { | ||||
|   // Do not set last_traffic_ on send | ||||
|   return true; | ||||
| } | ||||
| #ifdef USE_API_PASSWORD | ||||
| void APIConnection::on_unauthenticated_access() { | ||||
|   this->on_fatal_error(); | ||||
|   ESP_LOGD(TAG, "%s access without authentication", this->get_client_combined_info().c_str()); | ||||
| } | ||||
| #endif | ||||
| void APIConnection::on_no_setup_connection() { | ||||
|   this->on_fatal_error(); | ||||
|   ESP_LOGD(TAG, "%s access without full connection", this->get_client_combined_info().c_str()); | ||||
| @@ -1671,6 +1641,10 @@ ProtoWriteBuffer APIConnection::allocate_batch_message_buffer(uint16_t size) { | ||||
| } | ||||
|  | ||||
| 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()) { | ||||
|     this->flags_.batch_scheduled = false; | ||||
|     return; | ||||
| @@ -1708,9 +1682,12 @@ void APIConnection::process_batch_() { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   // Pre-allocate storage for packet info | ||||
|   std::vector<PacketInfo> packet_info; | ||||
|   packet_info.reserve(num_items); | ||||
|   size_t packets_to_process = std::min(num_items, MAX_PACKETS_PER_BATCH); | ||||
|  | ||||
|   // 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 | ||||
|   const uint8_t header_padding = this->helper_->frame_header_padding(); | ||||
| @@ -1742,8 +1719,8 @@ void APIConnection::process_batch_() { | ||||
|   // The actual message data follows after the header padding | ||||
|   uint32_t current_offset = 0; | ||||
|  | ||||
|   // Process items and encode directly to buffer | ||||
|   for (size_t i = 0; i < this->deferred_batch_.size(); i++) { | ||||
|   // Process items and encode directly to buffer (up to our limit) | ||||
|   for (size_t i = 0; i < packets_to_process; i++) { | ||||
|     const auto &item = this->deferred_batch_[i]; | ||||
|     // Try to encode message | ||||
|     // The creator will calculate overhead to determine if the message fits | ||||
| @@ -1757,7 +1734,11 @@ void APIConnection::process_batch_() { | ||||
|     // Message was encoded successfully | ||||
|     // payload_size is header_padding + actual payload size + 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 | ||||
|     items_processed++; | ||||
| @@ -1783,8 +1764,8 @@ void APIConnection::process_batch_() { | ||||
|   } | ||||
|  | ||||
|   // Send all collected packets | ||||
|   APIError err = | ||||
|       this->helper_->write_protobuf_packets(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, packet_info); | ||||
|   APIError err = this->helper_->write_protobuf_packets(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, | ||||
|                                                        std::span<const PacketInfo>(packet_info, packet_count)); | ||||
|   if (err != APIError::OK && err != APIError::WOULD_BLOCK) { | ||||
|     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), | ||||
| @@ -1844,6 +1825,27 @@ uint16_t APIConnection::try_send_ping_request(EntityBase *entity, APIConnection | ||||
|   return encode_message_to_buffer(req, PingRequest::MESSAGE_TYPE, conn, remaining_size, is_single); | ||||
| } | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
| #ifdef USE_API_HOMEASSISTANT_STATES | ||||
| void APIConnection::process_state_subscriptions_() { | ||||
|   const auto &subs = this->parent_->get_state_subs(); | ||||
|   if (this->state_subs_at_ >= static_cast<int>(subs.size())) { | ||||
|     this->state_subs_at_ = -1; | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   const auto &it = subs[this->state_subs_at_]; | ||||
|   SubscribeHomeAssistantStateResponse resp; | ||||
|   resp.set_entity_id(StringRef(it.entity_id)); | ||||
|  | ||||
|   // Avoid string copy by directly using the optional's value if it exists | ||||
|   resp.set_attribute(it.attribute.has_value() ? StringRef(it.attribute.value()) : StringRef("")); | ||||
|  | ||||
|   resp.once = it.once; | ||||
|   if (this->send_message(resp, SubscribeHomeAssistantStateResponse::MESSAGE_TYPE)) { | ||||
|     this->state_subs_at_++; | ||||
|   } | ||||
| } | ||||
| #endif  // USE_API_HOMEASSISTANT_STATES | ||||
|  | ||||
| }  // namespace esphome::api | ||||
| #endif | ||||
|   | ||||
| @@ -13,13 +13,36 @@ | ||||
| #include <vector> | ||||
| #include <functional> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace api { | ||||
| namespace esphome::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 | ||||
| static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000; | ||||
| // 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 { | ||||
|  public: | ||||
| @@ -108,15 +131,16 @@ class APIConnection : public APIServerConnection { | ||||
|   void media_player_command(const MediaPlayerCommandRequest &msg) override; | ||||
| #endif | ||||
|   bool try_send_log_message(int level, const char *tag, const char *line, size_t message_len); | ||||
| #ifdef USE_API_HOMEASSISTANT_SERVICES | ||||
|   void send_homeassistant_service_call(const HomeassistantServiceResponse &call) { | ||||
|     if (!this->flags_.service_call_subscription) | ||||
|       return; | ||||
|     this->send_message(call, HomeassistantServiceResponse::MESSAGE_TYPE); | ||||
|   } | ||||
| #endif | ||||
| #ifdef USE_BLUETOOTH_PROXY | ||||
|   void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &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_gatt_read(const BluetoothGATTReadRequest &msg) override; | ||||
| @@ -125,8 +149,7 @@ class APIConnection : public APIServerConnection { | ||||
|   void bluetooth_gatt_write_descriptor(const BluetoothGATTWriteDescriptorRequest &msg) override; | ||||
|   void bluetooth_gatt_get_services(const BluetoothGATTGetServicesRequest &msg) override; | ||||
|   void bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) override; | ||||
|   BluetoothConnectionsFreeResponse subscribe_bluetooth_connections_free( | ||||
|       const SubscribeBluetoothConnectionsFreeRequest &msg) override; | ||||
|   bool send_subscribe_bluetooth_connections_free_response(const SubscribeBluetoothConnectionsFreeRequest &msg) override; | ||||
|   void bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) override; | ||||
|  | ||||
| #endif | ||||
| @@ -144,8 +167,7 @@ class APIConnection : public APIServerConnection { | ||||
|   void on_voice_assistant_audio(const VoiceAssistantAudio &msg) override; | ||||
|   void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &msg) override; | ||||
|   void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &msg) override; | ||||
|   VoiceAssistantConfigurationResponse voice_assistant_get_configuration( | ||||
|       const VoiceAssistantConfigurationRequest &msg) override; | ||||
|   bool send_voice_assistant_get_configuration_response(const VoiceAssistantConfigurationRequest &msg) override; | ||||
|   void voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) override; | ||||
| #endif | ||||
|  | ||||
| @@ -168,15 +190,17 @@ class APIConnection : public APIServerConnection { | ||||
|     // we initiated ping | ||||
|     this->flags_.sent_ping = false; | ||||
|   } | ||||
| #ifdef USE_API_HOMEASSISTANT_STATES | ||||
|   void on_home_assistant_state_response(const HomeAssistantStateResponse &msg) override; | ||||
| #endif | ||||
| #ifdef USE_HOMEASSISTANT_TIME | ||||
|   void on_get_time_response(const GetTimeResponse &value) override; | ||||
| #endif | ||||
|   HelloResponse hello(const HelloRequest &msg) override; | ||||
|   ConnectResponse connect(const ConnectRequest &msg) override; | ||||
|   DisconnectResponse disconnect(const DisconnectRequest &msg) override; | ||||
|   PingResponse ping(const PingRequest &msg) override { return {}; } | ||||
|   DeviceInfoResponse device_info(const DeviceInfoRequest &msg) override; | ||||
|   bool send_hello_response(const HelloRequest &msg) override; | ||||
|   bool send_connect_response(const ConnectRequest &msg) override; | ||||
|   bool send_disconnect_response(const DisconnectRequest &msg) override; | ||||
|   bool send_ping_response(const PingRequest &msg) override; | ||||
|   bool send_device_info_response(const DeviceInfoRequest &msg) override; | ||||
|   void list_entities(const ListEntitiesRequest &msg) override { this->list_entities_iterator_.begin(); } | ||||
|   void subscribe_states(const SubscribeStatesRequest &msg) override { | ||||
|     this->flags_.state_subscription = true; | ||||
| @@ -187,19 +211,20 @@ class APIConnection : public APIServerConnection { | ||||
|     if (msg.dump_config) | ||||
|       App.schedule_dump_config(); | ||||
|   } | ||||
| #ifdef USE_API_HOMEASSISTANT_SERVICES | ||||
|   void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) override { | ||||
|     this->flags_.service_call_subscription = true; | ||||
|   } | ||||
| #endif | ||||
| #ifdef USE_API_HOMEASSISTANT_STATES | ||||
|   void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) override; | ||||
|   GetTimeResponse get_time(const GetTimeRequest &msg) override { | ||||
|     // TODO | ||||
|     return {}; | ||||
|   } | ||||
| #endif | ||||
|   bool send_get_time_response(const GetTimeRequest &msg) override; | ||||
| #ifdef USE_API_SERVICES | ||||
|   void execute_service(const ExecuteServiceRequest &msg) override; | ||||
| #endif | ||||
| #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 | ||||
|  | ||||
|   bool is_authenticated() override { | ||||
| @@ -211,7 +236,9 @@ class APIConnection : public APIServerConnection { | ||||
|   } | ||||
|   uint8_t get_log_subscription_level() const { return this->flags_.log_subscription; } | ||||
|   void on_fatal_error() override; | ||||
| #ifdef USE_API_PASSWORD | ||||
|   void on_unauthenticated_access() override; | ||||
| #endif | ||||
|   void on_no_setup_connection() override; | ||||
|   ProtoWriteBuffer create_buffer(uint32_t reserve_size) override { | ||||
|     // FIXME: ensure no recursive writes can happen | ||||
| @@ -261,13 +288,7 @@ class APIConnection : public APIServerConnection { | ||||
|   bool try_to_clear_buffer(bool log_out_of_space); | ||||
|   bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override; | ||||
|  | ||||
|   std::string get_client_combined_info() const { | ||||
|     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_ + ")"; | ||||
|   } | ||||
|   std::string get_client_combined_info() const { return this->client_info_.get_combined_info(); } | ||||
|  | ||||
|   // Buffer allocator methods for batch processing | ||||
|   ProtoWriteBuffer allocate_single_message_buffer(uint16_t size); | ||||
| @@ -277,6 +298,10 @@ class APIConnection : public APIServerConnection { | ||||
|   // Helper function to handle authentication completion | ||||
|   void complete_authentication_(); | ||||
|  | ||||
| #ifdef USE_API_HOMEASSISTANT_STATES | ||||
|   void process_state_subscriptions_(); | ||||
| #endif | ||||
|  | ||||
|   // Non-template helper to encode any ProtoMessage | ||||
|   static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint8_t message_type, APIConnection *conn, | ||||
|                                            uint32_t remaining_size, bool is_single); | ||||
| @@ -296,13 +321,18 @@ class APIConnection : public APIServerConnection { | ||||
|                                               APIConnection *conn, uint32_t remaining_size, bool is_single) { | ||||
|     // Set common fields that are shared by all entity types | ||||
|     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()) | ||||
|       msg.name = entity->get_name(); | ||||
|     if (entity->has_own_name()) { | ||||
|       msg.set_name(entity->get_name()); | ||||
|     } | ||||
|  | ||||
|     // 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.entity_category = static_cast<enums::EntityCategory>(entity->get_entity_category()); | ||||
| #ifdef USE_DEVICES | ||||
| @@ -471,13 +501,14 @@ class APIConnection : public APIServerConnection { | ||||
|   std::unique_ptr<camera::CameraImageReader> image_reader_; | ||||
| #endif | ||||
|  | ||||
|   // Group 3: Strings (12 bytes each on 32-bit, 4-byte aligned) | ||||
|   std::string client_info_; | ||||
|   std::string client_peername_; | ||||
|   // Group 3: Client info struct (24 bytes on 32-bit: 2 strings × 12 bytes each) | ||||
|   ClientInfo client_info_; | ||||
|  | ||||
|   // Group 4: 4-byte types | ||||
|   uint32_t last_traffic_; | ||||
| #ifdef USE_API_HOMEASSISTANT_STATES | ||||
|   int state_subs_at_ = -1; | ||||
| #endif | ||||
|  | ||||
|   // Function pointer type for message encoding | ||||
|   using MessageCreatorPtr = uint16_t (*)(EntityBase *, APIConnection *, uint32_t remaining_size, bool is_single); | ||||
| @@ -707,6 +738,5 @@ class APIConnection : public APIServerConnection { | ||||
|   } | ||||
| }; | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
| }  // namespace esphome::api | ||||
| #endif | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -8,16 +8,17 @@ | ||||
|  | ||||
| #include "esphome/core/defines.h" | ||||
| #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/core/application.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace api { | ||||
| namespace esphome::api { | ||||
|  | ||||
| // uncomment to log raw packets | ||||
| //#define HELPER_LOG_PACKETS | ||||
|  | ||||
| // Forward declaration | ||||
| struct ClientInfo; | ||||
|  | ||||
| class ProtoWriteBuffer; | ||||
|  | ||||
| @@ -40,7 +41,6 @@ struct PacketInfo { | ||||
| enum class APIError : uint16_t { | ||||
|   OK = 0, | ||||
|   WOULD_BLOCK = 1001, | ||||
|   BAD_HANDSHAKE_PACKET_LEN = 1002, | ||||
|   BAD_INDICATOR = 1003, | ||||
|   BAD_DATA_PACKET = 1004, | ||||
|   TCP_NODELAY_FAILED = 1005, | ||||
| @@ -51,16 +51,19 @@ enum class APIError : uint16_t { | ||||
|   BAD_ARG = 1010, | ||||
|   SOCKET_READ_FAILED = 1011, | ||||
|   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_WRITE_FAILED = 1014, | ||||
|   HANDSHAKESTATE_BAD_STATE = 1015, | ||||
|   CIPHERSTATE_DECRYPT_FAILED = 1016, | ||||
|   CIPHERSTATE_ENCRYPT_FAILED = 1017, | ||||
|   OUT_OF_MEMORY = 1018, | ||||
|   HANDSHAKESTATE_SETUP_FAILED = 1019, | ||||
|   HANDSHAKESTATE_SPLIT_FAILED = 1020, | ||||
|   BAD_HANDSHAKE_ERROR_BYTE = 1021, | ||||
|   CONNECTION_CLOSED = 1022, | ||||
| #endif | ||||
| }; | ||||
|  | ||||
| const char *api_error_to_str(APIError err); | ||||
| @@ -68,7 +71,8 @@ const char *api_error_to_str(APIError err); | ||||
| class APIFrameHelper { | ||||
|  public: | ||||
|   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(); | ||||
|   } | ||||
|   virtual ~APIFrameHelper() = default; | ||||
| @@ -94,8 +98,6 @@ class APIFrameHelper { | ||||
|     } | ||||
|     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; | ||||
|   // Write multiple protobuf packets in a single operation | ||||
|   // 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(); } | ||||
|  | ||||
|  protected: | ||||
|   // Struct for holding parsed frame data | ||||
|   struct ParsedFrame { | ||||
|     std::vector<uint8_t> msg; | ||||
|   }; | ||||
|  | ||||
|   // Buffer containing data to be sent | ||||
|   struct SendBuffer { | ||||
|     std::vector<uint8_t> data; | ||||
|     uint16_t offset{0};  // Current offset within the buffer (uint16_t to reduce memory usage) | ||||
|     std::unique_ptr<uint8_t[]> data; | ||||
|     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 | ||||
|     uint16_t remaining() const { return static_cast<uint16_t>(data.size()) - offset; } | ||||
|     const uint8_t *current_data() const { return data.data() + offset; } | ||||
|     uint16_t remaining() const { return size - offset; } | ||||
|     const uint8_t *current_data() const { return data.get() + offset; } | ||||
|   }; | ||||
|  | ||||
|   // 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 | ||||
|   APIError try_send_tx_buf_(); | ||||
|  | ||||
|   // 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> | ||||
|   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); | ||||
| @@ -161,10 +162,13 @@ class APIFrameHelper { | ||||
|  | ||||
|   // Containers (size varies, but typically 12+ bytes on 32-bit) | ||||
|   std::deque<SendBuffer> tx_buf_; | ||||
|   std::string info_; | ||||
|   std::vector<struct iovec> reusable_iovs_; | ||||
|   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 | ||||
|   uint16_t rx_buf_len_ = 0; | ||||
|   State state_{State::INITIALIZE}; | ||||
| @@ -179,105 +183,6 @@ class APIFrameHelper { | ||||
|   APIError handle_socket_read_result_(ssize_t received); | ||||
| }; | ||||
|  | ||||
| #ifdef USE_API_NOISE | ||||
| 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_; } | ||||
| }  // namespace esphome::api | ||||
|  | ||||
|  protected: | ||||
|   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 | ||||
| #endif  // USE_API | ||||
|   | ||||
							
								
								
									
										583
									
								
								esphome/components/api/api_frame_helper_noise.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										583
									
								
								esphome/components/api/api_frame_helper_noise.cpp
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										68
									
								
								esphome/components/api/api_frame_helper_noise.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								esphome/components/api/api_frame_helper_noise.h
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										290
									
								
								esphome/components/api/api_frame_helper_plaintext.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										290
									
								
								esphome/components/api/api_frame_helper_plaintext.cpp
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										53
									
								
								esphome/components/api/api_frame_helper_plaintext.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								esphome/components/api/api_frame_helper_plaintext.h
									
									
									
									
									
										Normal 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 | ||||
| @@ -3,8 +3,7 @@ | ||||
| #include <cstdint> | ||||
| #include "esphome/core/defines.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace api { | ||||
| namespace esphome::api { | ||||
|  | ||||
| #ifdef USE_API_NOISE | ||||
| using psk_t = std::array<uint8_t, 32>; | ||||
| @@ -28,5 +27,4 @@ class APINoiseContext { | ||||
| }; | ||||
| #endif  // USE_API_NOISE | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
| }  // namespace esphome::api | ||||
|   | ||||
| @@ -26,4 +26,6 @@ extend google.protobuf.MessageOptions { | ||||
|  | ||||
| extend google.protobuf.FieldOptions { | ||||
|     optional string field_ifdef = 1042; | ||||
|     optional uint32 fixed_array_size = 50007; | ||||
|     optional bool no_zero_copy = 50008 [default=false]; | ||||
| } | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -3,11 +3,11 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/defines.h" | ||||
| #include "esphome/core/string_ref.h" | ||||
|  | ||||
| #include "proto.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace api { | ||||
| namespace esphome::api { | ||||
|  | ||||
| namespace enums { | ||||
|  | ||||
| @@ -17,27 +17,13 @@ enum EntityCategory : uint32_t { | ||||
|   ENTITY_CATEGORY_DIAGNOSTIC = 2, | ||||
| }; | ||||
| #ifdef USE_COVER | ||||
| enum LegacyCoverState : uint32_t { | ||||
|   LEGACY_COVER_STATE_OPEN = 0, | ||||
|   LEGACY_COVER_STATE_CLOSED = 1, | ||||
| }; | ||||
| enum CoverOperation : uint32_t { | ||||
|   COVER_OPERATION_IDLE = 0, | ||||
|   COVER_OPERATION_IS_OPENING = 1, | ||||
|   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 | ||||
| #ifdef USE_FAN | ||||
| enum FanSpeed : uint32_t { | ||||
|   FAN_SPEED_LOW = 0, | ||||
|   FAN_SPEED_MEDIUM = 1, | ||||
|   FAN_SPEED_HIGH = 2, | ||||
| }; | ||||
| enum FanDirection : uint32_t { | ||||
|   FAN_DIRECTION_FORWARD = 0, | ||||
|   FAN_DIRECTION_REVERSE = 1, | ||||
| @@ -65,11 +51,6 @@ enum SensorStateClass : uint32_t { | ||||
|   STATE_CLASS_TOTAL_INCREASING = 2, | ||||
|   STATE_CLASS_TOTAL = 3, | ||||
| }; | ||||
| enum SensorLastResetType : uint32_t { | ||||
|   LAST_RESET_NONE = 0, | ||||
|   LAST_RESET_NEVER = 1, | ||||
|   LAST_RESET_AUTO = 2, | ||||
| }; | ||||
| #endif | ||||
| enum LogLevel : uint32_t { | ||||
|   LOG_LEVEL_NONE = 0, | ||||
| @@ -288,13 +269,20 @@ enum UpdateCommand : uint32_t { | ||||
| class InfoResponseProtoMessage : public ProtoMessage { | ||||
|  public: | ||||
|   ~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}; | ||||
|   std::string name{}; | ||||
|   StringRef name_ref_{}; | ||||
|   void set_name(const StringRef &ref) { this->name_ref_ = ref; } | ||||
|   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{}; | ||||
| #ifdef USE_DEVICES | ||||
|   uint32_t device_id{0}; | ||||
| #endif | ||||
|  | ||||
|  protected: | ||||
| }; | ||||
| @@ -303,7 +291,9 @@ class StateResponseProtoMessage : public ProtoMessage { | ||||
|  public: | ||||
|   ~StateResponseProtoMessage() override = default; | ||||
|   uint32_t key{0}; | ||||
| #ifdef USE_DEVICES | ||||
|   uint32_t device_id{0}; | ||||
| #endif | ||||
|  | ||||
|  protected: | ||||
| }; | ||||
| @@ -312,7 +302,9 @@ class CommandProtoMessage : public ProtoDecodableMessage { | ||||
|  public: | ||||
|   ~CommandProtoMessage() override = default; | ||||
|   uint32_t key{0}; | ||||
| #ifdef USE_DEVICES | ||||
|   uint32_t device_id{0}; | ||||
| #endif | ||||
|  | ||||
|  protected: | ||||
| }; | ||||
| @@ -343,8 +335,10 @@ class HelloResponse : public ProtoMessage { | ||||
| #endif | ||||
|   uint32_t api_version_major{0}; | ||||
|   uint32_t api_version_minor{0}; | ||||
|   std::string server_info{}; | ||||
|   std::string name{}; | ||||
|   StringRef server_info_ref_{}; | ||||
|   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 calculate_size(uint32_t &total_size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -449,10 +443,12 @@ class DeviceInfoRequest : public ProtoDecodableMessage { | ||||
|  | ||||
|  protected: | ||||
| }; | ||||
| #ifdef USE_AREAS | ||||
| class AreaInfo : public ProtoMessage { | ||||
|  public: | ||||
|   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 calculate_size(uint32_t &total_size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -461,10 +457,13 @@ class AreaInfo : public ProtoMessage { | ||||
|  | ||||
|  protected: | ||||
| }; | ||||
| #endif | ||||
| #ifdef USE_DEVICES | ||||
| class DeviceInfo : public ProtoMessage { | ||||
|  public: | ||||
|   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}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| @@ -474,50 +473,58 @@ class DeviceInfo : public ProtoMessage { | ||||
|  | ||||
|  protected: | ||||
| }; | ||||
| #endif | ||||
| class DeviceInfoResponse : public ProtoMessage { | ||||
|  public: | ||||
|   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 | ||||
|   const char *message_name() const override { return "device_info_response"; } | ||||
| #endif | ||||
| #ifdef USE_API_PASSWORD | ||||
|   bool uses_password{false}; | ||||
|   std::string name{}; | ||||
|   std::string mac_address{}; | ||||
|   std::string esphome_version{}; | ||||
|   std::string compilation_time{}; | ||||
|   std::string model{}; | ||||
| #endif | ||||
|   StringRef name_ref_{}; | ||||
|   void set_name(const StringRef &ref) { this->name_ref_ = ref; } | ||||
|   StringRef mac_address_ref_{}; | ||||
|   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 | ||||
|   bool has_deep_sleep{false}; | ||||
| #endif | ||||
| #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 | ||||
| #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 | ||||
| #ifdef USE_WEBSERVER | ||||
|   uint32_t webserver_port{0}; | ||||
| #endif | ||||
| #ifdef USE_BLUETOOTH_PROXY | ||||
|   uint32_t legacy_bluetooth_proxy_version{0}; | ||||
| #endif | ||||
| #ifdef USE_BLUETOOTH_PROXY | ||||
|   uint32_t bluetooth_proxy_feature_flags{0}; | ||||
| #endif | ||||
|   std::string manufacturer{}; | ||||
|   std::string friendly_name{}; | ||||
| #ifdef USE_VOICE_ASSISTANT | ||||
|   uint32_t legacy_voice_assistant_version{0}; | ||||
| #endif | ||||
|   StringRef manufacturer_ref_{}; | ||||
|   void set_manufacturer(const StringRef &ref) { this->manufacturer_ref_ = ref; } | ||||
|   StringRef friendly_name_ref_{}; | ||||
|   void set_friendly_name(const StringRef &ref) { this->friendly_name_ref_ = ref; } | ||||
| #ifdef USE_VOICE_ASSISTANT | ||||
|   uint32_t voice_assistant_feature_flags{0}; | ||||
| #endif | ||||
| #ifdef USE_AREAS | ||||
|   std::string suggested_area{}; | ||||
|   StringRef suggested_area_ref_{}; | ||||
|   void set_suggested_area(const StringRef &ref) { this->suggested_area_ref_ = ref; } | ||||
| #endif | ||||
| #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 | ||||
| #ifdef USE_API_NOISE | ||||
|   bool api_encryption_supported{false}; | ||||
| @@ -586,7 +593,8 @@ class ListEntitiesBinarySensorResponse : public InfoResponseProtoMessage { | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   const char *message_name() const override { return "list_entities_binary_sensor_response"; } | ||||
| #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}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| @@ -625,7 +633,8 @@ class ListEntitiesCoverResponse : public InfoResponseProtoMessage { | ||||
|   bool assumed_state{false}; | ||||
|   bool supports_position{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}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| @@ -638,11 +647,10 @@ class ListEntitiesCoverResponse : public InfoResponseProtoMessage { | ||||
| class CoverStateResponse : public StateResponseProtoMessage { | ||||
|  public: | ||||
|   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 | ||||
|   const char *message_name() const override { return "cover_state_response"; } | ||||
| #endif | ||||
|   enums::LegacyCoverState legacy_state{}; | ||||
|   float position{0.0f}; | ||||
|   float tilt{0.0f}; | ||||
|   enums::CoverOperation current_operation{}; | ||||
| @@ -657,12 +665,10 @@ class CoverStateResponse : public StateResponseProtoMessage { | ||||
| class CoverCommandRequest : public CommandProtoMessage { | ||||
|  public: | ||||
|   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 | ||||
|   const char *message_name() const override { return "cover_command_request"; } | ||||
| #endif | ||||
|   bool has_legacy_command{false}; | ||||
|   enums::LegacyCoverCommand legacy_command{}; | ||||
|   bool has_position{false}; | ||||
|   float position{0.0f}; | ||||
|   bool has_tilt{false}; | ||||
| @@ -701,16 +707,16 @@ class ListEntitiesFanResponse : public InfoResponseProtoMessage { | ||||
| class FanStateResponse : public StateResponseProtoMessage { | ||||
|  public: | ||||
|   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 | ||||
|   const char *message_name() const override { return "fan_state_response"; } | ||||
| #endif | ||||
|   bool state{false}; | ||||
|   bool oscillating{false}; | ||||
|   enums::FanSpeed speed{}; | ||||
|   enums::FanDirection direction{}; | ||||
|   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 calculate_size(uint32_t &total_size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -722,14 +728,12 @@ class FanStateResponse : public StateResponseProtoMessage { | ||||
| class FanCommandRequest : public CommandProtoMessage { | ||||
|  public: | ||||
|   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 | ||||
|   const char *message_name() const override { return "fan_command_request"; } | ||||
| #endif | ||||
|   bool has_state{false}; | ||||
|   bool state{false}; | ||||
|   bool has_speed{false}; | ||||
|   enums::FanSpeed speed{}; | ||||
|   bool has_oscillating{false}; | ||||
|   bool oscillating{false}; | ||||
|   bool has_direction{false}; | ||||
| @@ -752,15 +756,11 @@ class FanCommandRequest : public CommandProtoMessage { | ||||
| class ListEntitiesLightResponse : public InfoResponseProtoMessage { | ||||
|  public: | ||||
|   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 | ||||
|   const char *message_name() const override { return "list_entities_light_response"; } | ||||
| #endif | ||||
|   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 max_mireds{0.0f}; | ||||
|   std::vector<std::string> effects{}; | ||||
| @@ -790,7 +790,8 @@ class LightStateResponse : public StateResponseProtoMessage { | ||||
|   float color_temperature{0.0f}; | ||||
|   float cold_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 calculate_size(uint32_t &total_size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -846,16 +847,17 @@ class LightCommandRequest : public CommandProtoMessage { | ||||
| class ListEntitiesSensorResponse : public InfoResponseProtoMessage { | ||||
|  public: | ||||
|   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 | ||||
|   const char *message_name() const override { return "list_entities_sensor_response"; } | ||||
| #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}; | ||||
|   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::SensorLastResetType legacy_last_reset_type{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -891,7 +893,8 @@ class ListEntitiesSwitchResponse : public InfoResponseProtoMessage { | ||||
|   const char *message_name() const override { return "list_entities_switch_response"; } | ||||
| #endif | ||||
|   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 calculate_size(uint32_t &total_size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -941,7 +944,8 @@ class ListEntitiesTextSensorResponse : public InfoResponseProtoMessage { | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   const char *message_name() const override { return "list_entities_text_sensor_response"; } | ||||
| #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 calculate_size(uint32_t &total_size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -957,7 +961,8 @@ class TextSensorStateResponse : public StateResponseProtoMessage { | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   const char *message_name() const override { return "text_sensor_state_response"; } | ||||
| #endif | ||||
|   std::string state{}; | ||||
|   StringRef state_ref_{}; | ||||
|   void set_state(const StringRef &ref) { this->state_ref_ = ref; } | ||||
|   bool missing_state{false}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| @@ -987,13 +992,17 @@ class SubscribeLogsRequest : public ProtoDecodableMessage { | ||||
| class SubscribeLogsResponse : public ProtoMessage { | ||||
|  public: | ||||
|   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 | ||||
|   const char *message_name() const override { return "subscribe_logs_response"; } | ||||
| #endif | ||||
|   enums::LogLevel level{}; | ||||
|   std::string message{}; | ||||
|   bool send_failed{false}; | ||||
|   const uint8_t *message_ptr_{nullptr}; | ||||
|   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 calculate_size(uint32_t &total_size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -1035,6 +1044,7 @@ class NoiseEncryptionSetKeyResponse : public ProtoMessage { | ||||
|  protected: | ||||
| }; | ||||
| #endif | ||||
| #ifdef USE_API_HOMEASSISTANT_SERVICES | ||||
| class SubscribeHomeassistantServicesRequest : public ProtoDecodableMessage { | ||||
|  public: | ||||
|   static constexpr uint8_t MESSAGE_TYPE = 34; | ||||
| @@ -1050,7 +1060,8 @@ class SubscribeHomeassistantServicesRequest : public ProtoDecodableMessage { | ||||
| }; | ||||
| class HomeassistantServiceMap : public ProtoMessage { | ||||
|  public: | ||||
|   std::string key{}; | ||||
|   StringRef key_ref_{}; | ||||
|   void set_key(const StringRef &ref) { this->key_ref_ = ref; } | ||||
|   std::string value{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| @@ -1067,7 +1078,8 @@ class HomeassistantServiceResponse : public ProtoMessage { | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   const char *message_name() const override { return "homeassistant_service_response"; } | ||||
| #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_template{}; | ||||
|   std::vector<HomeassistantServiceMap> variables{}; | ||||
| @@ -1080,6 +1092,8 @@ class HomeassistantServiceResponse : public ProtoMessage { | ||||
|  | ||||
|  protected: | ||||
| }; | ||||
| #endif | ||||
| #ifdef USE_API_HOMEASSISTANT_STATES | ||||
| class SubscribeHomeAssistantStatesRequest : public ProtoDecodableMessage { | ||||
|  public: | ||||
|   static constexpr uint8_t MESSAGE_TYPE = 38; | ||||
| @@ -1100,8 +1114,10 @@ class SubscribeHomeAssistantStateResponse : public ProtoMessage { | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   const char *message_name() const override { return "subscribe_home_assistant_state_response"; } | ||||
| #endif | ||||
|   std::string entity_id{}; | ||||
|   std::string attribute{}; | ||||
|   StringRef entity_id_ref_{}; | ||||
|   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}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| @@ -1128,6 +1144,7 @@ class HomeAssistantStateResponse : public ProtoDecodableMessage { | ||||
|  protected: | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
| }; | ||||
| #endif | ||||
| class GetTimeRequest : public ProtoDecodableMessage { | ||||
|  public: | ||||
|   static constexpr uint8_t MESSAGE_TYPE = 36; | ||||
| @@ -1161,7 +1178,8 @@ class GetTimeResponse : public ProtoDecodableMessage { | ||||
| #ifdef USE_API_SERVICES | ||||
| class ListEntitiesServicesArgument : public ProtoMessage { | ||||
|  public: | ||||
|   std::string name{}; | ||||
|   StringRef name_ref_{}; | ||||
|   void set_name(const StringRef &ref) { this->name_ref_ = ref; } | ||||
|   enums::ServiceArgType type{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| @@ -1178,7 +1196,8 @@ class ListEntitiesServicesResponse : public ProtoMessage { | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   const char *message_name() const override { return "list_entities_services_response"; } | ||||
| #endif | ||||
|   std::string name{}; | ||||
|   StringRef name_ref_{}; | ||||
|   void set_name(const StringRef &ref) { this->name_ref_ = ref; } | ||||
|   uint32_t key{0}; | ||||
|   std::vector<ListEntitiesServicesArgument> args{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| @@ -1250,7 +1269,12 @@ class CameraImageResponse : public StateResponseProtoMessage { | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   const char *message_name() const override { return "camera_image_response"; } | ||||
| #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}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| @@ -1281,7 +1305,7 @@ class CameraImageRequest : public ProtoDecodableMessage { | ||||
| class ListEntitiesClimateResponse : public InfoResponseProtoMessage { | ||||
|  public: | ||||
|   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 | ||||
|   const char *message_name() const override { return "list_entities_climate_response"; } | ||||
| #endif | ||||
| @@ -1291,7 +1315,6 @@ class ListEntitiesClimateResponse : public InfoResponseProtoMessage { | ||||
|   float visual_min_temperature{0.0f}; | ||||
|   float visual_max_temperature{0.0f}; | ||||
|   float visual_target_temperature_step{0.0f}; | ||||
|   bool legacy_supports_away{false}; | ||||
|   bool supports_action{false}; | ||||
|   std::vector<enums::ClimateFanMode> supported_fan_modes{}; | ||||
|   std::vector<enums::ClimateSwingMode> supported_swing_modes{}; | ||||
| @@ -1314,7 +1337,7 @@ class ListEntitiesClimateResponse : public InfoResponseProtoMessage { | ||||
| class ClimateStateResponse : public StateResponseProtoMessage { | ||||
|  public: | ||||
|   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 | ||||
|   const char *message_name() const override { return "climate_state_response"; } | ||||
| #endif | ||||
| @@ -1323,13 +1346,14 @@ class ClimateStateResponse : public StateResponseProtoMessage { | ||||
|   float target_temperature{0.0f}; | ||||
|   float target_temperature_low{0.0f}; | ||||
|   float target_temperature_high{0.0f}; | ||||
|   bool unused_legacy_away{false}; | ||||
|   enums::ClimateAction action{}; | ||||
|   enums::ClimateFanMode fan_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{}; | ||||
|   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 target_humidity{0.0f}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| @@ -1343,7 +1367,7 @@ class ClimateStateResponse : public StateResponseProtoMessage { | ||||
| class ClimateCommandRequest : public CommandProtoMessage { | ||||
|  public: | ||||
|   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 | ||||
|   const char *message_name() const override { return "climate_command_request"; } | ||||
| #endif | ||||
| @@ -1355,8 +1379,6 @@ class ClimateCommandRequest : public CommandProtoMessage { | ||||
|   float target_temperature_low{0.0f}; | ||||
|   bool has_target_temperature_high{false}; | ||||
|   float target_temperature_high{0.0f}; | ||||
|   bool unused_has_legacy_away{false}; | ||||
|   bool unused_legacy_away{false}; | ||||
|   bool has_fan_mode{false}; | ||||
|   enums::ClimateFanMode fan_mode{}; | ||||
|   bool has_swing_mode{false}; | ||||
| @@ -1390,9 +1412,11 @@ class ListEntitiesNumberResponse : public InfoResponseProtoMessage { | ||||
|   float min_value{0.0f}; | ||||
|   float max_value{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{}; | ||||
|   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 calculate_size(uint32_t &total_size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -1459,7 +1483,8 @@ class SelectStateResponse : public StateResponseProtoMessage { | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   const char *message_name() const override { return "select_state_response"; } | ||||
| #endif | ||||
|   std::string state{}; | ||||
|   StringRef state_ref_{}; | ||||
|   void set_state(const StringRef &ref) { this->state_ref_ = ref; } | ||||
|   bool missing_state{false}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| @@ -1558,7 +1583,8 @@ class ListEntitiesLockResponse : public InfoResponseProtoMessage { | ||||
|   bool assumed_state{false}; | ||||
|   bool supports_open{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 calculate_size(uint32_t &total_size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -1611,7 +1637,8 @@ class ListEntitiesButtonResponse : public InfoResponseProtoMessage { | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   const char *message_name() const override { return "list_entities_button_response"; } | ||||
| #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 calculate_size(uint32_t &total_size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -1639,7 +1666,8 @@ class ButtonCommandRequest : public CommandProtoMessage { | ||||
| #ifdef USE_MEDIA_PLAYER | ||||
| class MediaPlayerSupportedFormat : public ProtoMessage { | ||||
|  public: | ||||
|   std::string format{}; | ||||
|   StringRef format_ref_{}; | ||||
|   void set_format(const StringRef &ref) { this->format_ref_ = ref; } | ||||
|   uint32_t sample_rate{0}; | ||||
|   uint32_t num_channels{0}; | ||||
|   enums::MediaPlayerFormatPurpose purpose{}; | ||||
| @@ -1728,47 +1756,13 @@ class SubscribeBluetoothLEAdvertisementsRequest : public ProtoDecodableMessage { | ||||
|  protected: | ||||
|   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 { | ||||
|  public: | ||||
|   uint64_t address{0}; | ||||
|   int32_t rssi{0}; | ||||
|   uint32_t address_type{0}; | ||||
|   std::string data{}; | ||||
|   uint8_t data[62]{}; | ||||
|   uint8_t data_len{0}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -1847,7 +1841,7 @@ class BluetoothGATTGetServicesRequest : public ProtoDecodableMessage { | ||||
| }; | ||||
| class BluetoothGATTDescriptor : public ProtoMessage { | ||||
|  public: | ||||
|   std::vector<uint64_t> uuid{}; | ||||
|   std::array<uint64_t, 2> uuid{}; | ||||
|   uint32_t handle{0}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| @@ -1859,7 +1853,7 @@ class BluetoothGATTDescriptor : public ProtoMessage { | ||||
| }; | ||||
| class BluetoothGATTCharacteristic : public ProtoMessage { | ||||
|  public: | ||||
|   std::vector<uint64_t> uuid{}; | ||||
|   std::array<uint64_t, 2> uuid{}; | ||||
|   uint32_t handle{0}; | ||||
|   uint32_t properties{0}; | ||||
|   std::vector<BluetoothGATTDescriptor> descriptors{}; | ||||
| @@ -1873,7 +1867,7 @@ class BluetoothGATTCharacteristic : public ProtoMessage { | ||||
| }; | ||||
| class BluetoothGATTService : public ProtoMessage { | ||||
|  public: | ||||
|   std::vector<uint64_t> uuid{}; | ||||
|   std::array<uint64_t, 2> uuid{}; | ||||
|   uint32_t handle{0}; | ||||
|   std::vector<BluetoothGATTCharacteristic> characteristics{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| @@ -1887,12 +1881,12 @@ class BluetoothGATTService : public ProtoMessage { | ||||
| class BluetoothGATTGetServicesResponse : public ProtoMessage { | ||||
|  public: | ||||
|   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 | ||||
|   const char *message_name() const override { return "bluetooth_gatt_get_services_response"; } | ||||
| #endif | ||||
|   uint64_t address{0}; | ||||
|   std::vector<BluetoothGATTService> services{}; | ||||
|   std::array<BluetoothGATTService, 1> services{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -1942,7 +1936,12 @@ class BluetoothGATTReadResponse : public ProtoMessage { | ||||
| #endif | ||||
|   uint64_t address{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 calculate_size(uint32_t &total_size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -2030,7 +2029,12 @@ class BluetoothGATTNotifyDataResponse : public ProtoMessage { | ||||
| #endif | ||||
|   uint64_t address{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 calculate_size(uint32_t &total_size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -2260,10 +2264,12 @@ class VoiceAssistantRequest : public ProtoMessage { | ||||
|   const char *message_name() const override { return "voice_assistant_request"; } | ||||
| #endif | ||||
|   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}; | ||||
|   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 calculate_size(uint32_t &total_size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -2324,6 +2330,12 @@ class VoiceAssistantAudio : public ProtoDecodableMessage { | ||||
|   const char *message_name() const override { return "voice_assistant_audio"; } | ||||
| #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 end{false}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| @@ -2393,8 +2405,10 @@ class VoiceAssistantAnnounceFinished : public ProtoMessage { | ||||
| }; | ||||
| class VoiceAssistantWakeWord : public ProtoMessage { | ||||
|  public: | ||||
|   std::string id{}; | ||||
|   std::string wake_word{}; | ||||
|   StringRef id_ref_{}; | ||||
|   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{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| @@ -2515,7 +2529,8 @@ class ListEntitiesTextResponse : public InfoResponseProtoMessage { | ||||
| #endif | ||||
|   uint32_t min_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{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| @@ -2532,7 +2547,8 @@ class TextStateResponse : public StateResponseProtoMessage { | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   const char *message_name() const override { return "text_state_response"; } | ||||
| #endif | ||||
|   std::string state{}; | ||||
|   StringRef state_ref_{}; | ||||
|   void set_state(const StringRef &ref) { this->state_ref_ = ref; } | ||||
|   bool missing_state{false}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| @@ -2676,7 +2692,8 @@ class ListEntitiesEventResponse : public InfoResponseProtoMessage { | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   const char *message_name() const override { return "list_entities_event_response"; } | ||||
| #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{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| @@ -2693,7 +2710,8 @@ class EventResponse : public StateResponseProtoMessage { | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   const char *message_name() const override { return "event_response"; } | ||||
| #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 calculate_size(uint32_t &total_size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -2711,7 +2729,8 @@ class ListEntitiesValveResponse : public InfoResponseProtoMessage { | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   const char *message_name() const override { return "list_entities_valve_response"; } | ||||
| #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 supports_position{false}; | ||||
|   bool supports_stop{false}; | ||||
| @@ -2817,7 +2836,8 @@ class ListEntitiesUpdateResponse : public InfoResponseProtoMessage { | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   const char *message_name() const override { return "list_entities_update_response"; } | ||||
| #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 calculate_size(uint32_t &total_size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -2837,11 +2857,16 @@ class UpdateStateResponse : public StateResponseProtoMessage { | ||||
|   bool in_progress{false}; | ||||
|   bool has_progress{false}; | ||||
|   float progress{0.0f}; | ||||
|   std::string current_version{}; | ||||
|   std::string latest_version{}; | ||||
|   std::string title{}; | ||||
|   std::string release_summary{}; | ||||
|   std::string release_url{}; | ||||
|   StringRef current_version_ref_{}; | ||||
|   void set_current_version(const StringRef &ref) { this->current_version_ref_ = ref; } | ||||
|   StringRef latest_version_ref_{}; | ||||
|   void set_latest_version(const StringRef &ref) { this->latest_version_ref_ = ref; } | ||||
|   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 calculate_size(uint32_t &total_size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -2868,5 +2893,4 @@ class UpdateCommandRequest : public CommandProtoMessage { | ||||
| }; | ||||
| #endif | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
| }  // namespace esphome::api | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -3,8 +3,7 @@ | ||||
| #include "api_pb2_service.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace api { | ||||
| namespace esphome::api { | ||||
|  | ||||
| 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) { | ||||
|   switch (msg_type) { | ||||
|     case 1: { | ||||
|     case HelloRequest::MESSAGE_TYPE: { | ||||
|       HelloRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #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); | ||||
|       break; | ||||
|     } | ||||
|     case 3: { | ||||
|     case ConnectRequest::MESSAGE_TYPE: { | ||||
|       ConnectRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #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); | ||||
|       break; | ||||
|     } | ||||
|     case 5: { | ||||
|     case DisconnectRequest::MESSAGE_TYPE: { | ||||
|       DisconnectRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #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); | ||||
|       break; | ||||
|     } | ||||
|     case 6: { | ||||
|     case DisconnectResponse::MESSAGE_TYPE: { | ||||
|       DisconnectResponse msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #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); | ||||
|       break; | ||||
|     } | ||||
|     case 7: { | ||||
|     case PingRequest::MESSAGE_TYPE: { | ||||
|       PingRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #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); | ||||
|       break; | ||||
|     } | ||||
|     case 8: { | ||||
|     case PingResponse::MESSAGE_TYPE: { | ||||
|       PingResponse msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #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); | ||||
|       break; | ||||
|     } | ||||
|     case 9: { | ||||
|     case DeviceInfoRequest::MESSAGE_TYPE: { | ||||
|       DeviceInfoRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #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); | ||||
|       break; | ||||
|     } | ||||
|     case 11: { | ||||
|     case ListEntitiesRequest::MESSAGE_TYPE: { | ||||
|       ListEntitiesRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #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); | ||||
|       break; | ||||
|     } | ||||
|     case 20: { | ||||
|     case SubscribeStatesRequest::MESSAGE_TYPE: { | ||||
|       SubscribeStatesRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #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); | ||||
|       break; | ||||
|     } | ||||
|     case 28: { | ||||
|     case SubscribeLogsRequest::MESSAGE_TYPE: { | ||||
|       SubscribeLogsRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -107,7 +106,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|       break; | ||||
|     } | ||||
| #ifdef USE_COVER | ||||
|     case 30: { | ||||
|     case CoverCommandRequest::MESSAGE_TYPE: { | ||||
|       CoverCommandRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -118,7 +117,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|     } | ||||
| #endif | ||||
| #ifdef USE_FAN | ||||
|     case 31: { | ||||
|     case FanCommandRequest::MESSAGE_TYPE: { | ||||
|       FanCommandRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -129,7 +128,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|     } | ||||
| #endif | ||||
| #ifdef USE_LIGHT | ||||
|     case 32: { | ||||
|     case LightCommandRequest::MESSAGE_TYPE: { | ||||
|       LightCommandRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -140,7 +139,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|     } | ||||
| #endif | ||||
| #ifdef USE_SWITCH | ||||
|     case 33: { | ||||
|     case SwitchCommandRequest::MESSAGE_TYPE: { | ||||
|       SwitchCommandRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -150,7 +149,8 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|       break; | ||||
|     } | ||||
| #endif | ||||
|     case 34: { | ||||
| #ifdef USE_API_HOMEASSISTANT_SERVICES | ||||
|     case SubscribeHomeassistantServicesRequest::MESSAGE_TYPE: { | ||||
|       SubscribeHomeassistantServicesRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -159,7 +159,8 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|       this->on_subscribe_homeassistant_services_request(msg); | ||||
|       break; | ||||
|     } | ||||
|     case 36: { | ||||
| #endif | ||||
|     case GetTimeRequest::MESSAGE_TYPE: { | ||||
|       GetTimeRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -168,7 +169,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|       this->on_get_time_request(msg); | ||||
|       break; | ||||
|     } | ||||
|     case 37: { | ||||
|     case GetTimeResponse::MESSAGE_TYPE: { | ||||
|       GetTimeResponse msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -177,7 +178,8 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|       this->on_get_time_response(msg); | ||||
|       break; | ||||
|     } | ||||
|     case 38: { | ||||
| #ifdef USE_API_HOMEASSISTANT_STATES | ||||
|     case SubscribeHomeAssistantStatesRequest::MESSAGE_TYPE: { | ||||
|       SubscribeHomeAssistantStatesRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -186,7 +188,9 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|       this->on_subscribe_home_assistant_states_request(msg); | ||||
|       break; | ||||
|     } | ||||
|     case 40: { | ||||
| #endif | ||||
| #ifdef USE_API_HOMEASSISTANT_STATES | ||||
|     case HomeAssistantStateResponse::MESSAGE_TYPE: { | ||||
|       HomeAssistantStateResponse msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -195,8 +199,9 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|       this->on_home_assistant_state_response(msg); | ||||
|       break; | ||||
|     } | ||||
| #endif | ||||
| #ifdef USE_API_SERVICES | ||||
|     case 42: { | ||||
|     case ExecuteServiceRequest::MESSAGE_TYPE: { | ||||
|       ExecuteServiceRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -207,7 +212,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|     } | ||||
| #endif | ||||
| #ifdef USE_CAMERA | ||||
|     case 45: { | ||||
|     case CameraImageRequest::MESSAGE_TYPE: { | ||||
|       CameraImageRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -218,7 +223,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|     } | ||||
| #endif | ||||
| #ifdef USE_CLIMATE | ||||
|     case 48: { | ||||
|     case ClimateCommandRequest::MESSAGE_TYPE: { | ||||
|       ClimateCommandRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -229,7 +234,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|     } | ||||
| #endif | ||||
| #ifdef USE_NUMBER | ||||
|     case 51: { | ||||
|     case NumberCommandRequest::MESSAGE_TYPE: { | ||||
|       NumberCommandRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -240,7 +245,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|     } | ||||
| #endif | ||||
| #ifdef USE_SELECT | ||||
|     case 54: { | ||||
|     case SelectCommandRequest::MESSAGE_TYPE: { | ||||
|       SelectCommandRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -251,7 +256,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|     } | ||||
| #endif | ||||
| #ifdef USE_SIREN | ||||
|     case 57: { | ||||
|     case SirenCommandRequest::MESSAGE_TYPE: { | ||||
|       SirenCommandRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -262,7 +267,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|     } | ||||
| #endif | ||||
| #ifdef USE_LOCK | ||||
|     case 60: { | ||||
|     case LockCommandRequest::MESSAGE_TYPE: { | ||||
|       LockCommandRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -273,7 +278,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|     } | ||||
| #endif | ||||
| #ifdef USE_BUTTON | ||||
|     case 62: { | ||||
|     case ButtonCommandRequest::MESSAGE_TYPE: { | ||||
|       ButtonCommandRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -284,7 +289,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|     } | ||||
| #endif | ||||
| #ifdef USE_MEDIA_PLAYER | ||||
|     case 65: { | ||||
|     case MediaPlayerCommandRequest::MESSAGE_TYPE: { | ||||
|       MediaPlayerCommandRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -295,7 +300,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|     } | ||||
| #endif | ||||
| #ifdef USE_BLUETOOTH_PROXY | ||||
|     case 66: { | ||||
|     case SubscribeBluetoothLEAdvertisementsRequest::MESSAGE_TYPE: { | ||||
|       SubscribeBluetoothLEAdvertisementsRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -306,7 +311,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|     } | ||||
| #endif | ||||
| #ifdef USE_BLUETOOTH_PROXY | ||||
|     case 68: { | ||||
|     case BluetoothDeviceRequest::MESSAGE_TYPE: { | ||||
|       BluetoothDeviceRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -317,7 +322,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|     } | ||||
| #endif | ||||
| #ifdef USE_BLUETOOTH_PROXY | ||||
|     case 70: { | ||||
|     case BluetoothGATTGetServicesRequest::MESSAGE_TYPE: { | ||||
|       BluetoothGATTGetServicesRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -328,7 +333,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|     } | ||||
| #endif | ||||
| #ifdef USE_BLUETOOTH_PROXY | ||||
|     case 73: { | ||||
|     case BluetoothGATTReadRequest::MESSAGE_TYPE: { | ||||
|       BluetoothGATTReadRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -339,7 +344,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|     } | ||||
| #endif | ||||
| #ifdef USE_BLUETOOTH_PROXY | ||||
|     case 75: { | ||||
|     case BluetoothGATTWriteRequest::MESSAGE_TYPE: { | ||||
|       BluetoothGATTWriteRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -350,7 +355,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|     } | ||||
| #endif | ||||
| #ifdef USE_BLUETOOTH_PROXY | ||||
|     case 76: { | ||||
|     case BluetoothGATTReadDescriptorRequest::MESSAGE_TYPE: { | ||||
|       BluetoothGATTReadDescriptorRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -361,7 +366,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|     } | ||||
| #endif | ||||
| #ifdef USE_BLUETOOTH_PROXY | ||||
|     case 77: { | ||||
|     case BluetoothGATTWriteDescriptorRequest::MESSAGE_TYPE: { | ||||
|       BluetoothGATTWriteDescriptorRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -372,7 +377,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|     } | ||||
| #endif | ||||
| #ifdef USE_BLUETOOTH_PROXY | ||||
|     case 78: { | ||||
|     case BluetoothGATTNotifyRequest::MESSAGE_TYPE: { | ||||
|       BluetoothGATTNotifyRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -383,7 +388,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|     } | ||||
| #endif | ||||
| #ifdef USE_BLUETOOTH_PROXY | ||||
|     case 80: { | ||||
|     case SubscribeBluetoothConnectionsFreeRequest::MESSAGE_TYPE: { | ||||
|       SubscribeBluetoothConnectionsFreeRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -394,7 +399,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|     } | ||||
| #endif | ||||
| #ifdef USE_BLUETOOTH_PROXY | ||||
|     case 87: { | ||||
|     case UnsubscribeBluetoothLEAdvertisementsRequest::MESSAGE_TYPE: { | ||||
|       UnsubscribeBluetoothLEAdvertisementsRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -405,7 +410,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|     } | ||||
| #endif | ||||
| #ifdef USE_VOICE_ASSISTANT | ||||
|     case 89: { | ||||
|     case SubscribeVoiceAssistantRequest::MESSAGE_TYPE: { | ||||
|       SubscribeVoiceAssistantRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -416,7 +421,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|     } | ||||
| #endif | ||||
| #ifdef USE_VOICE_ASSISTANT | ||||
|     case 91: { | ||||
|     case VoiceAssistantResponse::MESSAGE_TYPE: { | ||||
|       VoiceAssistantResponse msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -427,7 +432,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|     } | ||||
| #endif | ||||
| #ifdef USE_VOICE_ASSISTANT | ||||
|     case 92: { | ||||
|     case VoiceAssistantEventResponse::MESSAGE_TYPE: { | ||||
|       VoiceAssistantEventResponse msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -438,7 +443,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|     } | ||||
| #endif | ||||
| #ifdef USE_ALARM_CONTROL_PANEL | ||||
|     case 96: { | ||||
|     case AlarmControlPanelCommandRequest::MESSAGE_TYPE: { | ||||
|       AlarmControlPanelCommandRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -449,7 +454,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|     } | ||||
| #endif | ||||
| #ifdef USE_TEXT | ||||
|     case 99: { | ||||
|     case TextCommandRequest::MESSAGE_TYPE: { | ||||
|       TextCommandRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -460,7 +465,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|     } | ||||
| #endif | ||||
| #ifdef USE_DATETIME_DATE | ||||
|     case 102: { | ||||
|     case DateCommandRequest::MESSAGE_TYPE: { | ||||
|       DateCommandRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -471,7 +476,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|     } | ||||
| #endif | ||||
| #ifdef USE_DATETIME_TIME | ||||
|     case 105: { | ||||
|     case TimeCommandRequest::MESSAGE_TYPE: { | ||||
|       TimeCommandRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -482,7 +487,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|     } | ||||
| #endif | ||||
| #ifdef USE_VOICE_ASSISTANT | ||||
|     case 106: { | ||||
|     case VoiceAssistantAudio::MESSAGE_TYPE: { | ||||
|       VoiceAssistantAudio msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -493,7 +498,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|     } | ||||
| #endif | ||||
| #ifdef USE_VALVE | ||||
|     case 111: { | ||||
|     case ValveCommandRequest::MESSAGE_TYPE: { | ||||
|       ValveCommandRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -504,7 +509,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|     } | ||||
| #endif | ||||
| #ifdef USE_DATETIME_DATETIME | ||||
|     case 114: { | ||||
|     case DateTimeCommandRequest::MESSAGE_TYPE: { | ||||
|       DateTimeCommandRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -515,7 +520,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|     } | ||||
| #endif | ||||
| #ifdef USE_VOICE_ASSISTANT | ||||
|     case 115: { | ||||
|     case VoiceAssistantTimerEventResponse::MESSAGE_TYPE: { | ||||
|       VoiceAssistantTimerEventResponse msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -526,7 +531,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|     } | ||||
| #endif | ||||
| #ifdef USE_UPDATE | ||||
|     case 118: { | ||||
|     case UpdateCommandRequest::MESSAGE_TYPE: { | ||||
|       UpdateCommandRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -537,7 +542,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|     } | ||||
| #endif | ||||
| #ifdef USE_VOICE_ASSISTANT | ||||
|     case 119: { | ||||
|     case VoiceAssistantAnnounceRequest::MESSAGE_TYPE: { | ||||
|       VoiceAssistantAnnounceRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -548,7 +553,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|     } | ||||
| #endif | ||||
| #ifdef USE_VOICE_ASSISTANT | ||||
|     case 121: { | ||||
|     case VoiceAssistantConfigurationRequest::MESSAGE_TYPE: { | ||||
|       VoiceAssistantConfigurationRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -559,7 +564,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|     } | ||||
| #endif | ||||
| #ifdef USE_VOICE_ASSISTANT | ||||
|     case 123: { | ||||
|     case VoiceAssistantSetConfiguration::MESSAGE_TYPE: { | ||||
|       VoiceAssistantSetConfiguration msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -570,7 +575,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|     } | ||||
| #endif | ||||
| #ifdef USE_API_NOISE | ||||
|     case 124: { | ||||
|     case NoiseEncryptionSetKeyRequest::MESSAGE_TYPE: { | ||||
|       NoiseEncryptionSetKeyRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -581,7 +586,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|     } | ||||
| #endif | ||||
| #ifdef USE_BLUETOOTH_PROXY | ||||
|     case 127: { | ||||
|     case BluetoothScannerSetModeRequest::MESSAGE_TYPE: { | ||||
|       BluetoothScannerSetModeRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -597,35 +602,28 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
| } | ||||
|  | ||||
| void APIServerConnection::on_hello_request(const HelloRequest &msg) { | ||||
|   HelloResponse ret = this->hello(msg); | ||||
|   if (!this->send_message(ret, HelloResponse::MESSAGE_TYPE)) { | ||||
|   if (!this->send_hello_response(msg)) { | ||||
|     this->on_fatal_error(); | ||||
|   } | ||||
| } | ||||
| void APIServerConnection::on_connect_request(const ConnectRequest &msg) { | ||||
|   ConnectResponse ret = this->connect(msg); | ||||
|   if (!this->send_message(ret, ConnectResponse::MESSAGE_TYPE)) { | ||||
|   if (!this->send_connect_response(msg)) { | ||||
|     this->on_fatal_error(); | ||||
|   } | ||||
| } | ||||
| void APIServerConnection::on_disconnect_request(const DisconnectRequest &msg) { | ||||
|   DisconnectResponse ret = this->disconnect(msg); | ||||
|   if (!this->send_message(ret, DisconnectResponse::MESSAGE_TYPE)) { | ||||
|   if (!this->send_disconnect_response(msg)) { | ||||
|     this->on_fatal_error(); | ||||
|   } | ||||
| } | ||||
| void APIServerConnection::on_ping_request(const PingRequest &msg) { | ||||
|   PingResponse ret = this->ping(msg); | ||||
|   if (!this->send_message(ret, PingResponse::MESSAGE_TYPE)) { | ||||
|   if (!this->send_ping_response(msg)) { | ||||
|     this->on_fatal_error(); | ||||
|   } | ||||
| } | ||||
| void APIServerConnection::on_device_info_request(const DeviceInfoRequest &msg) { | ||||
|   if (this->check_connection_setup_()) { | ||||
|     DeviceInfoResponse ret = this->device_info(msg); | ||||
|     if (!this->send_message(ret, DeviceInfoResponse::MESSAGE_TYPE)) { | ||||
|       this->on_fatal_error(); | ||||
|     } | ||||
|   if (this->check_connection_setup_() && !this->send_device_info_response(msg)) { | ||||
|     this->on_fatal_error(); | ||||
|   } | ||||
| } | ||||
| void APIServerConnection::on_list_entities_request(const ListEntitiesRequest &msg) { | ||||
| @@ -643,23 +641,24 @@ void APIServerConnection::on_subscribe_logs_request(const SubscribeLogsRequest & | ||||
|     this->subscribe_logs(msg); | ||||
|   } | ||||
| } | ||||
| #ifdef USE_API_HOMEASSISTANT_SERVICES | ||||
| void APIServerConnection::on_subscribe_homeassistant_services_request( | ||||
|     const SubscribeHomeassistantServicesRequest &msg) { | ||||
|   if (this->check_authenticated_()) { | ||||
|     this->subscribe_homeassistant_services(msg); | ||||
|   } | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_API_HOMEASSISTANT_STATES | ||||
| void APIServerConnection::on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) { | ||||
|   if (this->check_authenticated_()) { | ||||
|     this->subscribe_home_assistant_states(msg); | ||||
|   } | ||||
| } | ||||
| #endif | ||||
| void APIServerConnection::on_get_time_request(const GetTimeRequest &msg) { | ||||
|   if (this->check_connection_setup_()) { | ||||
|     GetTimeResponse ret = this->get_time(msg); | ||||
|     if (!this->send_message(ret, GetTimeResponse::MESSAGE_TYPE)) { | ||||
|       this->on_fatal_error(); | ||||
|     } | ||||
|   if (this->check_connection_setup_() && !this->send_get_time_response(msg)) { | ||||
|     this->on_fatal_error(); | ||||
|   } | ||||
| } | ||||
| #ifdef USE_API_SERVICES | ||||
| @@ -671,11 +670,8 @@ void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest | ||||
| #endif | ||||
| #ifdef USE_API_NOISE | ||||
| void APIServerConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) { | ||||
|   if (this->check_authenticated_()) { | ||||
|     NoiseEncryptionSetKeyResponse ret = this->noise_encryption_set_key(msg); | ||||
|     if (!this->send_message(ret, NoiseEncryptionSetKeyResponse::MESSAGE_TYPE)) { | ||||
|       this->on_fatal_error(); | ||||
|     } | ||||
|   if (this->check_authenticated_() && !this->send_noise_encryption_set_key_response(msg)) { | ||||
|     this->on_fatal_error(); | ||||
|   } | ||||
| } | ||||
| #endif | ||||
| @@ -865,11 +861,8 @@ void APIServerConnection::on_bluetooth_gatt_notify_request(const BluetoothGATTNo | ||||
| #ifdef USE_BLUETOOTH_PROXY | ||||
| void APIServerConnection::on_subscribe_bluetooth_connections_free_request( | ||||
|     const SubscribeBluetoothConnectionsFreeRequest &msg) { | ||||
|   if (this->check_authenticated_()) { | ||||
|     BluetoothConnectionsFreeResponse ret = this->subscribe_bluetooth_connections_free(msg); | ||||
|     if (!this->send_message(ret, BluetoothConnectionsFreeResponse::MESSAGE_TYPE)) { | ||||
|       this->on_fatal_error(); | ||||
|     } | ||||
|   if (this->check_authenticated_() && !this->send_subscribe_bluetooth_connections_free_response(msg)) { | ||||
|     this->on_fatal_error(); | ||||
|   } | ||||
| } | ||||
| #endif | ||||
| @@ -897,11 +890,8 @@ void APIServerConnection::on_subscribe_voice_assistant_request(const SubscribeVo | ||||
| #endif | ||||
| #ifdef USE_VOICE_ASSISTANT | ||||
| void APIServerConnection::on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) { | ||||
|   if (this->check_authenticated_()) { | ||||
|     VoiceAssistantConfigurationResponse ret = this->voice_assistant_get_configuration(msg); | ||||
|     if (!this->send_message(ret, VoiceAssistantConfigurationResponse::MESSAGE_TYPE)) { | ||||
|       this->on_fatal_error(); | ||||
|     } | ||||
|   if (this->check_authenticated_() && !this->send_voice_assistant_get_configuration_response(msg)) { | ||||
|     this->on_fatal_error(); | ||||
|   } | ||||
| } | ||||
| #endif | ||||
| @@ -920,5 +910,4 @@ void APIServerConnection::on_alarm_control_panel_command_request(const AlarmCont | ||||
| } | ||||
| #endif | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
| }  // namespace esphome::api | ||||
|   | ||||
| @@ -6,8 +6,7 @@ | ||||
|  | ||||
| #include "api_pb2.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace api { | ||||
| namespace esphome::api { | ||||
|  | ||||
| class APIServerConnectionBase : public ProtoService { | ||||
|  public: | ||||
| @@ -61,11 +60,17 @@ class APIServerConnectionBase : public ProtoService { | ||||
|   virtual void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &value){}; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_API_HOMEASSISTANT_SERVICES | ||||
|   virtual void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &value){}; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_API_HOMEASSISTANT_STATES | ||||
|   virtual void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &value){}; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_API_HOMEASSISTANT_STATES | ||||
|   virtual void on_home_assistant_state_response(const HomeAssistantStateResponse &value){}; | ||||
| #endif | ||||
|   virtual void on_get_time_request(const GetTimeRequest &value){}; | ||||
|   virtual void on_get_time_response(const GetTimeResponse &value){}; | ||||
|  | ||||
| @@ -207,22 +212,26 @@ class APIServerConnectionBase : public ProtoService { | ||||
|  | ||||
| class APIServerConnection : public APIServerConnectionBase { | ||||
|  public: | ||||
|   virtual HelloResponse hello(const HelloRequest &msg) = 0; | ||||
|   virtual ConnectResponse connect(const ConnectRequest &msg) = 0; | ||||
|   virtual DisconnectResponse disconnect(const DisconnectRequest &msg) = 0; | ||||
|   virtual PingResponse ping(const PingRequest &msg) = 0; | ||||
|   virtual DeviceInfoResponse device_info(const DeviceInfoRequest &msg) = 0; | ||||
|   virtual bool send_hello_response(const HelloRequest &msg) = 0; | ||||
|   virtual bool send_connect_response(const ConnectRequest &msg) = 0; | ||||
|   virtual bool send_disconnect_response(const DisconnectRequest &msg) = 0; | ||||
|   virtual bool send_ping_response(const PingRequest &msg) = 0; | ||||
|   virtual bool send_device_info_response(const DeviceInfoRequest &msg) = 0; | ||||
|   virtual void list_entities(const ListEntitiesRequest &msg) = 0; | ||||
|   virtual void subscribe_states(const SubscribeStatesRequest &msg) = 0; | ||||
|   virtual void subscribe_logs(const SubscribeLogsRequest &msg) = 0; | ||||
| #ifdef USE_API_HOMEASSISTANT_SERVICES | ||||
|   virtual void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) = 0; | ||||
| #endif | ||||
| #ifdef USE_API_HOMEASSISTANT_STATES | ||||
|   virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0; | ||||
|   virtual GetTimeResponse get_time(const GetTimeRequest &msg) = 0; | ||||
| #endif | ||||
|   virtual bool send_get_time_response(const GetTimeRequest &msg) = 0; | ||||
| #ifdef USE_API_SERVICES | ||||
|   virtual void execute_service(const ExecuteServiceRequest &msg) = 0; | ||||
| #endif | ||||
| #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 | ||||
| #ifdef USE_BUTTON | ||||
|   virtual void button_command(const ButtonCommandRequest &msg) = 0; | ||||
| @@ -303,7 +312,7 @@ class APIServerConnection : public APIServerConnectionBase { | ||||
|   virtual void bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) = 0; | ||||
| #endif | ||||
| #ifdef USE_BLUETOOTH_PROXY | ||||
|   virtual BluetoothConnectionsFreeResponse subscribe_bluetooth_connections_free( | ||||
|   virtual bool send_subscribe_bluetooth_connections_free_response( | ||||
|       const SubscribeBluetoothConnectionsFreeRequest &msg) = 0; | ||||
| #endif | ||||
| #ifdef USE_BLUETOOTH_PROXY | ||||
| @@ -316,8 +325,7 @@ class APIServerConnection : public APIServerConnectionBase { | ||||
|   virtual void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) = 0; | ||||
| #endif | ||||
| #ifdef USE_VOICE_ASSISTANT | ||||
|   virtual VoiceAssistantConfigurationResponse voice_assistant_get_configuration( | ||||
|       const VoiceAssistantConfigurationRequest &msg) = 0; | ||||
|   virtual bool send_voice_assistant_get_configuration_response(const VoiceAssistantConfigurationRequest &msg) = 0; | ||||
| #endif | ||||
| #ifdef USE_VOICE_ASSISTANT | ||||
|   virtual void voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) = 0; | ||||
| @@ -334,8 +342,12 @@ class APIServerConnection : public APIServerConnectionBase { | ||||
|   void on_list_entities_request(const ListEntitiesRequest &msg) override; | ||||
|   void on_subscribe_states_request(const SubscribeStatesRequest &msg) override; | ||||
|   void on_subscribe_logs_request(const SubscribeLogsRequest &msg) override; | ||||
| #ifdef USE_API_HOMEASSISTANT_SERVICES | ||||
|   void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &msg) override; | ||||
| #endif | ||||
| #ifdef USE_API_HOMEASSISTANT_STATES | ||||
|   void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override; | ||||
| #endif | ||||
|   void on_get_time_request(const GetTimeRequest &msg) override; | ||||
| #ifdef USE_API_SERVICES | ||||
|   void on_execute_service_request(const ExecuteServiceRequest &msg) override; | ||||
| @@ -445,5 +457,4 @@ class APIServerConnection : public APIServerConnectionBase { | ||||
| #endif | ||||
| }; | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
| }  // namespace esphome::api | ||||
|   | ||||
| @@ -16,8 +16,7 @@ | ||||
|  | ||||
| #include <algorithm> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace api { | ||||
| namespace esphome::api { | ||||
|  | ||||
| static const char *const TAG = "api"; | ||||
|  | ||||
| @@ -184,9 +183,9 @@ void APIServer::loop() { | ||||
|  | ||||
|     // Rare case: handle disconnection | ||||
| #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 | ||||
|     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) | ||||
|     if (client_index < this->clients_.size() - 1) { | ||||
| @@ -370,12 +369,15 @@ void APIServer::set_password(const std::string &password) { this->password_ = pa | ||||
|  | ||||
| void APIServer::set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = batch_delay; } | ||||
|  | ||||
| #ifdef USE_API_HOMEASSISTANT_SERVICES | ||||
| void APIServer::send_homeassistant_service_call(const HomeassistantServiceResponse &call) { | ||||
|   for (auto &client : this->clients_) { | ||||
|     client->send_homeassistant_service_call(call); | ||||
|   } | ||||
| } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_API_HOMEASSISTANT_STATES | ||||
| void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute, | ||||
|                                                std::function<void(std::string)> f) { | ||||
|   this->state_subs_.push_back(HomeAssistantStateSubscription{ | ||||
| @@ -399,6 +401,7 @@ void APIServer::get_home_assistant_state(std::string entity_id, optional<std::st | ||||
| const std::vector<APIServer::HomeAssistantStateSubscription> &APIServer::get_state_subs() const { | ||||
|   return this->state_subs_; | ||||
| } | ||||
| #endif | ||||
|  | ||||
| uint16_t APIServer::get_port() const { return this->port_; } | ||||
|  | ||||
| @@ -483,6 +486,5 @@ bool APIServer::teardown() { | ||||
|   return this->clients_.empty(); | ||||
| } | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
| }  // namespace esphome::api | ||||
| #endif | ||||
|   | ||||
| @@ -18,8 +18,7 @@ | ||||
|  | ||||
| #include <vector> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace api { | ||||
| namespace esphome::api { | ||||
|  | ||||
| #ifdef USE_API_NOISE | ||||
| struct SavedNoisePsk { | ||||
| @@ -107,7 +106,9 @@ class APIServer : public Component, public Controller { | ||||
| #ifdef USE_MEDIA_PLAYER | ||||
|   void on_media_player_update(media_player::MediaPlayer *obj) override; | ||||
| #endif | ||||
| #ifdef USE_API_HOMEASSISTANT_SERVICES | ||||
|   void send_homeassistant_service_call(const HomeassistantServiceResponse &call); | ||||
| #endif | ||||
| #ifdef USE_API_SERVICES | ||||
|   void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } | ||||
| #endif | ||||
| @@ -127,6 +128,7 @@ class APIServer : public Component, public Controller { | ||||
|  | ||||
|   bool is_connected() const; | ||||
|  | ||||
| #ifdef USE_API_HOMEASSISTANT_STATES | ||||
|   struct HomeAssistantStateSubscription { | ||||
|     std::string entity_id; | ||||
|     optional<std::string> attribute; | ||||
| @@ -139,6 +141,7 @@ class APIServer : public Component, public Controller { | ||||
|   void get_home_assistant_state(std::string entity_id, optional<std::string> attribute, | ||||
|                                 std::function<void(std::string)> f); | ||||
|   const std::vector<HomeAssistantStateSubscription> &get_state_subs() const; | ||||
| #endif | ||||
| #ifdef USE_API_SERVICES | ||||
|   const std::vector<UserServiceDescriptor *> &get_user_services() const { return this->user_services_; } | ||||
| #endif | ||||
| @@ -172,7 +175,9 @@ class APIServer : public Component, public Controller { | ||||
|   std::string password_; | ||||
| #endif | ||||
|   std::vector<uint8_t> shared_write_buffer_;  // Shared proto write buffer for all connections | ||||
| #ifdef USE_API_HOMEASSISTANT_STATES | ||||
|   std::vector<HomeAssistantStateSubscription> state_subs_; | ||||
| #endif | ||||
| #ifdef USE_API_SERVICES | ||||
|   std::vector<UserServiceDescriptor *> user_services_; | ||||
| #endif | ||||
| @@ -196,6 +201,5 @@ template<typename... Ts> class APIConnectedCondition : public Condition<Ts...> { | ||||
|   bool check(Ts... x) override { return global_api_server->is_connected(); } | ||||
| }; | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
| }  // namespace esphome::api | ||||
| #endif | ||||
|   | ||||
| @@ -14,6 +14,8 @@ with warnings.catch_warnings(): | ||||
|     from aioesphomeapi import APIClient, parse_log_message | ||||
|     from aioesphomeapi.log_runner import async_run | ||||
|  | ||||
| import contextlib | ||||
|  | ||||
| from esphome.const import CONF_KEY, CONF_PASSWORD, CONF_PORT, __version__ | ||||
| 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: | ||||
|     """Run the logs command.""" | ||||
|     try: | ||||
|     with contextlib.suppress(KeyboardInterrupt): | ||||
|         asyncio.run(async_run_logs(config, address)) | ||||
|     except KeyboardInterrupt: | ||||
|         pass | ||||
|   | ||||
| @@ -6,8 +6,7 @@ | ||||
| #ifdef USE_API_SERVICES | ||||
| #include "user_services.h" | ||||
| #endif | ||||
| namespace esphome { | ||||
| namespace api { | ||||
| namespace esphome::api { | ||||
|  | ||||
| #ifdef USE_API_SERVICES | ||||
| template<typename T, typename... Ts> class CustomAPIDeviceService : public UserServiceBase<Ts...> { | ||||
| @@ -84,6 +83,7 @@ class CustomAPIDevice { | ||||
|   } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_API_HOMEASSISTANT_STATES | ||||
|   /** Subscribe to the state (or attribute state) of an entity from Home Assistant. | ||||
|    * | ||||
|    * Usage: | ||||
| @@ -135,7 +135,9 @@ class CustomAPIDevice { | ||||
|     auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1); | ||||
|     global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), f); | ||||
|   } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_API_HOMEASSISTANT_SERVICES | ||||
|   /** Call a Home Assistant service from ESPHome. | ||||
|    * | ||||
|    * Usage: | ||||
| @@ -148,7 +150,7 @@ class CustomAPIDevice { | ||||
|    */ | ||||
|   void call_homeassistant_service(const std::string &service_name) { | ||||
|     HomeassistantServiceResponse resp; | ||||
|     resp.service = service_name; | ||||
|     resp.set_service(StringRef(service_name)); | ||||
|     global_api_server->send_homeassistant_service_call(resp); | ||||
|   } | ||||
|  | ||||
| @@ -168,12 +170,12 @@ class CustomAPIDevice { | ||||
|    */ | ||||
|   void call_homeassistant_service(const std::string &service_name, const std::map<std::string, std::string> &data) { | ||||
|     HomeassistantServiceResponse resp; | ||||
|     resp.service = service_name; | ||||
|     resp.set_service(StringRef(service_name)); | ||||
|     for (auto &it : data) { | ||||
|       HomeassistantServiceMap kv; | ||||
|       kv.key = it.first; | ||||
|       resp.data.emplace_back(); | ||||
|       auto &kv = resp.data.back(); | ||||
|       kv.set_key(StringRef(it.first)); | ||||
|       kv.value = it.second; | ||||
|       resp.data.push_back(kv); | ||||
|     } | ||||
|     global_api_server->send_homeassistant_service_call(resp); | ||||
|   } | ||||
| @@ -190,7 +192,7 @@ class CustomAPIDevice { | ||||
|    */ | ||||
|   void fire_homeassistant_event(const std::string &event_name) { | ||||
|     HomeassistantServiceResponse resp; | ||||
|     resp.service = event_name; | ||||
|     resp.set_service(StringRef(event_name)); | ||||
|     resp.is_event = true; | ||||
|     global_api_server->send_homeassistant_service_call(resp); | ||||
|   } | ||||
| @@ -210,18 +212,18 @@ class CustomAPIDevice { | ||||
|    */ | ||||
|   void fire_homeassistant_event(const std::string &service_name, const std::map<std::string, std::string> &data) { | ||||
|     HomeassistantServiceResponse resp; | ||||
|     resp.service = service_name; | ||||
|     resp.set_service(StringRef(service_name)); | ||||
|     resp.is_event = true; | ||||
|     for (auto &it : data) { | ||||
|       HomeassistantServiceMap kv; | ||||
|       kv.key = it.first; | ||||
|       resp.data.emplace_back(); | ||||
|       auto &kv = resp.data.back(); | ||||
|       kv.set_key(StringRef(it.first)); | ||||
|       kv.value = it.second; | ||||
|       resp.data.push_back(kv); | ||||
|     } | ||||
|     global_api_server->send_homeassistant_service_call(resp); | ||||
|   } | ||||
| #endif | ||||
| }; | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
| }  // namespace esphome::api | ||||
| #endif | ||||
|   | ||||
| @@ -2,13 +2,13 @@ | ||||
|  | ||||
| #include "api_server.h" | ||||
| #ifdef USE_API | ||||
| #ifdef USE_API_HOMEASSISTANT_SERVICES | ||||
| #include "api_pb2.h" | ||||
| #include "esphome/core/automation.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include <vector> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace api { | ||||
| namespace esphome::api { | ||||
|  | ||||
| template<typename... X> class TemplatableStringValue : public TemplatableValue<std::string, X...> { | ||||
|  private: | ||||
| @@ -36,6 +36,9 @@ template<typename... X> class TemplatableStringValue : public TemplatableValue<s | ||||
|  | ||||
| template<typename... Ts> class TemplatableKeyValuePair { | ||||
|  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) {} | ||||
|   std::string key; | ||||
|   TemplatableStringValue<Ts...> value; | ||||
| @@ -47,37 +50,39 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts | ||||
|  | ||||
|   template<typename T> void set_service(T service) { this->service_ = service; } | ||||
|  | ||||
|   template<typename T> void add_data(std::string key, T value) { | ||||
|     this->data_.push_back(TemplatableKeyValuePair<Ts...>(key, value)); | ||||
|   } | ||||
|   // Keys are always string literals from the Python code generation (e.g., cg.add(var.add_data("tag_id", templ))). | ||||
|   // 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) { | ||||
|     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) { | ||||
|     this->variables_.push_back(TemplatableKeyValuePair<Ts...>(key, value)); | ||||
|     this->variables_.emplace_back(std::move(key), value); | ||||
|   } | ||||
|  | ||||
|   void play(Ts... x) override { | ||||
|     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_; | ||||
|     for (auto &it : this->data_) { | ||||
|       HomeassistantServiceMap kv; | ||||
|       kv.key = it.key; | ||||
|       resp.data.emplace_back(); | ||||
|       auto &kv = resp.data.back(); | ||||
|       kv.set_key(StringRef(it.key)); | ||||
|       kv.value = it.value.value(x...); | ||||
|       resp.data.push_back(kv); | ||||
|     } | ||||
|     for (auto &it : this->data_template_) { | ||||
|       HomeassistantServiceMap kv; | ||||
|       kv.key = it.key; | ||||
|       resp.data_template.emplace_back(); | ||||
|       auto &kv = resp.data_template.back(); | ||||
|       kv.set_key(StringRef(it.key)); | ||||
|       kv.value = it.value.value(x...); | ||||
|       resp.data_template.push_back(kv); | ||||
|     } | ||||
|     for (auto &it : this->variables_) { | ||||
|       HomeassistantServiceMap kv; | ||||
|       kv.key = it.key; | ||||
|       resp.variables.emplace_back(); | ||||
|       auto &kv = resp.variables.back(); | ||||
|       kv.set_key(StringRef(it.key)); | ||||
|       kv.value = it.value.value(x...); | ||||
|       resp.variables.push_back(kv); | ||||
|     } | ||||
|     this->parent_->send_homeassistant_service_call(resp); | ||||
|   } | ||||
| @@ -91,6 +96,6 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts | ||||
|   std::vector<TemplatableKeyValuePair<Ts...>> variables_; | ||||
| }; | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
| }  // namespace esphome::api | ||||
| #endif | ||||
| #endif | ||||
|   | ||||
| @@ -6,8 +6,7 @@ | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/util.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace api { | ||||
| namespace esphome::api { | ||||
|  | ||||
| // Generate entity handler implementations using macros | ||||
| #ifdef USE_BINARY_SENSOR | ||||
| @@ -90,6 +89,5 @@ bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) { | ||||
| } | ||||
| #endif | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
| }  // namespace esphome::api | ||||
| #endif | ||||
|   | ||||
| @@ -4,8 +4,7 @@ | ||||
| #ifdef USE_API | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/component_iterator.h" | ||||
| namespace esphome { | ||||
| namespace api { | ||||
| namespace esphome::api { | ||||
|  | ||||
| class APIConnection; | ||||
|  | ||||
| @@ -96,6 +95,5 @@ class ListEntitiesIterator : public ComponentIterator { | ||||
|   APIConnection *client_; | ||||
| }; | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
| }  // namespace esphome::api | ||||
| #endif | ||||
|   | ||||
| @@ -3,8 +3,7 @@ | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace api { | ||||
| namespace esphome::api { | ||||
|  | ||||
| static const char *const TAG = "api.proto"; | ||||
|  | ||||
| @@ -89,5 +88,4 @@ std::string ProtoMessage::dump() const { | ||||
| } | ||||
| #endif | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
| }  // namespace esphome::api | ||||
|   | ||||
| @@ -3,16 +3,47 @@ | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/string_ref.h" | ||||
|  | ||||
| #include <cassert> | ||||
| #include <cstring> | ||||
| #include <vector> | ||||
|  | ||||
| #ifdef ESPHOME_LOG_HAS_VERY_VERBOSE | ||||
| #define HAS_PROTO_MESSAGE_DUMP | ||||
| #endif | ||||
|  | ||||
| namespace esphome { | ||||
| namespace api { | ||||
| namespace esphome::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. Concatenation: msg.set_field(StringRef(str1 + str2)) // Result is temporary | ||||
|  * | ||||
|  * For unsafe patterns, store in a local variable first: | ||||
|  *    std::string temp = 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 | ||||
| class ProtoVarInt { | ||||
| @@ -206,12 +237,20 @@ class ProtoWriteBuffer { | ||||
|  | ||||
|     this->encode_field_raw(field_id, 2);  // type 2: Length-delimited string | ||||
|     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) { | ||||
|     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) { | ||||
|     this->encode_string(field_id, reinterpret_cast<const char *>(data), len, force); | ||||
|   } | ||||
| @@ -527,25 +566,6 @@ class ProtoSize { | ||||
|     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 | ||||
|    */ | ||||
| @@ -682,17 +702,16 @@ class ProtoSize { | ||||
|   // 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 | ||||
|     if (str.empty()) { | ||||
|     if (len == 0) { | ||||
|       return;  // No need to update total_size | ||||
|     } | ||||
|  | ||||
|     // Calculate and directly add to total_size | ||||
|     const uint32_t str_size = static_cast<uint32_t>(str.size()); | ||||
|     total_size += field_id_size + varint(str_size) + str_size; | ||||
|     // Field ID + length varint + string bytes | ||||
|     total_size += field_id_size + varint(static_cast<uint32_t>(len)) + static_cast<uint32_t>(len); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
| @@ -704,6 +723,19 @@ class ProtoSize { | ||||
|     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 | ||||
|    * | ||||
| @@ -827,7 +859,9 @@ class ProtoService { | ||||
|   virtual bool is_authenticated() = 0; | ||||
|   virtual bool is_connection_setup() = 0; | ||||
|   virtual void on_fatal_error() = 0; | ||||
| #ifdef USE_API_PASSWORD | ||||
|   virtual void on_unauthenticated_access() = 0; | ||||
| #endif | ||||
|   virtual void on_no_setup_connection() = 0; | ||||
|   /** | ||||
|    * Create a buffer with a reserved size. | ||||
| @@ -865,6 +899,7 @@ class ProtoService { | ||||
|   } | ||||
|  | ||||
|   bool check_authenticated_() { | ||||
| #ifdef USE_API_PASSWORD | ||||
|     if (!this->check_connection_setup_()) { | ||||
|       return false; | ||||
|     } | ||||
| @@ -873,8 +908,10 @@ class ProtoService { | ||||
|       return false; | ||||
|     } | ||||
|     return true; | ||||
| #else | ||||
|     return this->check_connection_setup_(); | ||||
| #endif | ||||
|   } | ||||
| }; | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
| }  // namespace esphome::api | ||||
|   | ||||
| @@ -3,8 +3,7 @@ | ||||
| #include "api_connection.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace api { | ||||
| namespace esphome::api { | ||||
|  | ||||
| // Generate entity handler implementations using macros | ||||
| #ifdef USE_BINARY_SENSOR | ||||
| @@ -69,6 +68,5 @@ INITIAL_STATE_HANDLER(update, update::UpdateEntity) | ||||
|  | ||||
| InitialStateIterator::InitialStateIterator(APIConnection *client) : client_(client) {} | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
| }  // namespace esphome::api | ||||
| #endif | ||||
|   | ||||
| @@ -5,8 +5,7 @@ | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/component_iterator.h" | ||||
| #include "esphome/core/controller.h" | ||||
| namespace esphome { | ||||
| namespace api { | ||||
| namespace esphome::api { | ||||
|  | ||||
| class APIConnection; | ||||
|  | ||||
| @@ -89,6 +88,5 @@ class InitialStateIterator : public ComponentIterator { | ||||
|   APIConnection *client_; | ||||
| }; | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
| }  // namespace esphome::api | ||||
| #endif | ||||
|   | ||||
| @@ -1,8 +1,7 @@ | ||||
| #include "user_services.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace api { | ||||
| namespace esphome::api { | ||||
|  | ||||
| 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) { | ||||
| @@ -40,5 +39,4 @@ template<> enums::ServiceArgType to_service_arg_type<std::vector<std::string>>() | ||||
|   return enums::SERVICE_ARG_TYPE_STRING_ARRAY; | ||||
| } | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
| }  // namespace esphome::api | ||||
|   | ||||
| @@ -8,8 +8,7 @@ | ||||
| #include "api_pb2.h" | ||||
|  | ||||
| #ifdef USE_API_SERVICES | ||||
| namespace esphome { | ||||
| namespace api { | ||||
| namespace esphome::api { | ||||
|  | ||||
| class UserServiceDescriptor { | ||||
|  public: | ||||
| @@ -33,14 +32,14 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor { | ||||
|  | ||||
|   ListEntitiesServicesResponse encode_list_service_response() override { | ||||
|     ListEntitiesServicesResponse msg; | ||||
|     msg.name = this->name_; | ||||
|     msg.set_name(StringRef(this->name_)); | ||||
|     msg.key = this->key_; | ||||
|     std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...}; | ||||
|     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.name = this->arg_names_[i]; | ||||
|       msg.args.push_back(arg); | ||||
|       arg.set_name(StringRef(this->arg_names_[i])); | ||||
|     } | ||||
|     return msg; | ||||
|   } | ||||
| @@ -74,6 +73,5 @@ template<typename... Ts> class UserServiceTrigger : public UserServiceBase<Ts... | ||||
|   void execute(Ts... x) override { this->trigger(x...); }  // NOLINT | ||||
| }; | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
| }  // namespace esphome::api | ||||
| #endif  // USE_API_SERVICES | ||||
|   | ||||
| @@ -7,8 +7,6 @@ namespace as3935 { | ||||
| static const char *const TAG = "as3935"; | ||||
|  | ||||
| void AS3935Component::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
|  | ||||
|   this->irq_pin_->setup(); | ||||
|   LOG_PIN("  IRQ Pin: ", this->irq_pin_); | ||||
|  | ||||
|   | ||||
| @@ -7,9 +7,7 @@ namespace as3935_spi { | ||||
| static const char *const TAG = "as3935_spi"; | ||||
|  | ||||
| void SPIAS3935Component::setup() { | ||||
|   ESP_LOGI(TAG, "SPIAS3935Component setup started!"); | ||||
|   this->spi_setup(); | ||||
|   ESP_LOGI(TAG, "SPI setup finished!"); | ||||
|   AS3935Component::setup(); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -23,8 +23,6 @@ static const uint8_t REGISTER_AGC = 0x1A;        // 8 bytes  / R | ||||
| static const uint8_t REGISTER_MAGNITUDE = 0x1B;  // 16 bytes / R | ||||
|  | ||||
| void AS5600Component::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
|  | ||||
|   if (!this->read_byte(REGISTER_STATUS).has_value()) { | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   | ||||
| @@ -8,7 +8,6 @@ namespace as7341 { | ||||
| static const char *const TAG = "as7341"; | ||||
|  | ||||
| void AS7341Component::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
|   LOG_I2C_DEVICE(this); | ||||
|  | ||||
|   // Verify device ID | ||||
|   | ||||
| @@ -71,7 +71,7 @@ bool AT581XComponent::i2c_read_reg(uint8_t addr, uint8_t &data) { | ||||
|   return this->read_register(addr, &data, 1) == esphome::i2c::NO_ERROR; | ||||
| } | ||||
|  | ||||
| void AT581XComponent::setup() { ESP_LOGCONFIG(TAG, "Running setup"); } | ||||
| void AT581XComponent::setup() {} | ||||
| void AT581XComponent::dump_config() { LOG_I2C_DEVICE(this); } | ||||
| #define ARRAY_SIZE(X) (sizeof(X) / sizeof((X)[0])) | ||||
| bool AT581XComponent::i2c_write_config() { | ||||
|   | ||||
| @@ -41,7 +41,6 @@ void ATM90E26Component::update() { | ||||
| } | ||||
|  | ||||
| void ATM90E26Component::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
|   this->spi_setup(); | ||||
|  | ||||
|   uint16_t mmode = 0x422;  // default values for everything but L/N line current gains | ||||
|   | ||||
| @@ -109,7 +109,6 @@ void ATM90E32Component::update() { | ||||
| } | ||||
|  | ||||
| void ATM90E32Component::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
|   this->spi_setup(); | ||||
|  | ||||
|   uint16_t mmode0 = 0x87;  // 3P4W 50Hz | ||||
|   | ||||
| @@ -15,7 +15,7 @@ class AudioStreamInfo { | ||||
|    *  - An audio sample represents a unit of audio for one 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; | ||||
|    * 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. | ||||
| @@ -76,7 +76,7 @@ class AudioStreamInfo { | ||||
|  | ||||
|   /// @brief Computes the duration, in microseconds, the given amount of frames represents. | ||||
|   /// @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. | ||||
|   uint32_t frames_to_microseconds(uint32_t frames) const; | ||||
|  | ||||
|   | ||||
| @@ -17,7 +17,6 @@ constexpr static const uint8_t AXS_READ_TOUCHPAD[11] = {0xb5, 0xab, 0xa5, 0x5a, | ||||
|   } | ||||
|  | ||||
| void AXS15231Touchscreen::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
|   if (this->reset_pin_ != nullptr) { | ||||
|     this->reset_pin_->setup(); | ||||
|     this->reset_pin_->digital_write(false); | ||||
| @@ -36,7 +35,6 @@ void AXS15231Touchscreen::setup() { | ||||
|   if (this->y_raw_max_ == 0) { | ||||
|     this->y_raw_max_ = this->display_->get_native_height(); | ||||
|   } | ||||
|   ESP_LOGCONFIG(TAG, "AXS15231 Touchscreen setup complete"); | ||||
| } | ||||
|  | ||||
| void AXS15231Touchscreen::update_touches() { | ||||
|   | ||||
| @@ -121,8 +121,6 @@ void spi_dma_tx_finish_callback(unsigned int param) { | ||||
| } | ||||
|  | ||||
| void BekenSPILEDStripLightOutput::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
|  | ||||
|   size_t buffer_size = this->get_buffer_size_(); | ||||
|   size_t dma_buffer_size = (buffer_size * 8) + (2 * 64); | ||||
|  | ||||
|   | ||||
| @@ -38,7 +38,6 @@ MTreg: | ||||
| */ | ||||
|  | ||||
| void BH1750Sensor::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->name_.c_str()); | ||||
|   uint8_t turn_on = BH1750_COMMAND_POWER_ON; | ||||
|   if (this->write(&turn_on, 1) != i2c::ERROR_OK) { | ||||
|     this->mark_failed(); | ||||
|   | ||||
| @@ -266,8 +266,10 @@ async def delayed_off_filter_to_code(config, filter_id): | ||||
| async def autorepeat_filter_to_code(config, filter_id): | ||||
|     timings = [] | ||||
|     if len(config) > 0: | ||||
|         for conf in config: | ||||
|             timings.append((conf[CONF_DELAY], conf[CONF_TIME_OFF], conf[CONF_TIME_ON])) | ||||
|         timings.extend( | ||||
|             (conf[CONF_DELAY], conf[CONF_TIME_OFF], conf[CONF_TIME_ON]) | ||||
|             for conf in config | ||||
|         ) | ||||
|     else: | ||||
|         timings.append( | ||||
|             ( | ||||
| @@ -573,16 +575,15 @@ async def setup_binary_sensor_core_(var, config): | ||||
|         await automation.build_automation(trigger, [], conf) | ||||
|  | ||||
|     for conf in config.get(CONF_ON_MULTI_CLICK, []): | ||||
|         timings = [] | ||||
|         for tim in conf[CONF_TIMING]: | ||||
|             timings.append( | ||||
|                 cg.StructInitializer( | ||||
|                     MultiClickTriggerEvent, | ||||
|                     ("state", tim[CONF_STATE]), | ||||
|                     ("min_length", tim[CONF_MIN_LENGTH]), | ||||
|                     ("max_length", tim.get(CONF_MAX_LENGTH, 4294967294)), | ||||
|                 ) | ||||
|         timings = [ | ||||
|             cg.StructInitializer( | ||||
|                 MultiClickTriggerEvent, | ||||
|                 ("state", tim[CONF_STATE]), | ||||
|                 ("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) | ||||
|         if CONF_INVALID_COOLDOWN in conf: | ||||
|             cg.add(trigger.set_invalid_cooldown(conf[CONF_INVALID_COOLDOWN])) | ||||
|   | ||||
| @@ -8,16 +8,184 @@ | ||||
|  | ||||
| #include "bluetooth_proxy.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace bluetooth_proxy { | ||||
| namespace esphome::bluetooth_proxy { | ||||
|  | ||||
| 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() { | ||||
|   ESP_LOGCONFIG(TAG, "BLE Connection:"); | ||||
|   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 || service_count == 0) { | ||||
|     ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_service %s, status=%d, service_count=%d, offset=%d", | ||||
|              this->connection_index_, this->address_str().c_str(), service_status != ESP_GATT_OK ? "error" : "missing", | ||||
|              service_status, service_count, this->send_service_ - 1); | ||||
|     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) { | ||||
|     ESP_LOGW(TAG, "[%d] [%s] Error getting characteristic count, status=%d", this->connection_index_, | ||||
|              this->address_str().c_str(), char_count_status); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   if (total_char_count == 0) { | ||||
|     // No characteristics, just send the service response | ||||
|     api_conn->send_message(resp, api::BluetoothGATTGetServicesResponse::MESSAGE_TYPE); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   // Reserve space and process characteristics | ||||
|   service_resp.characteristics.reserve(total_char_count); | ||||
|   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); | ||||
|       return; | ||||
|     } | ||||
|     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, 0, 0, char_result.char_handle, &total_desc_count); | ||||
|  | ||||
|     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); | ||||
|       return; | ||||
|     } | ||||
|     if (total_desc_count == 0) { | ||||
|       // No descriptors, continue to next characteristic | ||||
|       continue; | ||||
|     } | ||||
|  | ||||
|     // Reserve space and process descriptors | ||||
|     characteristic_resp.descriptors.reserve(total_desc_count); | ||||
|     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); | ||||
|         return; | ||||
|       } | ||||
|       if (desc_count == 0) { | ||||
|         break;  // No more descriptors | ||||
|       } | ||||
|  | ||||
|       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, | ||||
|                                               esp_ble_gattc_cb_param_t *param) { | ||||
|   if (!BLEClientBase::gattc_event_handler(event, gattc_if, param)) | ||||
| @@ -25,22 +193,16 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga | ||||
|  | ||||
|   switch (event) { | ||||
|     case ESP_GATTC_DISCONNECT_EVT: { | ||||
|       this->proxy_->send_device_connection(this->address_, false, 0, param->disconnect.reason); | ||||
|       this->set_address(0); | ||||
|       this->proxy_->send_connections_free(); | ||||
|       this->reset_connection_(param->disconnect.reason); | ||||
|       break; | ||||
|     } | ||||
|     case ESP_GATTC_CLOSE_EVT: { | ||||
|       this->proxy_->send_device_connection(this->address_, false, 0, param->close.reason); | ||||
|       this->set_address(0); | ||||
|       this->proxy_->send_connections_free(); | ||||
|       this->reset_connection_(param->close.reason); | ||||
|       break; | ||||
|     } | ||||
|     case ESP_GATTC_OPEN_EVT: { | ||||
|       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->set_address(0); | ||||
|         this->proxy_->send_connections_free(); | ||||
|         this->reset_connection_(param->open.status); | ||||
|       } else if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) { | ||||
|         this->proxy_->send_device_connection(this->address_, true, this->mtu_); | ||||
|         this->proxy_->send_connections_free(); | ||||
| @@ -72,9 +234,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga | ||||
|       api::BluetoothGATTReadResponse resp; | ||||
|       resp.address = this->address_; | ||||
|       resp.handle = param->read.handle; | ||||
|       resp.data.reserve(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); | ||||
|       resp.set_data(param->read.value, param->read.value_len); | ||||
|       this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTReadResponse::MESSAGE_TYPE); | ||||
|       break; | ||||
|     } | ||||
| @@ -125,9 +285,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga | ||||
|       api::BluetoothGATTNotifyDataResponse resp; | ||||
|       resp.address = this->address_; | ||||
|       resp.handle = param->notify.handle; | ||||
|       resp.data.reserve(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); | ||||
|       resp.set_data(param->notify.value, param->notify.value_len); | ||||
|       this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTNotifyDataResponse::MESSAGE_TYPE); | ||||
|       break; | ||||
|     } | ||||
| @@ -265,7 +423,6 @@ esp32_ble_tracker::AdvertisementParserType BluetoothConnection::get_advertisemen | ||||
|   return this->proxy_->get_advertisement_parser_type(); | ||||
| } | ||||
|  | ||||
| }  // namespace bluetooth_proxy | ||||
| }  // namespace esphome | ||||
| }  // namespace esphome::bluetooth_proxy | ||||
|  | ||||
| #endif  // USE_ESP32 | ||||
|   | ||||
| @@ -4,14 +4,14 @@ | ||||
|  | ||||
| #include "esphome/components/esp32_ble_client/ble_client_base.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace bluetooth_proxy { | ||||
| namespace esphome::bluetooth_proxy { | ||||
|  | ||||
| class BluetoothProxy; | ||||
|  | ||||
| class BluetoothConnection : public esp32_ble_client::BLEClientBase { | ||||
|  public: | ||||
|   void dump_config() override; | ||||
|   void loop() override; | ||||
|   bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, | ||||
|                            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; | ||||
| @@ -27,6 +27,9 @@ class BluetoothConnection : public esp32_ble_client::BLEClientBase { | ||||
|  protected: | ||||
|   friend class BluetoothProxy; | ||||
|  | ||||
|   void send_service_for_discovery_(); | ||||
|   void reset_connection_(esp_err_t reason); | ||||
|  | ||||
|   // Memory optimized layout for 32-bit systems | ||||
|   // Group 1: Pointers (4 bytes each, naturally aligned) | ||||
|   BluetoothProxy *proxy_; | ||||
| @@ -39,7 +42,6 @@ class BluetoothConnection : public esp32_ble_client::BLEClientBase { | ||||
|   // 1 byte used, 1 byte padding | ||||
| }; | ||||
|  | ||||
| }  // namespace bluetooth_proxy | ||||
| }  // namespace esphome | ||||
| }  // namespace esphome::bluetooth_proxy | ||||
|  | ||||
| #endif  // USE_ESP32 | ||||
|   | ||||
| @@ -3,30 +3,38 @@ | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/macros.h" | ||||
| #include "esphome/core/application.h" | ||||
| #include <cstring> | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| namespace esphome { | ||||
| namespace bluetooth_proxy { | ||||
| namespace esphome::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 | ||||
| // Each advertisement is up to 80 bytes when packaged (including protocol overhead) | ||||
| // Most advertisements are 20-30 bytes, allowing even more to fit per packet | ||||
| // 16 advertisements × 80 bytes (worst case) = 1280 bytes out of ~1320 bytes usable payload | ||||
| // This achieves ~97% WiFi MTU utilization while staying under the limit | ||||
| static constexpr size_t FLUSH_BATCH_SIZE = 16; | ||||
|  | ||||
| // Verify BLE advertisement data array size matches the BLE specification (31 bytes adv + 31 bytes scan response) | ||||
| static_assert(sizeof(((api::BluetoothLERawAdvertisement *) nullptr)->data) == 62, | ||||
|               "BLE advertisement data array size mismatch"); | ||||
|  | ||||
| BluetoothProxy::BluetoothProxy() { global_bluetooth_proxy = this; } | ||||
|  | ||||
| void BluetoothProxy::setup() { | ||||
|   // Pre-allocate response object | ||||
|   this->response_ = std::make_unique<api::BluetoothLERawAdvertisementsResponse>(); | ||||
|  | ||||
|   // Reserve capacity but start with size 0 | ||||
|   // Reserve 50% since we'll grow naturally and flush at FLUSH_BATCH_SIZE | ||||
|   this->response_->advertisements.reserve(FLUSH_BATCH_SIZE / 2); | ||||
|  | ||||
|   // Don't pre-allocate pool - let it grow only if needed in busy environments | ||||
|   // Many devices in quiet areas will never need the overflow pool | ||||
|  | ||||
|   this->parent_->add_scanner_state_callback([this](esp32_ble_tracker::ScannerState state) { | ||||
|     if (this->api_connection_ != nullptr) { | ||||
|       this->send_bluetooth_scanner_state_(state); | ||||
| @@ -50,110 +58,74 @@ bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device) | ||||
| } | ||||
| #endif | ||||
|  | ||||
| // Batch size for BLE advertisements to maximize WiFi efficiency | ||||
| // Each advertisement is up to 80 bytes when packaged (including protocol overhead) | ||||
| // Most advertisements are 20-30 bytes, allowing even more to fit per packet | ||||
| // 16 advertisements × 80 bytes (worst case) = 1280 bytes out of ~1320 bytes usable payload | ||||
| // This achieves ~97% WiFi MTU utilization while staying under the limit | ||||
| static constexpr size_t FLUSH_BATCH_SIZE = 16; | ||||
|  | ||||
| namespace { | ||||
| // Batch buffer in anonymous namespace to avoid guard variable (saves 8 bytes) | ||||
| // This is initialized at program startup before any threads | ||||
| // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) | ||||
| std::vector<api::BluetoothLERawAdvertisement> batch_buffer; | ||||
| }  // namespace | ||||
|  | ||||
| static std::vector<api::BluetoothLERawAdvertisement> &get_batch_buffer() { return batch_buffer; } | ||||
|  | ||||
| bool BluetoothProxy::parse_devices(const esp32_ble::BLEScanResult *scan_results, size_t count) { | ||||
|   if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr) | ||||
|     return false; | ||||
|  | ||||
|   // Get the batch buffer reference | ||||
|   auto &batch_buffer = get_batch_buffer(); | ||||
|   auto &advertisements = this->response_->advertisements; | ||||
|  | ||||
|   // Reserve additional capacity if needed | ||||
|   size_t new_size = batch_buffer.size() + count; | ||||
|   if (batch_buffer.capacity() < new_size) { | ||||
|     batch_buffer.reserve(new_size); | ||||
|   } | ||||
|  | ||||
|   // Add new advertisements to the batch buffer | ||||
|   for (size_t i = 0; i < count; i++) { | ||||
|     auto &result = scan_results[i]; | ||||
|     uint8_t length = result.adv_data_len + result.scan_rsp_len; | ||||
|  | ||||
|     batch_buffer.emplace_back(); | ||||
|     auto &adv = batch_buffer.back(); | ||||
|     // Check if we need to expand the vector | ||||
|     if (this->advertisement_count_ >= advertisements.size()) { | ||||
|       if (this->advertisement_pool_.empty()) { | ||||
|         // No room in pool, need to allocate | ||||
|         advertisements.emplace_back(); | ||||
|       } else { | ||||
|         // Pull from pool | ||||
|         advertisements.push_back(std::move(this->advertisement_pool_.back())); | ||||
|         this->advertisement_pool_.pop_back(); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // Fill in the data directly at current position | ||||
|     auto &adv = advertisements[this->advertisement_count_]; | ||||
|     adv.address = esp32_ble::ble_addr_to_uint64(result.bda); | ||||
|     adv.rssi = result.rssi; | ||||
|     adv.address_type = result.ble_addr_type; | ||||
|     adv.data.assign(&result.ble_adv[0], &result.ble_adv[length]); | ||||
|     adv.data_len = length; | ||||
|     std::memcpy(adv.data, result.ble_adv, length); | ||||
|  | ||||
|     this->advertisement_count_++; | ||||
|  | ||||
|     ESP_LOGV(TAG, "Queuing raw packet from %02X:%02X:%02X:%02X:%02X:%02X, length %d. RSSI: %d dB", result.bda[0], | ||||
|              result.bda[1], result.bda[2], result.bda[3], result.bda[4], result.bda[5], length, result.rssi); | ||||
|   } | ||||
|  | ||||
|   // Only send if we've accumulated a good batch size to maximize batching efficiency | ||||
|   // https://github.com/esphome/backlog/issues/21 | ||||
|   if (batch_buffer.size() >= FLUSH_BATCH_SIZE) { | ||||
|     this->flush_pending_advertisements(); | ||||
|     // Flush if we have reached FLUSH_BATCH_SIZE | ||||
|     if (this->advertisement_count_ >= FLUSH_BATCH_SIZE) { | ||||
|       this->flush_pending_advertisements(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return true; | ||||
| } | ||||
|  | ||||
| void BluetoothProxy::flush_pending_advertisements() { | ||||
|   auto &batch_buffer = get_batch_buffer(); | ||||
|   if (batch_buffer.empty() || !api::global_api_server->is_connected() || this->api_connection_ == nullptr) | ||||
|   if (this->advertisement_count_ == 0 || !api::global_api_server->is_connected() || this->api_connection_ == nullptr) | ||||
|     return; | ||||
|  | ||||
|   api::BluetoothLERawAdvertisementsResponse resp; | ||||
|   resp.advertisements.swap(batch_buffer); | ||||
|   this->api_connection_->send_message(resp, api::BluetoothLERawAdvertisementsResponse::MESSAGE_TYPE); | ||||
|   auto &advertisements = this->response_->advertisements; | ||||
|  | ||||
|   // Return any items beyond advertisement_count_ to the pool | ||||
|   if (advertisements.size() > this->advertisement_count_) { | ||||
|     // Move unused items back to pool | ||||
|     this->advertisement_pool_.insert(this->advertisement_pool_.end(), | ||||
|                                      std::make_move_iterator(advertisements.begin() + this->advertisement_count_), | ||||
|                                      std::make_move_iterator(advertisements.end())); | ||||
|  | ||||
|     // Resize to actual count | ||||
|     advertisements.resize(this->advertisement_count_); | ||||
|   } | ||||
|  | ||||
|   // Send the message | ||||
|   this->api_connection_->send_message(*this->response_, api::BluetoothLERawAdvertisementsResponse::MESSAGE_TYPE); | ||||
|  | ||||
|   // Reset count - existing items will be overwritten in next batch | ||||
|   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() { | ||||
|   ESP_LOGCONFIG(TAG, "Bluetooth Proxy:"); | ||||
|   ESP_LOGCONFIG(TAG, | ||||
| @@ -187,130 +159,12 @@ void BluetoothProxy::loop() { | ||||
|   } | ||||
|  | ||||
|   // 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(); | ||||
|  | ||||
|   // Flush accumulated advertisements every 100ms | ||||
|   if (now - last_flush_time >= 100) { | ||||
|   if (now - this->last_advertisement_flush_time_ >= 100) { | ||||
|     this->flush_pending_advertisements(); | ||||
|     last_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); | ||||
|     } | ||||
|     this->last_advertisement_flush_time_ = now; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -647,7 +501,6 @@ void BluetoothProxy::bluetooth_scanner_set_mode(bool active) { | ||||
|  | ||||
| BluetoothProxy *global_bluetooth_proxy = nullptr;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | ||||
|  | ||||
| }  // namespace bluetooth_proxy | ||||
| }  // namespace esphome | ||||
| }  // namespace esphome::bluetooth_proxy | ||||
|  | ||||
| #endif  // USE_ESP32 | ||||
|   | ||||
| @@ -18,10 +18,10 @@ | ||||
| #include <esp_bt.h> | ||||
| #include <esp_bt_device.h> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace bluetooth_proxy { | ||||
| namespace esphome::bluetooth_proxy { | ||||
|  | ||||
| static const esp_err_t ESP_GATT_NOT_CONNECTED = -1; | ||||
| static const int DONE_SENDING_SERVICES = -2; | ||||
|  | ||||
| using namespace esp32_ble_client; | ||||
|  | ||||
| @@ -131,9 +131,6 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com | ||||
|   } | ||||
|  | ||||
|  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); | ||||
|  | ||||
|   BluetoothConnection *get_connection_(uint64_t address, bool reserve); | ||||
| @@ -145,14 +142,21 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com | ||||
|   // Group 2: Container types (typically 12 bytes on 32-bit) | ||||
|   std::vector<BluetoothConnection *> connections_{}; | ||||
|  | ||||
|   // Group 3: 1-byte types grouped together | ||||
|   // BLE advertisement batching | ||||
|   std::vector<api::BluetoothLERawAdvertisement> advertisement_pool_; | ||||
|   std::unique_ptr<api::BluetoothLERawAdvertisementsResponse> response_; | ||||
|  | ||||
|   // Group 3: 4-byte types | ||||
|   uint32_t last_advertisement_flush_time_{0}; | ||||
|  | ||||
|   // Group 4: 1-byte types grouped together | ||||
|   bool active_; | ||||
|   // 1 byte used, 3 bytes padding | ||||
|   uint8_t advertisement_count_{0}; | ||||
|   // 2 bytes used, 2 bytes padding | ||||
| }; | ||||
|  | ||||
| extern BluetoothProxy *global_bluetooth_proxy;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | ||||
|  | ||||
| }  // namespace bluetooth_proxy | ||||
| }  // namespace esphome | ||||
| }  // namespace esphome::bluetooth_proxy | ||||
|  | ||||
| #endif  // USE_ESP32 | ||||
|   | ||||
| @@ -88,7 +88,6 @@ const char *oversampling_to_str(BME280Oversampling oversampling) {  // NOLINT | ||||
| } | ||||
|  | ||||
| void BME280Component::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
|   uint8_t chip_id = 0; | ||||
|  | ||||
|   // Mark as not failed before initializing. Some devices will turn off sensors to save on batteries | ||||
|   | ||||
| @@ -71,7 +71,6 @@ static const char *iir_filter_to_str(BME680IIRFilter filter) { | ||||
| } | ||||
|  | ||||
| void BME680Component::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
|   uint8_t chip_id; | ||||
|   if (!this->read_byte(BME680_REGISTER_CHIPID, &chip_id) || chip_id != 0x61) { | ||||
|     this->mark_failed(); | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import esp32, i2c | ||||
| 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"] | ||||
| DEPENDENCIES = ["i2c"] | ||||
| @@ -56,7 +56,15 @@ CONFIG_SCHEMA = cv.All( | ||||
|             ): cv.positive_time_period_minutes, | ||||
|         } | ||||
|     ).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.only_on_esp8266, | ||||
|         cv.All( | ||||
|   | ||||
| @@ -15,8 +15,6 @@ std::vector<BME680BSECComponent *> | ||||
| uint8_t BME680BSECComponent::work_buffer_[BSEC_MAX_WORKBUFFER_SIZE] = {0}; | ||||
|  | ||||
| void BME680BSECComponent::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->device_id_.c_str()); | ||||
|  | ||||
|   uint8_t new_idx = BME680BSECComponent::instances.size(); | ||||
|   BME680BSECComponent::instances.push_back(this); | ||||
|  | ||||
|   | ||||
| @@ -21,8 +21,6 @@ static const char *const TAG = "bme68x_bsec2.sensor"; | ||||
| static const std::string IAQ_ACCURACY_STATES[4] = {"Stabilizing", "Uncertain", "Calibrating", "Calibrated"}; | ||||
|  | ||||
| void BME68xBSEC2Component::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
|  | ||||
|   this->bsec_status_ = bsec_init_m(&this->bsec_instance_); | ||||
|   if (this->bsec_status_ != BSEC_OK) { | ||||
|     this->mark_failed(); | ||||
|   | ||||
| @@ -119,7 +119,6 @@ const float GRAVITY_EARTH = 9.80665f; | ||||
| void BMI160Component::internal_setup_(int stage) { | ||||
|   switch (stage) { | ||||
|     case 0: | ||||
|       ESP_LOGCONFIG(TAG, "Running setup"); | ||||
|       uint8_t chipid; | ||||
|       if (!this->read_byte(BMI160_REGISTER_CHIPID, &chipid) || (chipid != 0b11010001)) { | ||||
|         this->mark_failed(); | ||||
|   | ||||
| @@ -20,7 +20,6 @@ void BMP085Component::update() { | ||||
|   this->set_timeout("temperature", 5, [this]() { this->read_temperature_(); }); | ||||
| } | ||||
| void BMP085Component::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
|   uint8_t data[22]; | ||||
|   if (!this->read_bytes(BMP085_REGISTER_AC1_H, data, 22)) { | ||||
|     this->mark_failed(); | ||||
|   | ||||
| @@ -57,7 +57,6 @@ static const char *iir_filter_to_str(BMP280IIRFilter filter) { | ||||
| } | ||||
|  | ||||
| void BMP280Component::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
|   uint8_t chip_id = 0; | ||||
|  | ||||
|   // Read the chip id twice, to work around a bug where the first read is 0. | ||||
|   | ||||
| @@ -70,7 +70,6 @@ static const LogString *iir_filter_to_str(IIRFilter filter) { | ||||
|  | ||||
| void BMP3XXComponent::setup() { | ||||
|   this->error_code_ = NONE; | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
|   // Call the Device base class "initialise" function | ||||
|   if (!reset()) { | ||||
|     ESP_LOGE(TAG, "Failed to reset"); | ||||
|   | ||||
| @@ -128,8 +128,6 @@ void BMP581Component::setup() { | ||||
|    */ | ||||
|  | ||||
|   this->error_code_ = NONE; | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
|  | ||||
|   //////////////////// | ||||
|   // 1) Soft reboot // | ||||
|   //////////////////// | ||||
|   | ||||
| @@ -15,7 +15,6 @@ static const uint8_t BP1658CJ_ADDR_START_5CH = 0x30; | ||||
| static const uint8_t BP1658CJ_DELAY = 2; | ||||
|  | ||||
| void BP1658CJ::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
|   this->data_pin_->setup(); | ||||
|   this->data_pin_->digital_write(false); | ||||
|   this->clock_pin_->setup(); | ||||
|   | ||||
| @@ -20,7 +20,6 @@ static const uint8_t BP5758D_ALL_DATA_CHANNEL_ENABLEMENT = 0b00011111; | ||||
| static const uint8_t BP5758D_DELAY = 2; | ||||
|  | ||||
| void BP5758D::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
|   this->data_pin_->setup(); | ||||
|   this->data_pin_->digital_write(false); | ||||
|   delayMicroseconds(BP5758D_DELAY); | ||||
|   | ||||
| @@ -22,9 +22,8 @@ def validate_id(config): | ||||
|     if CONF_CAN_ID in config: | ||||
|         can_id = config[CONF_CAN_ID] | ||||
|         id_ext = config[CONF_USE_EXTENDED_ID] | ||||
|         if not id_ext: | ||||
|             if can_id > 0x7FF: | ||||
|                 raise cv.Invalid("Standard IDs must be 11 Bit (0x000-0x7ff / 0-2047)") | ||||
|         if not id_ext and can_id > 0x7FF: | ||||
|             raise cv.Invalid("Standard IDs must be 11 Bit (0x000-0x7ff / 0-2047)") | ||||
|     return config | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -7,7 +7,6 @@ namespace canbus { | ||||
| static const char *const TAG = "canbus"; | ||||
|  | ||||
| void Canbus::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
|   if (!this->setup_internal()) { | ||||
|     ESP_LOGE(TAG, "setup error!"); | ||||
|     this->mark_failed(); | ||||
|   | ||||
| @@ -8,8 +8,6 @@ namespace cap1188 { | ||||
| static const char *const TAG = "cap1188"; | ||||
|  | ||||
| void CAP1188Component::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
|  | ||||
|   // Reset device using the reset pin | ||||
|   if (this->reset_pin_ != nullptr) { | ||||
|     this->reset_pin_->setup(); | ||||
|   | ||||
| @@ -10,8 +10,6 @@ static const char *const TAG = "cd74hc4067"; | ||||
| float CD74HC4067Component::get_setup_priority() const { return setup_priority::DATA; } | ||||
|  | ||||
| void CD74HC4067Component::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
|  | ||||
|   this->pin_s0_->setup(); | ||||
|   this->pin_s1_->setup(); | ||||
|   this->pin_s2_->setup(); | ||||
|   | ||||
| @@ -14,7 +14,6 @@ static const uint8_t CH422G_REG_OUT_UPPER = 0x23;    // write reg for output bit | ||||
| static const char *const TAG = "ch422g"; | ||||
|  | ||||
| void CH422GComponent::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
|   // set outputs before mode | ||||
|   this->write_outputs_(); | ||||
|   // Set mode and check for errors | ||||
|   | ||||
| @@ -4,7 +4,6 @@ namespace esphome { | ||||
| namespace chsc6x { | ||||
|  | ||||
| void CHSC6XTouchscreen::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
|   if (this->interrupt_pin_ != nullptr) { | ||||
|     this->interrupt_pin_->setup(); | ||||
|     this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE); | ||||
| @@ -15,8 +14,6 @@ void CHSC6XTouchscreen::setup() { | ||||
|   if (this->y_raw_max_ == this->y_raw_min_) { | ||||
|     this->y_raw_max_ = this->display_->get_native_height(); | ||||
|   } | ||||
|  | ||||
|   ESP_LOGCONFIG(TAG, "CHSC6X Touchscreen setup complete"); | ||||
| } | ||||
|  | ||||
| void CHSC6XTouchscreen::update_touches() { | ||||
|   | ||||
| @@ -20,7 +20,6 @@ uint8_t cm1106_checksum(const uint8_t *response, size_t len) { | ||||
| } | ||||
|  | ||||
| void CM1106Component::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
|   uint8_t response[8] = {0}; | ||||
|   if (!this->cm1106_write_command_(C_M1106_CMD_GET_CO2, sizeof(C_M1106_CMD_GET_CO2), response, sizeof(response))) { | ||||
|     ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); | ||||
|   | ||||
| @@ -3,7 +3,12 @@ | ||||
| CODEOWNERS = ["@esphome/core"] | ||||
|  | ||||
| CONF_BYTE_ORDER = "byte_order" | ||||
| BYTE_ORDER_LITTLE = "little_endian" | ||||
| BYTE_ORDER_BIG = "big_endian" | ||||
|  | ||||
| CONF_COLOR_DEPTH = "color_depth" | ||||
| CONF_DRAW_ROUNDING = "draw_rounding" | ||||
| CONF_ON_RECEIVE = "on_receive" | ||||
| CONF_ON_STATE_CHANGE = "on_state_change" | ||||
| CONF_REQUEST_HEADERS = "request_headers" | ||||
| CONF_USE_PSRAM = "use_psram" | ||||
|   | ||||
| @@ -52,8 +52,6 @@ bool CS5460AComponent::softreset_() { | ||||
| } | ||||
|  | ||||
| void CS5460AComponent::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
|  | ||||
|   float current_full_scale = (pga_gain_ == CS5460A_PGA_GAIN_10X) ? 0.25 : 0.10; | ||||
|   float voltage_full_scale = 0.25; | ||||
|   current_multiplier_ = current_full_scale / (fabsf(current_gain_) * 0x1000000); | ||||
|   | ||||
| @@ -42,7 +42,6 @@ static const uint8_t CSE7761_CMD_ENABLE_WRITE = 0xE5;  // Enable write operation | ||||
| enum CSE7761 { RMS_IAC, RMS_IBC, RMS_UC, POWER_PAC, POWER_PBC, POWER_SC, ENERGY_AC, ENERGY_BC }; | ||||
|  | ||||
| void CSE7761Component::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
|   this->write_(CSE7761_SPECIAL_COMMAND, CSE7761_CMD_RESET); | ||||
|   uint16_t syscon = this->read_(0x00, 2);  // Default 0x0A04 | ||||
|   if ((0x0A04 == syscon) && this->chip_init_()) { | ||||
|   | ||||
| @@ -6,7 +6,6 @@ namespace cst226 { | ||||
| static const char *const TAG = "cst226.touchscreen"; | ||||
|  | ||||
| void CST226Touchscreen::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
|   if (this->reset_pin_ != nullptr) { | ||||
|     this->reset_pin_->setup(); | ||||
|     this->reset_pin_->digital_write(true); | ||||
| @@ -95,7 +94,6 @@ void CST226Touchscreen::continue_setup_() { | ||||
|     } | ||||
|   } | ||||
|   this->setup_complete_ = true; | ||||
|   ESP_LOGCONFIG(TAG, "CST226 Touchscreen setup complete"); | ||||
| } | ||||
| void CST226Touchscreen::update_button_state_(bool state) { | ||||
|   if (this->button_touched_ == state) | ||||
|   | ||||
| @@ -35,11 +35,9 @@ void CST816Touchscreen::continue_setup_() { | ||||
|   if (this->y_raw_max_ == this->y_raw_min_) { | ||||
|     this->y_raw_max_ = this->display_->get_native_height(); | ||||
|   } | ||||
|   ESP_LOGCONFIG(TAG, "CST816 Touchscreen setup complete"); | ||||
| } | ||||
|  | ||||
| void CST816Touchscreen::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
|   if (this->reset_pin_ != nullptr) { | ||||
|     this->reset_pin_->setup(); | ||||
|     this->reset_pin_->digital_write(true); | ||||
|   | ||||
| @@ -20,8 +20,6 @@ static const uint8_t DAC7678_REG_INTERNAL_REF_0 = 0x80; | ||||
| static const uint8_t DAC7678_REG_INTERNAL_REF_1 = 0x90; | ||||
|  | ||||
| void DAC7678Output::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
|  | ||||
|   ESP_LOGV(TAG, "Resetting device"); | ||||
|  | ||||
|   // Reset device | ||||
|   | ||||
| @@ -70,7 +70,6 @@ bool DallasTemperatureSensor::read_scratch_pad_() { | ||||
| } | ||||
|  | ||||
| void DallasTemperatureSensor::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
|   if (!this->check_address_()) | ||||
|     return; | ||||
|   if (!this->read_scratch_pad_()) | ||||
|   | ||||
| @@ -12,7 +12,6 @@ static const uint32_t TEARDOWN_TIMEOUT_DEEP_SLEEP_MS = 5000; | ||||
| bool global_has_deep_sleep = false;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | ||||
|  | ||||
| void DeepSleepComponent::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
|   global_has_deep_sleep = true; | ||||
|  | ||||
|   const optional<uint32_t> run_duration = get_run_duration_(); | ||||
|   | ||||
| @@ -74,8 +74,7 @@ def range_segment_list(input): | ||||
|     if isinstance(input, list): | ||||
|         for list_item in input: | ||||
|             if isinstance(list_item, list): | ||||
|                 for item in list_item: | ||||
|                     flat_list.append(item) | ||||
|                 flat_list.extend(list_item) | ||||
|             else: | ||||
|                 flat_list.append(list_item) | ||||
|     else: | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user