mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00:00 
			
		
		
		
	Merge branch 'integration' into memory_api
This commit is contained in:
		| @@ -1 +1 @@ | |||||||
| 07f621354fe1350ba51953c80273cd44a04aa44f15cc30bd7b8fe2a641427b7a | 0c2acbc16bfb7d63571dbe7042f94f683be25e4ca8a0f158a960a94adac4b931 | ||||||
|   | |||||||
							
								
								
									
										3
									
								
								.github/workflows/auto-label-pr.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.github/workflows/auto-label-pr.yml
									
									
									
									
										vendored
									
									
								
							| @@ -305,8 +305,7 @@ jobs: | |||||||
|               const { data: codeownersFile } = await github.rest.repos.getContent({ |               const { data: codeownersFile } = await github.rest.repos.getContent({ | ||||||
|                 owner, |                 owner, | ||||||
|                 repo, |                 repo, | ||||||
|                 path: '.github/CODEOWNERS', |                 path: 'CODEOWNERS', | ||||||
|                 ref: context.payload.pull_request.head.sha |  | ||||||
|               }); |               }); | ||||||
|  |  | ||||||
|               const codeownersContent = Buffer.from(codeownersFile.content, 'base64').toString('utf8'); |               const codeownersContent = Buffer.from(codeownersFile.content, 'base64').toString('utf8'); | ||||||
|   | |||||||
							
								
								
									
										264
									
								
								.github/workflows/codeowner-review-request.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										264
									
								
								.github/workflows/codeowner-review-request.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,264 @@ | |||||||
|  | # 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`); | ||||||
|  |  | ||||||
|  |             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 `👋 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 `👋 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); | ||||||
|  |               }); | ||||||
|  |  | ||||||
|  |               // 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 or reviewed)'); | ||||||
|  |                 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'); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 // Add a comment to the PR mentioning what happened (include all matched codeowners) | ||||||
|  |                 const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, true); | ||||||
|  |  | ||||||
|  |                 await github.rest.issues.createComment({ | ||||||
|  |                   owner, | ||||||
|  |                   repo, | ||||||
|  |                   issue_number: pr_number, | ||||||
|  |                   body: commentBody | ||||||
|  |                 }); | ||||||
|  |               } catch (error) { | ||||||
|  |                 if (error.status === 422) { | ||||||
|  |                   console.log('Some reviewers may already be requested or unavailable:', error.message); | ||||||
|  |  | ||||||
|  |                   // Try to add a comment even if review request failed | ||||||
|  |                   const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, false); | ||||||
|  |  | ||||||
|  |                   try { | ||||||
|  |                     await github.rest.issues.createComment({ | ||||||
|  |                       owner, | ||||||
|  |                       repo, | ||||||
|  |                       issue_number: pr_number, | ||||||
|  |                       body: commentBody | ||||||
|  |                     }); | ||||||
|  |                   } catch (commentError) { | ||||||
|  |                     console.log('Failed to add comment:', commentError.message); | ||||||
|  |                   } | ||||||
|  |                 } else { | ||||||
|  |                   throw error; | ||||||
|  |                 } | ||||||
|  |               } | ||||||
|  |  | ||||||
|  |             } catch (error) { | ||||||
|  |               console.log('Failed to process codeowner review requests:', error.message); | ||||||
|  |               console.error(error); | ||||||
|  |             } | ||||||
							
								
								
									
										119
									
								
								.github/workflows/issue-codeowner-notify.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								.github/workflows/issue-codeowner-notify.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,119 @@ | |||||||
|  | # 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}`); | ||||||
|  |  | ||||||
|  |             // 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}` | ||||||
|  |               ); | ||||||
|  |  | ||||||
|  |               const allMentions = [...filteredUserOwners, ...teamOwners]; | ||||||
|  |  | ||||||
|  |               if (allMentions.length === 0) { | ||||||
|  |                 console.log('No codeowners to notify (issue author is the only codeowner)'); | ||||||
|  |                 return; | ||||||
|  |               } | ||||||
|  |  | ||||||
|  |               // Create comment body | ||||||
|  |               const mentionString = allMentions.join(', '); | ||||||
|  |               const commentBody = `👋 Hey ${mentionString}!\n\nThis issue has been labeled with \`component: ${componentName}\` and you've been identified as a codeowner of this component. Please take a look when you have a chance!\n\nThanks for maintaining this component! 🙏`; | ||||||
|  |  | ||||||
|  |               // Post comment | ||||||
|  |               await github.rest.issues.createComment({ | ||||||
|  |                 owner, | ||||||
|  |                 repo, | ||||||
|  |                 issue_number: issue_number, | ||||||
|  |                 body: commentBody | ||||||
|  |               }); | ||||||
|  |  | ||||||
|  |               console.log(`Successfully notified codeowners: ${mentionString}`); | ||||||
|  |  | ||||||
|  |             } catch (error) { | ||||||
|  |               console.log('Failed to process codeowner notifications:', error.message); | ||||||
|  |               console.error(error); | ||||||
|  |             } | ||||||
| @@ -230,14 +230,16 @@ message DeviceInfoResponse { | |||||||
|  |  | ||||||
|   uint32 webserver_port = 10 [(field_ifdef) = "USE_WEBSERVER"]; |   uint32 webserver_port = 10 [(field_ifdef) = "USE_WEBSERVER"]; | ||||||
|  |  | ||||||
|   uint32 legacy_bluetooth_proxy_version = 11 [(field_ifdef) = "USE_BLUETOOTH_PROXY"]; |   // Deprecated in API version 1.9 | ||||||
|  |   uint32 legacy_bluetooth_proxy_version = 11 [deprecated=true, (field_ifdef) = "USE_BLUETOOTH_PROXY"]; | ||||||
|   uint32 bluetooth_proxy_feature_flags = 15 [(field_ifdef) = "USE_BLUETOOTH_PROXY"]; |   uint32 bluetooth_proxy_feature_flags = 15 [(field_ifdef) = "USE_BLUETOOTH_PROXY"]; | ||||||
|  |  | ||||||
|   string manufacturer = 12; |   string manufacturer = 12; | ||||||
|  |  | ||||||
|   string friendly_name = 13; |   string friendly_name = 13; | ||||||
|  |  | ||||||
|   uint32 legacy_voice_assistant_version = 14 [(field_ifdef) = "USE_VOICE_ASSISTANT"]; |   // Deprecated in API version 1.10 | ||||||
|  |   uint32 legacy_voice_assistant_version = 14 [deprecated=true, (field_ifdef) = "USE_VOICE_ASSISTANT"]; | ||||||
|   uint32 voice_assistant_feature_flags = 17 [(field_ifdef) = "USE_VOICE_ASSISTANT"]; |   uint32 voice_assistant_feature_flags = 17 [(field_ifdef) = "USE_VOICE_ASSISTANT"]; | ||||||
|  |  | ||||||
|   string suggested_area = 16 [(field_ifdef) = "USE_AREAS"]; |   string suggested_area = 16 [(field_ifdef) = "USE_AREAS"]; | ||||||
| @@ -337,7 +339,9 @@ message ListEntitiesCoverResponse { | |||||||
|   uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"]; |   uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"]; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // Deprecated in API version 1.1 | ||||||
| enum LegacyCoverState { | enum LegacyCoverState { | ||||||
|  |   option deprecated = true; | ||||||
|   LEGACY_COVER_STATE_OPEN = 0; |   LEGACY_COVER_STATE_OPEN = 0; | ||||||
|   LEGACY_COVER_STATE_CLOSED = 1; |   LEGACY_COVER_STATE_CLOSED = 1; | ||||||
| } | } | ||||||
| @@ -356,7 +360,8 @@ message CoverStateResponse { | |||||||
|   fixed32 key = 1; |   fixed32 key = 1; | ||||||
|   // legacy: state has been removed in 1.13 |   // legacy: state has been removed in 1.13 | ||||||
|   // clients/servers must still send/accept it until the next protocol change |   // clients/servers must still send/accept it until the next protocol change | ||||||
|   LegacyCoverState legacy_state = 2; |   // Deprecated in API version 1.1 | ||||||
|  |   LegacyCoverState legacy_state = 2 [deprecated=true]; | ||||||
|  |  | ||||||
|   float position = 3; |   float position = 3; | ||||||
|   float tilt = 4; |   float tilt = 4; | ||||||
| @@ -364,7 +369,9 @@ message CoverStateResponse { | |||||||
|   uint32 device_id = 6 [(field_ifdef) = "USE_DEVICES"]; |   uint32 device_id = 6 [(field_ifdef) = "USE_DEVICES"]; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // Deprecated in API version 1.1 | ||||||
| enum LegacyCoverCommand { | enum LegacyCoverCommand { | ||||||
|  |   option deprecated = true; | ||||||
|   LEGACY_COVER_COMMAND_OPEN = 0; |   LEGACY_COVER_COMMAND_OPEN = 0; | ||||||
|   LEGACY_COVER_COMMAND_CLOSE = 1; |   LEGACY_COVER_COMMAND_CLOSE = 1; | ||||||
|   LEGACY_COVER_COMMAND_STOP = 2; |   LEGACY_COVER_COMMAND_STOP = 2; | ||||||
| @@ -380,8 +387,10 @@ message CoverCommandRequest { | |||||||
|  |  | ||||||
|   // legacy: command has been removed in 1.13 |   // legacy: command has been removed in 1.13 | ||||||
|   // clients/servers must still send/accept it until the next protocol change |   // clients/servers must still send/accept it until the next protocol change | ||||||
|   bool has_legacy_command = 2; |   // Deprecated in API version 1.1 | ||||||
|   LegacyCoverCommand legacy_command = 3; |   bool has_legacy_command = 2 [deprecated=true]; | ||||||
|  |   // Deprecated in API version 1.1 | ||||||
|  |   LegacyCoverCommand legacy_command = 3 [deprecated=true]; | ||||||
|  |  | ||||||
|   bool has_position = 4; |   bool has_position = 4; | ||||||
|   float position = 5; |   float position = 5; | ||||||
| @@ -413,7 +422,9 @@ message ListEntitiesFanResponse { | |||||||
|   repeated string supported_preset_modes = 12; |   repeated string supported_preset_modes = 12; | ||||||
|   uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"]; |   uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"]; | ||||||
| } | } | ||||||
|  | // Deprecated in API version 1.6 - only used in deprecated fields | ||||||
| enum FanSpeed { | enum FanSpeed { | ||||||
|  |   option deprecated = true; | ||||||
|   FAN_SPEED_LOW = 0; |   FAN_SPEED_LOW = 0; | ||||||
|   FAN_SPEED_MEDIUM = 1; |   FAN_SPEED_MEDIUM = 1; | ||||||
|   FAN_SPEED_HIGH = 2; |   FAN_SPEED_HIGH = 2; | ||||||
| @@ -432,6 +443,7 @@ message FanStateResponse { | |||||||
|   fixed32 key = 1; |   fixed32 key = 1; | ||||||
|   bool state = 2; |   bool state = 2; | ||||||
|   bool oscillating = 3; |   bool oscillating = 3; | ||||||
|  |   // Deprecated in API version 1.6 | ||||||
|   FanSpeed speed = 4 [deprecated=true]; |   FanSpeed speed = 4 [deprecated=true]; | ||||||
|   FanDirection direction = 5; |   FanDirection direction = 5; | ||||||
|   int32 speed_level = 6; |   int32 speed_level = 6; | ||||||
| @@ -448,7 +460,9 @@ message FanCommandRequest { | |||||||
|   fixed32 key = 1; |   fixed32 key = 1; | ||||||
|   bool has_state = 2; |   bool has_state = 2; | ||||||
|   bool state = 3; |   bool state = 3; | ||||||
|  |   // Deprecated in API version 1.6 | ||||||
|   bool has_speed = 4 [deprecated=true]; |   bool has_speed = 4 [deprecated=true]; | ||||||
|  |   // Deprecated in API version 1.6 | ||||||
|   FanSpeed speed = 5 [deprecated=true]; |   FanSpeed speed = 5 [deprecated=true]; | ||||||
|   bool has_oscillating = 6; |   bool has_oscillating = 6; | ||||||
|   bool oscillating = 7; |   bool oscillating = 7; | ||||||
| @@ -488,9 +502,13 @@ message ListEntitiesLightResponse { | |||||||
|  |  | ||||||
|   repeated ColorMode supported_color_modes = 12; |   repeated ColorMode supported_color_modes = 12; | ||||||
|   // next four supports_* are for legacy clients, newer clients should use color modes |   // next four supports_* are for legacy clients, newer clients should use color modes | ||||||
|  |   // Deprecated in API version 1.6 | ||||||
|   bool legacy_supports_brightness = 5 [deprecated=true]; |   bool legacy_supports_brightness = 5 [deprecated=true]; | ||||||
|  |   // Deprecated in API version 1.6 | ||||||
|   bool legacy_supports_rgb = 6 [deprecated=true]; |   bool legacy_supports_rgb = 6 [deprecated=true]; | ||||||
|  |   // Deprecated in API version 1.6 | ||||||
|   bool legacy_supports_white_value = 7 [deprecated=true]; |   bool legacy_supports_white_value = 7 [deprecated=true]; | ||||||
|  |   // Deprecated in API version 1.6 | ||||||
|   bool legacy_supports_color_temperature = 8 [deprecated=true]; |   bool legacy_supports_color_temperature = 8 [deprecated=true]; | ||||||
|   float min_mireds = 9; |   float min_mireds = 9; | ||||||
|   float max_mireds = 10; |   float max_mireds = 10; | ||||||
| @@ -567,7 +585,9 @@ enum SensorStateClass { | |||||||
|   STATE_CLASS_TOTAL = 3; |   STATE_CLASS_TOTAL = 3; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // Deprecated in API version 1.5 | ||||||
| enum SensorLastResetType { | enum SensorLastResetType { | ||||||
|  |   option deprecated = true; | ||||||
|   LAST_RESET_NONE = 0; |   LAST_RESET_NONE = 0; | ||||||
|   LAST_RESET_NEVER = 1; |   LAST_RESET_NEVER = 1; | ||||||
|   LAST_RESET_AUTO = 2; |   LAST_RESET_AUTO = 2; | ||||||
| @@ -591,7 +611,8 @@ message ListEntitiesSensorResponse { | |||||||
|   string device_class = 9; |   string device_class = 9; | ||||||
|   SensorStateClass state_class = 10; |   SensorStateClass state_class = 10; | ||||||
|   // Last reset type removed in 2021.9.0 |   // Last reset type removed in 2021.9.0 | ||||||
|   SensorLastResetType legacy_last_reset_type = 11; |   // Deprecated in API version 1.5 | ||||||
|  |   SensorLastResetType legacy_last_reset_type = 11 [deprecated=true]; | ||||||
|   bool disabled_by_default = 12; |   bool disabled_by_default = 12; | ||||||
|   EntityCategory entity_category = 13; |   EntityCategory entity_category = 13; | ||||||
|   uint32 device_id = 14 [(field_ifdef) = "USE_DEVICES"]; |   uint32 device_id = 14 [(field_ifdef) = "USE_DEVICES"]; | ||||||
| @@ -947,7 +968,8 @@ message ListEntitiesClimateResponse { | |||||||
|   float visual_target_temperature_step = 10; |   float visual_target_temperature_step = 10; | ||||||
|   // for older peer versions - in new system this |   // for older peer versions - in new system this | ||||||
|   // is if CLIMATE_PRESET_AWAY exists is supported_presets |   // is if CLIMATE_PRESET_AWAY exists is supported_presets | ||||||
|   bool legacy_supports_away = 11; |   // Deprecated in API version 1.5 | ||||||
|  |   bool legacy_supports_away = 11 [deprecated=true]; | ||||||
|   bool supports_action = 12; |   bool supports_action = 12; | ||||||
|   repeated ClimateFanMode supported_fan_modes = 13; |   repeated ClimateFanMode supported_fan_modes = 13; | ||||||
|   repeated ClimateSwingMode supported_swing_modes = 14; |   repeated ClimateSwingMode supported_swing_modes = 14; | ||||||
| @@ -978,7 +1000,8 @@ message ClimateStateResponse { | |||||||
|   float target_temperature_low = 5; |   float target_temperature_low = 5; | ||||||
|   float target_temperature_high = 6; |   float target_temperature_high = 6; | ||||||
|   // For older peers, equal to preset == CLIMATE_PRESET_AWAY |   // For older peers, equal to preset == CLIMATE_PRESET_AWAY | ||||||
|   bool unused_legacy_away = 7; |   // Deprecated in API version 1.5 | ||||||
|  |   bool unused_legacy_away = 7 [deprecated=true]; | ||||||
|   ClimateAction action = 8; |   ClimateAction action = 8; | ||||||
|   ClimateFanMode fan_mode = 9; |   ClimateFanMode fan_mode = 9; | ||||||
|   ClimateSwingMode swing_mode = 10; |   ClimateSwingMode swing_mode = 10; | ||||||
| @@ -1006,8 +1029,10 @@ message ClimateCommandRequest { | |||||||
|   bool has_target_temperature_high = 8; |   bool has_target_temperature_high = 8; | ||||||
|   float target_temperature_high = 9; |   float target_temperature_high = 9; | ||||||
|   // legacy, for older peers, newer ones should use CLIMATE_PRESET_AWAY in preset |   // legacy, for older peers, newer ones should use CLIMATE_PRESET_AWAY in preset | ||||||
|   bool unused_has_legacy_away = 10; |   // Deprecated in API version 1.5 | ||||||
|   bool unused_legacy_away = 11; |   bool unused_has_legacy_away = 10 [deprecated=true]; | ||||||
|  |   // Deprecated in API version 1.5 | ||||||
|  |   bool unused_legacy_away = 11 [deprecated=true]; | ||||||
|   bool has_fan_mode = 12; |   bool has_fan_mode = 12; | ||||||
|   ClimateFanMode fan_mode = 13; |   ClimateFanMode fan_mode = 13; | ||||||
|   bool has_swing_mode = 14; |   bool has_swing_mode = 14; | ||||||
| @@ -1354,12 +1379,17 @@ message SubscribeBluetoothLEAdvertisementsRequest { | |||||||
|   uint32 flags = 1; |   uint32 flags = 1; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // Deprecated - only used by deprecated BluetoothLEAdvertisementResponse | ||||||
| message BluetoothServiceData { | message BluetoothServiceData { | ||||||
|  |   option deprecated = true; | ||||||
|   string uuid = 1; |   string uuid = 1; | ||||||
|  |   // Deprecated in API version 1.7 | ||||||
|   repeated uint32 legacy_data = 2 [deprecated=true];  // Removed in api version 1.7 |   repeated uint32 legacy_data = 2 [deprecated=true];  // Removed in api version 1.7 | ||||||
|   bytes data = 3;  // Added in api version 1.7 |   bytes data = 3;  // Added in api version 1.7 | ||||||
| } | } | ||||||
|  | // Removed in ESPHome 2025.8.0 - use BluetoothLERawAdvertisementsResponse instead | ||||||
| message BluetoothLEAdvertisementResponse { | message BluetoothLEAdvertisementResponse { | ||||||
|  |   option deprecated = true; | ||||||
|   option (id) = 67; |   option (id) = 67; | ||||||
|   option (source) = SOURCE_SERVER; |   option (source) = SOURCE_SERVER; | ||||||
|   option (ifdef) = "USE_BLUETOOTH_PROXY"; |   option (ifdef) = "USE_BLUETOOTH_PROXY"; | ||||||
|   | |||||||
| @@ -362,8 +362,6 @@ uint16_t APIConnection::try_send_cover_state(EntityBase *entity, APIConnection * | |||||||
|   auto *cover = static_cast<cover::Cover *>(entity); |   auto *cover = static_cast<cover::Cover *>(entity); | ||||||
|   CoverStateResponse msg; |   CoverStateResponse msg; | ||||||
|   auto traits = cover->get_traits(); |   auto traits = cover->get_traits(); | ||||||
|   msg.legacy_state = |  | ||||||
|       (cover->position == cover::COVER_OPEN) ? enums::LEGACY_COVER_STATE_OPEN : enums::LEGACY_COVER_STATE_CLOSED; |  | ||||||
|   msg.position = cover->position; |   msg.position = cover->position; | ||||||
|   if (traits.get_supports_tilt()) |   if (traits.get_supports_tilt()) | ||||||
|     msg.tilt = cover->tilt; |     msg.tilt = cover->tilt; | ||||||
| @@ -385,19 +383,6 @@ uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *c | |||||||
| } | } | ||||||
| void APIConnection::cover_command(const CoverCommandRequest &msg) { | void APIConnection::cover_command(const CoverCommandRequest &msg) { | ||||||
|   ENTITY_COMMAND_MAKE_CALL(cover::Cover, cover, cover) |   ENTITY_COMMAND_MAKE_CALL(cover::Cover, cover, cover) | ||||||
|   if (msg.has_legacy_command) { |  | ||||||
|     switch (msg.legacy_command) { |  | ||||||
|       case enums::LEGACY_COVER_COMMAND_OPEN: |  | ||||||
|         call.set_command_open(); |  | ||||||
|         break; |  | ||||||
|       case enums::LEGACY_COVER_COMMAND_CLOSE: |  | ||||||
|         call.set_command_close(); |  | ||||||
|         break; |  | ||||||
|       case enums::LEGACY_COVER_COMMAND_STOP: |  | ||||||
|         call.set_command_stop(); |  | ||||||
|         break; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   if (msg.has_position) |   if (msg.has_position) | ||||||
|     call.set_position(msg.position); |     call.set_position(msg.position); | ||||||
|   if (msg.has_tilt) |   if (msg.has_tilt) | ||||||
| @@ -495,14 +480,8 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c | |||||||
|   auto traits = light->get_traits(); |   auto traits = light->get_traits(); | ||||||
|   for (auto mode : traits.get_supported_color_modes()) |   for (auto mode : traits.get_supported_color_modes()) | ||||||
|     msg.supported_color_modes.push_back(static_cast<enums::ColorMode>(mode)); |     msg.supported_color_modes.push_back(static_cast<enums::ColorMode>(mode)); | ||||||
|   msg.legacy_supports_brightness = traits.supports_color_capability(light::ColorCapability::BRIGHTNESS); |   if (traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) || | ||||||
|   msg.legacy_supports_rgb = traits.supports_color_capability(light::ColorCapability::RGB); |       traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE)) { | ||||||
|   msg.legacy_supports_white_value = |  | ||||||
|       msg.legacy_supports_rgb && (traits.supports_color_capability(light::ColorCapability::WHITE) || |  | ||||||
|                                   traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE)); |  | ||||||
|   msg.legacy_supports_color_temperature = traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) || |  | ||||||
|                                           traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE); |  | ||||||
|   if (msg.legacy_supports_color_temperature) { |  | ||||||
|     msg.min_mireds = traits.get_min_mireds(); |     msg.min_mireds = traits.get_min_mireds(); | ||||||
|     msg.max_mireds = traits.get_max_mireds(); |     msg.max_mireds = traits.get_max_mireds(); | ||||||
|   } |   } | ||||||
| @@ -692,7 +671,6 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection | |||||||
|   msg.visual_current_temperature_step = traits.get_visual_current_temperature_step(); |   msg.visual_current_temperature_step = traits.get_visual_current_temperature_step(); | ||||||
|   msg.visual_min_humidity = traits.get_visual_min_humidity(); |   msg.visual_min_humidity = traits.get_visual_min_humidity(); | ||||||
|   msg.visual_max_humidity = traits.get_visual_max_humidity(); |   msg.visual_max_humidity = traits.get_visual_max_humidity(); | ||||||
|   msg.legacy_supports_away = traits.supports_preset(climate::CLIMATE_PRESET_AWAY); |  | ||||||
|   msg.supports_action = traits.get_supports_action(); |   msg.supports_action = traits.get_supports_action(); | ||||||
|   for (auto fan_mode : traits.get_supported_fan_modes()) |   for (auto fan_mode : traits.get_supported_fan_modes()) | ||||||
|     msg.supported_fan_modes.push_back(static_cast<enums::ClimateFanMode>(fan_mode)); |     msg.supported_fan_modes.push_back(static_cast<enums::ClimateFanMode>(fan_mode)); | ||||||
| @@ -1113,21 +1091,6 @@ void APIConnection::subscribe_bluetooth_le_advertisements(const SubscribeBluetoo | |||||||
| void APIConnection::unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) { | void APIConnection::unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) { | ||||||
|   bluetooth_proxy::global_bluetooth_proxy->unsubscribe_api_connection(this); |   bluetooth_proxy::global_bluetooth_proxy->unsubscribe_api_connection(this); | ||||||
| } | } | ||||||
| bool APIConnection::send_bluetooth_le_advertisement(const BluetoothLEAdvertisementResponse &msg) { |  | ||||||
|   if (this->client_api_version_major_ < 1 || this->client_api_version_minor_ < 7) { |  | ||||||
|     BluetoothLEAdvertisementResponse resp = msg; |  | ||||||
|     for (auto &service : resp.service_data) { |  | ||||||
|       service.legacy_data.assign(service.data.begin(), service.data.end()); |  | ||||||
|       service.data.clear(); |  | ||||||
|     } |  | ||||||
|     for (auto &manufacturer_data : resp.manufacturer_data) { |  | ||||||
|       manufacturer_data.legacy_data.assign(manufacturer_data.data.begin(), manufacturer_data.data.end()); |  | ||||||
|       manufacturer_data.data.clear(); |  | ||||||
|     } |  | ||||||
|     return this->send_message(resp, BluetoothLEAdvertisementResponse::MESSAGE_TYPE); |  | ||||||
|   } |  | ||||||
|   return this->send_message(msg, BluetoothLEAdvertisementResponse::MESSAGE_TYPE); |  | ||||||
| } |  | ||||||
| void APIConnection::bluetooth_device_request(const BluetoothDeviceRequest &msg) { | void APIConnection::bluetooth_device_request(const BluetoothDeviceRequest &msg) { | ||||||
|   bluetooth_proxy::global_bluetooth_proxy->bluetooth_device_request(msg); |   bluetooth_proxy::global_bluetooth_proxy->bluetooth_device_request(msg); | ||||||
| } | } | ||||||
| @@ -1499,12 +1462,10 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { | |||||||
|   resp.webserver_port = USE_WEBSERVER_PORT; |   resp.webserver_port = USE_WEBSERVER_PORT; | ||||||
| #endif | #endif | ||||||
| #ifdef USE_BLUETOOTH_PROXY | #ifdef USE_BLUETOOTH_PROXY | ||||||
|   resp.legacy_bluetooth_proxy_version = bluetooth_proxy::global_bluetooth_proxy->get_legacy_version(); |  | ||||||
|   resp.bluetooth_proxy_feature_flags = bluetooth_proxy::global_bluetooth_proxy->get_feature_flags(); |   resp.bluetooth_proxy_feature_flags = bluetooth_proxy::global_bluetooth_proxy->get_feature_flags(); | ||||||
|   resp.bluetooth_mac_address = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_mac_address_pretty(); |   resp.bluetooth_mac_address = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_mac_address_pretty(); | ||||||
| #endif | #endif | ||||||
| #ifdef USE_VOICE_ASSISTANT | #ifdef USE_VOICE_ASSISTANT | ||||||
|   resp.legacy_voice_assistant_version = voice_assistant::global_voice_assistant->get_legacy_version(); |  | ||||||
|   resp.voice_assistant_feature_flags = voice_assistant::global_voice_assistant->get_feature_flags(); |   resp.voice_assistant_feature_flags = voice_assistant::global_voice_assistant->get_feature_flags(); | ||||||
| #endif | #endif | ||||||
| #ifdef USE_API_NOISE | #ifdef USE_API_NOISE | ||||||
|   | |||||||
| @@ -116,7 +116,6 @@ class APIConnection : public APIServerConnection { | |||||||
| #ifdef USE_BLUETOOTH_PROXY | #ifdef USE_BLUETOOTH_PROXY | ||||||
|   void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override; |   void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override; | ||||||
|   void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override; |   void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override; | ||||||
|   bool send_bluetooth_le_advertisement(const BluetoothLEAdvertisementResponse &msg); |  | ||||||
|  |  | ||||||
|   void bluetooth_device_request(const BluetoothDeviceRequest &msg) override; |   void bluetooth_device_request(const BluetoothDeviceRequest &msg) override; | ||||||
|   void bluetooth_gatt_read(const BluetoothGATTReadRequest &msg) override; |   void bluetooth_gatt_read(const BluetoothGATTReadRequest &msg) override; | ||||||
|   | |||||||
| @@ -94,17 +94,11 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { | |||||||
| #ifdef USE_WEBSERVER | #ifdef USE_WEBSERVER | ||||||
|   buffer.encode_uint32(10, this->webserver_port); |   buffer.encode_uint32(10, this->webserver_port); | ||||||
| #endif | #endif | ||||||
| #ifdef USE_BLUETOOTH_PROXY |  | ||||||
|   buffer.encode_uint32(11, this->legacy_bluetooth_proxy_version); |  | ||||||
| #endif |  | ||||||
| #ifdef USE_BLUETOOTH_PROXY | #ifdef USE_BLUETOOTH_PROXY | ||||||
|   buffer.encode_uint32(15, this->bluetooth_proxy_feature_flags); |   buffer.encode_uint32(15, this->bluetooth_proxy_feature_flags); | ||||||
| #endif | #endif | ||||||
|   buffer.encode_string(12, this->manufacturer); |   buffer.encode_string(12, this->manufacturer); | ||||||
|   buffer.encode_string(13, this->friendly_name); |   buffer.encode_string(13, this->friendly_name); | ||||||
| #ifdef USE_VOICE_ASSISTANT |  | ||||||
|   buffer.encode_uint32(14, this->legacy_voice_assistant_version); |  | ||||||
| #endif |  | ||||||
| #ifdef USE_VOICE_ASSISTANT | #ifdef USE_VOICE_ASSISTANT | ||||||
|   buffer.encode_uint32(17, this->voice_assistant_feature_flags); |   buffer.encode_uint32(17, this->voice_assistant_feature_flags); | ||||||
| #endif | #endif | ||||||
| @@ -150,17 +144,11 @@ void DeviceInfoResponse::calculate_size(uint32_t &total_size) const { | |||||||
| #ifdef USE_WEBSERVER | #ifdef USE_WEBSERVER | ||||||
|   ProtoSize::add_uint32_field(total_size, 1, this->webserver_port); |   ProtoSize::add_uint32_field(total_size, 1, this->webserver_port); | ||||||
| #endif | #endif | ||||||
| #ifdef USE_BLUETOOTH_PROXY |  | ||||||
|   ProtoSize::add_uint32_field(total_size, 1, this->legacy_bluetooth_proxy_version); |  | ||||||
| #endif |  | ||||||
| #ifdef USE_BLUETOOTH_PROXY | #ifdef USE_BLUETOOTH_PROXY | ||||||
|   ProtoSize::add_uint32_field(total_size, 1, this->bluetooth_proxy_feature_flags); |   ProtoSize::add_uint32_field(total_size, 1, this->bluetooth_proxy_feature_flags); | ||||||
| #endif | #endif | ||||||
|   ProtoSize::add_string_field(total_size, 1, this->manufacturer); |   ProtoSize::add_string_field(total_size, 1, this->manufacturer); | ||||||
|   ProtoSize::add_string_field(total_size, 1, this->friendly_name); |   ProtoSize::add_string_field(total_size, 1, this->friendly_name); | ||||||
| #ifdef USE_VOICE_ASSISTANT |  | ||||||
|   ProtoSize::add_uint32_field(total_size, 1, this->legacy_voice_assistant_version); |  | ||||||
| #endif |  | ||||||
| #ifdef USE_VOICE_ASSISTANT | #ifdef USE_VOICE_ASSISTANT | ||||||
|   ProtoSize::add_uint32_field(total_size, 2, this->voice_assistant_feature_flags); |   ProtoSize::add_uint32_field(total_size, 2, this->voice_assistant_feature_flags); | ||||||
| #endif | #endif | ||||||
| @@ -270,7 +258,6 @@ void ListEntitiesCoverResponse::calculate_size(uint32_t &total_size) const { | |||||||
| } | } | ||||||
| void CoverStateResponse::encode(ProtoWriteBuffer buffer) const { | void CoverStateResponse::encode(ProtoWriteBuffer buffer) const { | ||||||
|   buffer.encode_fixed32(1, this->key); |   buffer.encode_fixed32(1, this->key); | ||||||
|   buffer.encode_uint32(2, static_cast<uint32_t>(this->legacy_state)); |  | ||||||
|   buffer.encode_float(3, this->position); |   buffer.encode_float(3, this->position); | ||||||
|   buffer.encode_float(4, this->tilt); |   buffer.encode_float(4, this->tilt); | ||||||
|   buffer.encode_uint32(5, static_cast<uint32_t>(this->current_operation)); |   buffer.encode_uint32(5, static_cast<uint32_t>(this->current_operation)); | ||||||
| @@ -280,7 +267,6 @@ void CoverStateResponse::encode(ProtoWriteBuffer buffer) const { | |||||||
| } | } | ||||||
| void CoverStateResponse::calculate_size(uint32_t &total_size) const { | void CoverStateResponse::calculate_size(uint32_t &total_size) const { | ||||||
|   ProtoSize::add_fixed32_field(total_size, 1, this->key); |   ProtoSize::add_fixed32_field(total_size, 1, this->key); | ||||||
|   ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->legacy_state)); |  | ||||||
|   ProtoSize::add_float_field(total_size, 1, this->position); |   ProtoSize::add_float_field(total_size, 1, this->position); | ||||||
|   ProtoSize::add_float_field(total_size, 1, this->tilt); |   ProtoSize::add_float_field(total_size, 1, this->tilt); | ||||||
|   ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->current_operation)); |   ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->current_operation)); | ||||||
| @@ -290,12 +276,6 @@ void CoverStateResponse::calculate_size(uint32_t &total_size) const { | |||||||
| } | } | ||||||
| bool CoverCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { | bool CoverCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { | ||||||
|   switch (field_id) { |   switch (field_id) { | ||||||
|     case 2: |  | ||||||
|       this->has_legacy_command = value.as_bool(); |  | ||||||
|       break; |  | ||||||
|     case 3: |  | ||||||
|       this->legacy_command = static_cast<enums::LegacyCoverCommand>(value.as_uint32()); |  | ||||||
|       break; |  | ||||||
|     case 4: |     case 4: | ||||||
|       this->has_position = value.as_bool(); |       this->has_position = value.as_bool(); | ||||||
|       break; |       break; | ||||||
| @@ -379,7 +359,6 @@ void FanStateResponse::encode(ProtoWriteBuffer buffer) const { | |||||||
|   buffer.encode_fixed32(1, this->key); |   buffer.encode_fixed32(1, this->key); | ||||||
|   buffer.encode_bool(2, this->state); |   buffer.encode_bool(2, this->state); | ||||||
|   buffer.encode_bool(3, this->oscillating); |   buffer.encode_bool(3, this->oscillating); | ||||||
|   buffer.encode_uint32(4, static_cast<uint32_t>(this->speed)); |  | ||||||
|   buffer.encode_uint32(5, static_cast<uint32_t>(this->direction)); |   buffer.encode_uint32(5, static_cast<uint32_t>(this->direction)); | ||||||
|   buffer.encode_int32(6, this->speed_level); |   buffer.encode_int32(6, this->speed_level); | ||||||
|   buffer.encode_string(7, this->preset_mode); |   buffer.encode_string(7, this->preset_mode); | ||||||
| @@ -391,7 +370,6 @@ void FanStateResponse::calculate_size(uint32_t &total_size) const { | |||||||
|   ProtoSize::add_fixed32_field(total_size, 1, this->key); |   ProtoSize::add_fixed32_field(total_size, 1, this->key); | ||||||
|   ProtoSize::add_bool_field(total_size, 1, this->state); |   ProtoSize::add_bool_field(total_size, 1, this->state); | ||||||
|   ProtoSize::add_bool_field(total_size, 1, this->oscillating); |   ProtoSize::add_bool_field(total_size, 1, this->oscillating); | ||||||
|   ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->speed)); |  | ||||||
|   ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->direction)); |   ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->direction)); | ||||||
|   ProtoSize::add_int32_field(total_size, 1, this->speed_level); |   ProtoSize::add_int32_field(total_size, 1, this->speed_level); | ||||||
|   ProtoSize::add_string_field(total_size, 1, this->preset_mode); |   ProtoSize::add_string_field(total_size, 1, this->preset_mode); | ||||||
| @@ -407,12 +385,6 @@ bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { | |||||||
|     case 3: |     case 3: | ||||||
|       this->state = value.as_bool(); |       this->state = value.as_bool(); | ||||||
|       break; |       break; | ||||||
|     case 4: |  | ||||||
|       this->has_speed = value.as_bool(); |  | ||||||
|       break; |  | ||||||
|     case 5: |  | ||||||
|       this->speed = static_cast<enums::FanSpeed>(value.as_uint32()); |  | ||||||
|       break; |  | ||||||
|     case 6: |     case 6: | ||||||
|       this->has_oscillating = value.as_bool(); |       this->has_oscillating = value.as_bool(); | ||||||
|       break; |       break; | ||||||
| @@ -473,10 +445,6 @@ void ListEntitiesLightResponse::encode(ProtoWriteBuffer buffer) const { | |||||||
|   for (auto &it : this->supported_color_modes) { |   for (auto &it : this->supported_color_modes) { | ||||||
|     buffer.encode_uint32(12, static_cast<uint32_t>(it), true); |     buffer.encode_uint32(12, static_cast<uint32_t>(it), true); | ||||||
|   } |   } | ||||||
|   buffer.encode_bool(5, this->legacy_supports_brightness); |  | ||||||
|   buffer.encode_bool(6, this->legacy_supports_rgb); |  | ||||||
|   buffer.encode_bool(7, this->legacy_supports_white_value); |  | ||||||
|   buffer.encode_bool(8, this->legacy_supports_color_temperature); |  | ||||||
|   buffer.encode_float(9, this->min_mireds); |   buffer.encode_float(9, this->min_mireds); | ||||||
|   buffer.encode_float(10, this->max_mireds); |   buffer.encode_float(10, this->max_mireds); | ||||||
|   for (auto &it : this->effects) { |   for (auto &it : this->effects) { | ||||||
| @@ -500,10 +468,6 @@ void ListEntitiesLightResponse::calculate_size(uint32_t &total_size) const { | |||||||
|       ProtoSize::add_enum_field_repeated(total_size, 1, static_cast<uint32_t>(it)); |       ProtoSize::add_enum_field_repeated(total_size, 1, static_cast<uint32_t>(it)); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   ProtoSize::add_bool_field(total_size, 1, this->legacy_supports_brightness); |  | ||||||
|   ProtoSize::add_bool_field(total_size, 1, this->legacy_supports_rgb); |  | ||||||
|   ProtoSize::add_bool_field(total_size, 1, this->legacy_supports_white_value); |  | ||||||
|   ProtoSize::add_bool_field(total_size, 1, this->legacy_supports_color_temperature); |  | ||||||
|   ProtoSize::add_float_field(total_size, 1, this->min_mireds); |   ProtoSize::add_float_field(total_size, 1, this->min_mireds); | ||||||
|   ProtoSize::add_float_field(total_size, 1, this->max_mireds); |   ProtoSize::add_float_field(total_size, 1, this->max_mireds); | ||||||
|   if (!this->effects.empty()) { |   if (!this->effects.empty()) { | ||||||
| @@ -677,7 +641,6 @@ void ListEntitiesSensorResponse::encode(ProtoWriteBuffer buffer) const { | |||||||
|   buffer.encode_bool(8, this->force_update); |   buffer.encode_bool(8, this->force_update); | ||||||
|   buffer.encode_string(9, this->device_class); |   buffer.encode_string(9, this->device_class); | ||||||
|   buffer.encode_uint32(10, static_cast<uint32_t>(this->state_class)); |   buffer.encode_uint32(10, static_cast<uint32_t>(this->state_class)); | ||||||
|   buffer.encode_uint32(11, static_cast<uint32_t>(this->legacy_last_reset_type)); |  | ||||||
|   buffer.encode_bool(12, this->disabled_by_default); |   buffer.encode_bool(12, this->disabled_by_default); | ||||||
|   buffer.encode_uint32(13, static_cast<uint32_t>(this->entity_category)); |   buffer.encode_uint32(13, static_cast<uint32_t>(this->entity_category)); | ||||||
| #ifdef USE_DEVICES | #ifdef USE_DEVICES | ||||||
| @@ -696,7 +659,6 @@ void ListEntitiesSensorResponse::calculate_size(uint32_t &total_size) const { | |||||||
|   ProtoSize::add_bool_field(total_size, 1, this->force_update); |   ProtoSize::add_bool_field(total_size, 1, this->force_update); | ||||||
|   ProtoSize::add_string_field(total_size, 1, this->device_class); |   ProtoSize::add_string_field(total_size, 1, this->device_class); | ||||||
|   ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->state_class)); |   ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->state_class)); | ||||||
|   ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->legacy_last_reset_type)); |  | ||||||
|   ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); |   ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); | ||||||
|   ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->entity_category)); |   ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->entity_category)); | ||||||
| #ifdef USE_DEVICES | #ifdef USE_DEVICES | ||||||
| @@ -1105,7 +1067,6 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { | |||||||
|   buffer.encode_float(8, this->visual_min_temperature); |   buffer.encode_float(8, this->visual_min_temperature); | ||||||
|   buffer.encode_float(9, this->visual_max_temperature); |   buffer.encode_float(9, this->visual_max_temperature); | ||||||
|   buffer.encode_float(10, this->visual_target_temperature_step); |   buffer.encode_float(10, this->visual_target_temperature_step); | ||||||
|   buffer.encode_bool(11, this->legacy_supports_away); |  | ||||||
|   buffer.encode_bool(12, this->supports_action); |   buffer.encode_bool(12, this->supports_action); | ||||||
|   for (auto &it : this->supported_fan_modes) { |   for (auto &it : this->supported_fan_modes) { | ||||||
|     buffer.encode_uint32(13, static_cast<uint32_t>(it), true); |     buffer.encode_uint32(13, static_cast<uint32_t>(it), true); | ||||||
| @@ -1150,7 +1111,6 @@ void ListEntitiesClimateResponse::calculate_size(uint32_t &total_size) const { | |||||||
|   ProtoSize::add_float_field(total_size, 1, this->visual_min_temperature); |   ProtoSize::add_float_field(total_size, 1, this->visual_min_temperature); | ||||||
|   ProtoSize::add_float_field(total_size, 1, this->visual_max_temperature); |   ProtoSize::add_float_field(total_size, 1, this->visual_max_temperature); | ||||||
|   ProtoSize::add_float_field(total_size, 1, this->visual_target_temperature_step); |   ProtoSize::add_float_field(total_size, 1, this->visual_target_temperature_step); | ||||||
|   ProtoSize::add_bool_field(total_size, 1, this->legacy_supports_away); |  | ||||||
|   ProtoSize::add_bool_field(total_size, 1, this->supports_action); |   ProtoSize::add_bool_field(total_size, 1, this->supports_action); | ||||||
|   if (!this->supported_fan_modes.empty()) { |   if (!this->supported_fan_modes.empty()) { | ||||||
|     for (const auto &it : this->supported_fan_modes) { |     for (const auto &it : this->supported_fan_modes) { | ||||||
| @@ -1198,7 +1158,6 @@ void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const { | |||||||
|   buffer.encode_float(4, this->target_temperature); |   buffer.encode_float(4, this->target_temperature); | ||||||
|   buffer.encode_float(5, this->target_temperature_low); |   buffer.encode_float(5, this->target_temperature_low); | ||||||
|   buffer.encode_float(6, this->target_temperature_high); |   buffer.encode_float(6, this->target_temperature_high); | ||||||
|   buffer.encode_bool(7, this->unused_legacy_away); |  | ||||||
|   buffer.encode_uint32(8, static_cast<uint32_t>(this->action)); |   buffer.encode_uint32(8, static_cast<uint32_t>(this->action)); | ||||||
|   buffer.encode_uint32(9, static_cast<uint32_t>(this->fan_mode)); |   buffer.encode_uint32(9, static_cast<uint32_t>(this->fan_mode)); | ||||||
|   buffer.encode_uint32(10, static_cast<uint32_t>(this->swing_mode)); |   buffer.encode_uint32(10, static_cast<uint32_t>(this->swing_mode)); | ||||||
| @@ -1218,7 +1177,6 @@ void ClimateStateResponse::calculate_size(uint32_t &total_size) const { | |||||||
|   ProtoSize::add_float_field(total_size, 1, this->target_temperature); |   ProtoSize::add_float_field(total_size, 1, this->target_temperature); | ||||||
|   ProtoSize::add_float_field(total_size, 1, this->target_temperature_low); |   ProtoSize::add_float_field(total_size, 1, this->target_temperature_low); | ||||||
|   ProtoSize::add_float_field(total_size, 1, this->target_temperature_high); |   ProtoSize::add_float_field(total_size, 1, this->target_temperature_high); | ||||||
|   ProtoSize::add_bool_field(total_size, 1, this->unused_legacy_away); |  | ||||||
|   ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->action)); |   ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->action)); | ||||||
|   ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->fan_mode)); |   ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->fan_mode)); | ||||||
|   ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->swing_mode)); |   ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->swing_mode)); | ||||||
| @@ -1248,12 +1206,6 @@ bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) | |||||||
|     case 8: |     case 8: | ||||||
|       this->has_target_temperature_high = value.as_bool(); |       this->has_target_temperature_high = value.as_bool(); | ||||||
|       break; |       break; | ||||||
|     case 10: |  | ||||||
|       this->unused_has_legacy_away = value.as_bool(); |  | ||||||
|       break; |  | ||||||
|     case 11: |  | ||||||
|       this->unused_legacy_away = value.as_bool(); |  | ||||||
|       break; |  | ||||||
|     case 12: |     case 12: | ||||||
|       this->has_fan_mode = value.as_bool(); |       this->has_fan_mode = value.as_bool(); | ||||||
|       break; |       break; | ||||||
| @@ -1869,50 +1821,6 @@ bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id, | |||||||
|   } |   } | ||||||
|   return true; |   return true; | ||||||
| } | } | ||||||
| void BluetoothServiceData::encode(ProtoWriteBuffer buffer) const { |  | ||||||
|   buffer.encode_string(1, this->uuid); |  | ||||||
|   for (auto &it : this->legacy_data) { |  | ||||||
|     buffer.encode_uint32(2, it, true); |  | ||||||
|   } |  | ||||||
|   buffer.encode_bytes(3, reinterpret_cast<const uint8_t *>(this->data.data()), this->data.size()); |  | ||||||
| } |  | ||||||
| void BluetoothServiceData::calculate_size(uint32_t &total_size) const { |  | ||||||
|   ProtoSize::add_string_field(total_size, 1, this->uuid); |  | ||||||
|   if (!this->legacy_data.empty()) { |  | ||||||
|     for (const auto &it : this->legacy_data) { |  | ||||||
|       ProtoSize::add_uint32_field_repeated(total_size, 1, it); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   ProtoSize::add_string_field(total_size, 1, this->data); |  | ||||||
| } |  | ||||||
| void BluetoothLEAdvertisementResponse::encode(ProtoWriteBuffer buffer) const { |  | ||||||
|   buffer.encode_uint64(1, this->address); |  | ||||||
|   buffer.encode_bytes(2, reinterpret_cast<const uint8_t *>(this->name.data()), this->name.size()); |  | ||||||
|   buffer.encode_sint32(3, this->rssi); |  | ||||||
|   for (auto &it : this->service_uuids) { |  | ||||||
|     buffer.encode_string(4, it, true); |  | ||||||
|   } |  | ||||||
|   for (auto &it : this->service_data) { |  | ||||||
|     buffer.encode_message(5, it, true); |  | ||||||
|   } |  | ||||||
|   for (auto &it : this->manufacturer_data) { |  | ||||||
|     buffer.encode_message(6, it, true); |  | ||||||
|   } |  | ||||||
|   buffer.encode_uint32(7, this->address_type); |  | ||||||
| } |  | ||||||
| void BluetoothLEAdvertisementResponse::calculate_size(uint32_t &total_size) const { |  | ||||||
|   ProtoSize::add_uint64_field(total_size, 1, this->address); |  | ||||||
|   ProtoSize::add_string_field(total_size, 1, this->name); |  | ||||||
|   ProtoSize::add_sint32_field(total_size, 1, this->rssi); |  | ||||||
|   if (!this->service_uuids.empty()) { |  | ||||||
|     for (const auto &it : this->service_uuids) { |  | ||||||
|       ProtoSize::add_string_field_repeated(total_size, 1, it); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   ProtoSize::add_repeated_message(total_size, 1, this->service_data); |  | ||||||
|   ProtoSize::add_repeated_message(total_size, 1, this->manufacturer_data); |  | ||||||
|   ProtoSize::add_uint32_field(total_size, 1, this->address_type); |  | ||||||
| } |  | ||||||
| void BluetoothLERawAdvertisement::encode(ProtoWriteBuffer buffer) const { | void BluetoothLERawAdvertisement::encode(ProtoWriteBuffer buffer) const { | ||||||
|   buffer.encode_uint64(1, this->address); |   buffer.encode_uint64(1, this->address); | ||||||
|   buffer.encode_sint32(2, this->rssi); |   buffer.encode_sint32(2, this->rssi); | ||||||
|   | |||||||
| @@ -17,27 +17,13 @@ enum EntityCategory : uint32_t { | |||||||
|   ENTITY_CATEGORY_DIAGNOSTIC = 2, |   ENTITY_CATEGORY_DIAGNOSTIC = 2, | ||||||
| }; | }; | ||||||
| #ifdef USE_COVER | #ifdef USE_COVER | ||||||
| enum LegacyCoverState : uint32_t { |  | ||||||
|   LEGACY_COVER_STATE_OPEN = 0, |  | ||||||
|   LEGACY_COVER_STATE_CLOSED = 1, |  | ||||||
| }; |  | ||||||
| enum CoverOperation : uint32_t { | enum CoverOperation : uint32_t { | ||||||
|   COVER_OPERATION_IDLE = 0, |   COVER_OPERATION_IDLE = 0, | ||||||
|   COVER_OPERATION_IS_OPENING = 1, |   COVER_OPERATION_IS_OPENING = 1, | ||||||
|   COVER_OPERATION_IS_CLOSING = 2, |   COVER_OPERATION_IS_CLOSING = 2, | ||||||
| }; | }; | ||||||
| enum LegacyCoverCommand : uint32_t { |  | ||||||
|   LEGACY_COVER_COMMAND_OPEN = 0, |  | ||||||
|   LEGACY_COVER_COMMAND_CLOSE = 1, |  | ||||||
|   LEGACY_COVER_COMMAND_STOP = 2, |  | ||||||
| }; |  | ||||||
| #endif | #endif | ||||||
| #ifdef USE_FAN | #ifdef USE_FAN | ||||||
| enum FanSpeed : uint32_t { |  | ||||||
|   FAN_SPEED_LOW = 0, |  | ||||||
|   FAN_SPEED_MEDIUM = 1, |  | ||||||
|   FAN_SPEED_HIGH = 2, |  | ||||||
| }; |  | ||||||
| enum FanDirection : uint32_t { | enum FanDirection : uint32_t { | ||||||
|   FAN_DIRECTION_FORWARD = 0, |   FAN_DIRECTION_FORWARD = 0, | ||||||
|   FAN_DIRECTION_REVERSE = 1, |   FAN_DIRECTION_REVERSE = 1, | ||||||
| @@ -65,11 +51,6 @@ enum SensorStateClass : uint32_t { | |||||||
|   STATE_CLASS_TOTAL_INCREASING = 2, |   STATE_CLASS_TOTAL_INCREASING = 2, | ||||||
|   STATE_CLASS_TOTAL = 3, |   STATE_CLASS_TOTAL = 3, | ||||||
| }; | }; | ||||||
| enum SensorLastResetType : uint32_t { |  | ||||||
|   LAST_RESET_NONE = 0, |  | ||||||
|   LAST_RESET_NEVER = 1, |  | ||||||
|   LAST_RESET_AUTO = 2, |  | ||||||
| }; |  | ||||||
| #endif | #endif | ||||||
| enum LogLevel : uint32_t { | enum LogLevel : uint32_t { | ||||||
|   LOG_LEVEL_NONE = 0, |   LOG_LEVEL_NONE = 0, | ||||||
| @@ -477,7 +458,7 @@ class DeviceInfo : public ProtoMessage { | |||||||
| class DeviceInfoResponse : public ProtoMessage { | class DeviceInfoResponse : public ProtoMessage { | ||||||
|  public: |  public: | ||||||
|   static constexpr uint8_t MESSAGE_TYPE = 10; |   static constexpr uint8_t MESSAGE_TYPE = 10; | ||||||
|   static constexpr uint8_t ESTIMATED_SIZE = 219; |   static constexpr uint8_t ESTIMATED_SIZE = 211; | ||||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
|   const char *message_name() const override { return "device_info_response"; } |   const char *message_name() const override { return "device_info_response"; } | ||||||
| #endif | #endif | ||||||
| @@ -499,17 +480,11 @@ class DeviceInfoResponse : public ProtoMessage { | |||||||
| #ifdef USE_WEBSERVER | #ifdef USE_WEBSERVER | ||||||
|   uint32_t webserver_port{0}; |   uint32_t webserver_port{0}; | ||||||
| #endif | #endif | ||||||
| #ifdef USE_BLUETOOTH_PROXY |  | ||||||
|   uint32_t legacy_bluetooth_proxy_version{0}; |  | ||||||
| #endif |  | ||||||
| #ifdef USE_BLUETOOTH_PROXY | #ifdef USE_BLUETOOTH_PROXY | ||||||
|   uint32_t bluetooth_proxy_feature_flags{0}; |   uint32_t bluetooth_proxy_feature_flags{0}; | ||||||
| #endif | #endif | ||||||
|   std::string manufacturer{}; |   std::string manufacturer{}; | ||||||
|   std::string friendly_name{}; |   std::string friendly_name{}; | ||||||
| #ifdef USE_VOICE_ASSISTANT |  | ||||||
|   uint32_t legacy_voice_assistant_version{0}; |  | ||||||
| #endif |  | ||||||
| #ifdef USE_VOICE_ASSISTANT | #ifdef USE_VOICE_ASSISTANT | ||||||
|   uint32_t voice_assistant_feature_flags{0}; |   uint32_t voice_assistant_feature_flags{0}; | ||||||
| #endif | #endif | ||||||
| @@ -638,11 +613,10 @@ class ListEntitiesCoverResponse : public InfoResponseProtoMessage { | |||||||
| class CoverStateResponse : public StateResponseProtoMessage { | class CoverStateResponse : public StateResponseProtoMessage { | ||||||
|  public: |  public: | ||||||
|   static constexpr uint8_t MESSAGE_TYPE = 22; |   static constexpr uint8_t MESSAGE_TYPE = 22; | ||||||
|   static constexpr uint8_t ESTIMATED_SIZE = 23; |   static constexpr uint8_t ESTIMATED_SIZE = 21; | ||||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
|   const char *message_name() const override { return "cover_state_response"; } |   const char *message_name() const override { return "cover_state_response"; } | ||||||
| #endif | #endif | ||||||
|   enums::LegacyCoverState legacy_state{}; |  | ||||||
|   float position{0.0f}; |   float position{0.0f}; | ||||||
|   float tilt{0.0f}; |   float tilt{0.0f}; | ||||||
|   enums::CoverOperation current_operation{}; |   enums::CoverOperation current_operation{}; | ||||||
| @@ -657,12 +631,10 @@ class CoverStateResponse : public StateResponseProtoMessage { | |||||||
| class CoverCommandRequest : public CommandProtoMessage { | class CoverCommandRequest : public CommandProtoMessage { | ||||||
|  public: |  public: | ||||||
|   static constexpr uint8_t MESSAGE_TYPE = 30; |   static constexpr uint8_t MESSAGE_TYPE = 30; | ||||||
|   static constexpr uint8_t ESTIMATED_SIZE = 29; |   static constexpr uint8_t ESTIMATED_SIZE = 25; | ||||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
|   const char *message_name() const override { return "cover_command_request"; } |   const char *message_name() const override { return "cover_command_request"; } | ||||||
| #endif | #endif | ||||||
|   bool has_legacy_command{false}; |  | ||||||
|   enums::LegacyCoverCommand legacy_command{}; |  | ||||||
|   bool has_position{false}; |   bool has_position{false}; | ||||||
|   float position{0.0f}; |   float position{0.0f}; | ||||||
|   bool has_tilt{false}; |   bool has_tilt{false}; | ||||||
| @@ -701,13 +673,12 @@ class ListEntitiesFanResponse : public InfoResponseProtoMessage { | |||||||
| class FanStateResponse : public StateResponseProtoMessage { | class FanStateResponse : public StateResponseProtoMessage { | ||||||
|  public: |  public: | ||||||
|   static constexpr uint8_t MESSAGE_TYPE = 23; |   static constexpr uint8_t MESSAGE_TYPE = 23; | ||||||
|   static constexpr uint8_t ESTIMATED_SIZE = 30; |   static constexpr uint8_t ESTIMATED_SIZE = 28; | ||||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
|   const char *message_name() const override { return "fan_state_response"; } |   const char *message_name() const override { return "fan_state_response"; } | ||||||
| #endif | #endif | ||||||
|   bool state{false}; |   bool state{false}; | ||||||
|   bool oscillating{false}; |   bool oscillating{false}; | ||||||
|   enums::FanSpeed speed{}; |  | ||||||
|   enums::FanDirection direction{}; |   enums::FanDirection direction{}; | ||||||
|   int32_t speed_level{0}; |   int32_t speed_level{0}; | ||||||
|   std::string preset_mode{}; |   std::string preset_mode{}; | ||||||
| @@ -722,14 +693,12 @@ class FanStateResponse : public StateResponseProtoMessage { | |||||||
| class FanCommandRequest : public CommandProtoMessage { | class FanCommandRequest : public CommandProtoMessage { | ||||||
|  public: |  public: | ||||||
|   static constexpr uint8_t MESSAGE_TYPE = 31; |   static constexpr uint8_t MESSAGE_TYPE = 31; | ||||||
|   static constexpr uint8_t ESTIMATED_SIZE = 42; |   static constexpr uint8_t ESTIMATED_SIZE = 38; | ||||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
|   const char *message_name() const override { return "fan_command_request"; } |   const char *message_name() const override { return "fan_command_request"; } | ||||||
| #endif | #endif | ||||||
|   bool has_state{false}; |   bool has_state{false}; | ||||||
|   bool state{false}; |   bool state{false}; | ||||||
|   bool has_speed{false}; |  | ||||||
|   enums::FanSpeed speed{}; |  | ||||||
|   bool has_oscillating{false}; |   bool has_oscillating{false}; | ||||||
|   bool oscillating{false}; |   bool oscillating{false}; | ||||||
|   bool has_direction{false}; |   bool has_direction{false}; | ||||||
| @@ -752,15 +721,11 @@ class FanCommandRequest : public CommandProtoMessage { | |||||||
| class ListEntitiesLightResponse : public InfoResponseProtoMessage { | class ListEntitiesLightResponse : public InfoResponseProtoMessage { | ||||||
|  public: |  public: | ||||||
|   static constexpr uint8_t MESSAGE_TYPE = 15; |   static constexpr uint8_t MESSAGE_TYPE = 15; | ||||||
|   static constexpr uint8_t ESTIMATED_SIZE = 81; |   static constexpr uint8_t ESTIMATED_SIZE = 73; | ||||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
|   const char *message_name() const override { return "list_entities_light_response"; } |   const char *message_name() const override { return "list_entities_light_response"; } | ||||||
| #endif | #endif | ||||||
|   std::vector<enums::ColorMode> supported_color_modes{}; |   std::vector<enums::ColorMode> supported_color_modes{}; | ||||||
|   bool legacy_supports_brightness{false}; |  | ||||||
|   bool legacy_supports_rgb{false}; |  | ||||||
|   bool legacy_supports_white_value{false}; |  | ||||||
|   bool legacy_supports_color_temperature{false}; |  | ||||||
|   float min_mireds{0.0f}; |   float min_mireds{0.0f}; | ||||||
|   float max_mireds{0.0f}; |   float max_mireds{0.0f}; | ||||||
|   std::vector<std::string> effects{}; |   std::vector<std::string> effects{}; | ||||||
| @@ -846,7 +811,7 @@ class LightCommandRequest : public CommandProtoMessage { | |||||||
| class ListEntitiesSensorResponse : public InfoResponseProtoMessage { | class ListEntitiesSensorResponse : public InfoResponseProtoMessage { | ||||||
|  public: |  public: | ||||||
|   static constexpr uint8_t MESSAGE_TYPE = 16; |   static constexpr uint8_t MESSAGE_TYPE = 16; | ||||||
|   static constexpr uint8_t ESTIMATED_SIZE = 68; |   static constexpr uint8_t ESTIMATED_SIZE = 66; | ||||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
|   const char *message_name() const override { return "list_entities_sensor_response"; } |   const char *message_name() const override { return "list_entities_sensor_response"; } | ||||||
| #endif | #endif | ||||||
| @@ -855,7 +820,6 @@ class ListEntitiesSensorResponse : public InfoResponseProtoMessage { | |||||||
|   bool force_update{false}; |   bool force_update{false}; | ||||||
|   std::string device_class{}; |   std::string device_class{}; | ||||||
|   enums::SensorStateClass state_class{}; |   enums::SensorStateClass state_class{}; | ||||||
|   enums::SensorLastResetType legacy_last_reset_type{}; |  | ||||||
|   void encode(ProtoWriteBuffer buffer) const override; |   void encode(ProtoWriteBuffer buffer) const override; | ||||||
|   void calculate_size(uint32_t &total_size) const override; |   void calculate_size(uint32_t &total_size) const override; | ||||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
| @@ -1281,7 +1245,7 @@ class CameraImageRequest : public ProtoDecodableMessage { | |||||||
| class ListEntitiesClimateResponse : public InfoResponseProtoMessage { | class ListEntitiesClimateResponse : public InfoResponseProtoMessage { | ||||||
|  public: |  public: | ||||||
|   static constexpr uint8_t MESSAGE_TYPE = 46; |   static constexpr uint8_t MESSAGE_TYPE = 46; | ||||||
|   static constexpr uint8_t ESTIMATED_SIZE = 147; |   static constexpr uint8_t ESTIMATED_SIZE = 145; | ||||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
|   const char *message_name() const override { return "list_entities_climate_response"; } |   const char *message_name() const override { return "list_entities_climate_response"; } | ||||||
| #endif | #endif | ||||||
| @@ -1291,7 +1255,6 @@ class ListEntitiesClimateResponse : public InfoResponseProtoMessage { | |||||||
|   float visual_min_temperature{0.0f}; |   float visual_min_temperature{0.0f}; | ||||||
|   float visual_max_temperature{0.0f}; |   float visual_max_temperature{0.0f}; | ||||||
|   float visual_target_temperature_step{0.0f}; |   float visual_target_temperature_step{0.0f}; | ||||||
|   bool legacy_supports_away{false}; |  | ||||||
|   bool supports_action{false}; |   bool supports_action{false}; | ||||||
|   std::vector<enums::ClimateFanMode> supported_fan_modes{}; |   std::vector<enums::ClimateFanMode> supported_fan_modes{}; | ||||||
|   std::vector<enums::ClimateSwingMode> supported_swing_modes{}; |   std::vector<enums::ClimateSwingMode> supported_swing_modes{}; | ||||||
| @@ -1314,7 +1277,7 @@ class ListEntitiesClimateResponse : public InfoResponseProtoMessage { | |||||||
| class ClimateStateResponse : public StateResponseProtoMessage { | class ClimateStateResponse : public StateResponseProtoMessage { | ||||||
|  public: |  public: | ||||||
|   static constexpr uint8_t MESSAGE_TYPE = 47; |   static constexpr uint8_t MESSAGE_TYPE = 47; | ||||||
|   static constexpr uint8_t ESTIMATED_SIZE = 70; |   static constexpr uint8_t ESTIMATED_SIZE = 68; | ||||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
|   const char *message_name() const override { return "climate_state_response"; } |   const char *message_name() const override { return "climate_state_response"; } | ||||||
| #endif | #endif | ||||||
| @@ -1323,7 +1286,6 @@ class ClimateStateResponse : public StateResponseProtoMessage { | |||||||
|   float target_temperature{0.0f}; |   float target_temperature{0.0f}; | ||||||
|   float target_temperature_low{0.0f}; |   float target_temperature_low{0.0f}; | ||||||
|   float target_temperature_high{0.0f}; |   float target_temperature_high{0.0f}; | ||||||
|   bool unused_legacy_away{false}; |  | ||||||
|   enums::ClimateAction action{}; |   enums::ClimateAction action{}; | ||||||
|   enums::ClimateFanMode fan_mode{}; |   enums::ClimateFanMode fan_mode{}; | ||||||
|   enums::ClimateSwingMode swing_mode{}; |   enums::ClimateSwingMode swing_mode{}; | ||||||
| @@ -1343,7 +1305,7 @@ class ClimateStateResponse : public StateResponseProtoMessage { | |||||||
| class ClimateCommandRequest : public CommandProtoMessage { | class ClimateCommandRequest : public CommandProtoMessage { | ||||||
|  public: |  public: | ||||||
|   static constexpr uint8_t MESSAGE_TYPE = 48; |   static constexpr uint8_t MESSAGE_TYPE = 48; | ||||||
|   static constexpr uint8_t ESTIMATED_SIZE = 88; |   static constexpr uint8_t ESTIMATED_SIZE = 84; | ||||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
|   const char *message_name() const override { return "climate_command_request"; } |   const char *message_name() const override { return "climate_command_request"; } | ||||||
| #endif | #endif | ||||||
| @@ -1355,8 +1317,6 @@ class ClimateCommandRequest : public CommandProtoMessage { | |||||||
|   float target_temperature_low{0.0f}; |   float target_temperature_low{0.0f}; | ||||||
|   bool has_target_temperature_high{false}; |   bool has_target_temperature_high{false}; | ||||||
|   float target_temperature_high{0.0f}; |   float target_temperature_high{0.0f}; | ||||||
|   bool unused_has_legacy_away{false}; |  | ||||||
|   bool unused_legacy_away{false}; |  | ||||||
|   bool has_fan_mode{false}; |   bool has_fan_mode{false}; | ||||||
|   enums::ClimateFanMode fan_mode{}; |   enums::ClimateFanMode fan_mode{}; | ||||||
|   bool has_swing_mode{false}; |   bool has_swing_mode{false}; | ||||||
| @@ -1728,41 +1688,6 @@ class SubscribeBluetoothLEAdvertisementsRequest : public ProtoDecodableMessage { | |||||||
|  protected: |  protected: | ||||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; |   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||||
| }; | }; | ||||||
| class BluetoothServiceData : public ProtoMessage { |  | ||||||
|  public: |  | ||||||
|   std::string uuid{}; |  | ||||||
|   std::vector<uint32_t> legacy_data{}; |  | ||||||
|   std::string data{}; |  | ||||||
|   void encode(ProtoWriteBuffer buffer) const override; |  | ||||||
|   void calculate_size(uint32_t &total_size) const override; |  | ||||||
| #ifdef HAS_PROTO_MESSAGE_DUMP |  | ||||||
|   void dump_to(std::string &out) const override; |  | ||||||
| #endif |  | ||||||
|  |  | ||||||
|  protected: |  | ||||||
| }; |  | ||||||
| class BluetoothLEAdvertisementResponse : public ProtoMessage { |  | ||||||
|  public: |  | ||||||
|   static constexpr uint8_t MESSAGE_TYPE = 67; |  | ||||||
|   static constexpr uint8_t ESTIMATED_SIZE = 107; |  | ||||||
| #ifdef HAS_PROTO_MESSAGE_DUMP |  | ||||||
|   const char *message_name() const override { return "bluetooth_le_advertisement_response"; } |  | ||||||
| #endif |  | ||||||
|   uint64_t address{0}; |  | ||||||
|   std::string name{}; |  | ||||||
|   int32_t rssi{0}; |  | ||||||
|   std::vector<std::string> service_uuids{}; |  | ||||||
|   std::vector<BluetoothServiceData> service_data{}; |  | ||||||
|   std::vector<BluetoothServiceData> manufacturer_data{}; |  | ||||||
|   uint32_t address_type{0}; |  | ||||||
|   void encode(ProtoWriteBuffer buffer) const override; |  | ||||||
|   void calculate_size(uint32_t &total_size) const override; |  | ||||||
| #ifdef HAS_PROTO_MESSAGE_DUMP |  | ||||||
|   void dump_to(std::string &out) const override; |  | ||||||
| #endif |  | ||||||
|  |  | ||||||
|  protected: |  | ||||||
| }; |  | ||||||
| class BluetoothLERawAdvertisement : public ProtoMessage { | class BluetoothLERawAdvertisement : public ProtoMessage { | ||||||
|  public: |  public: | ||||||
|   uint64_t address{0}; |   uint64_t address{0}; | ||||||
|   | |||||||
| @@ -23,16 +23,6 @@ template<> const char *proto_enum_to_string<enums::EntityCategory>(enums::Entity | |||||||
|   } |   } | ||||||
| } | } | ||||||
| #ifdef USE_COVER | #ifdef USE_COVER | ||||||
| template<> const char *proto_enum_to_string<enums::LegacyCoverState>(enums::LegacyCoverState value) { |  | ||||||
|   switch (value) { |  | ||||||
|     case enums::LEGACY_COVER_STATE_OPEN: |  | ||||||
|       return "LEGACY_COVER_STATE_OPEN"; |  | ||||||
|     case enums::LEGACY_COVER_STATE_CLOSED: |  | ||||||
|       return "LEGACY_COVER_STATE_CLOSED"; |  | ||||||
|     default: |  | ||||||
|       return "UNKNOWN"; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| template<> const char *proto_enum_to_string<enums::CoverOperation>(enums::CoverOperation value) { | template<> const char *proto_enum_to_string<enums::CoverOperation>(enums::CoverOperation value) { | ||||||
|   switch (value) { |   switch (value) { | ||||||
|     case enums::COVER_OPERATION_IDLE: |     case enums::COVER_OPERATION_IDLE: | ||||||
| @@ -45,32 +35,8 @@ template<> const char *proto_enum_to_string<enums::CoverOperation>(enums::CoverO | |||||||
|       return "UNKNOWN"; |       return "UNKNOWN"; | ||||||
|   } |   } | ||||||
| } | } | ||||||
| template<> const char *proto_enum_to_string<enums::LegacyCoverCommand>(enums::LegacyCoverCommand value) { |  | ||||||
|   switch (value) { |  | ||||||
|     case enums::LEGACY_COVER_COMMAND_OPEN: |  | ||||||
|       return "LEGACY_COVER_COMMAND_OPEN"; |  | ||||||
|     case enums::LEGACY_COVER_COMMAND_CLOSE: |  | ||||||
|       return "LEGACY_COVER_COMMAND_CLOSE"; |  | ||||||
|     case enums::LEGACY_COVER_COMMAND_STOP: |  | ||||||
|       return "LEGACY_COVER_COMMAND_STOP"; |  | ||||||
|     default: |  | ||||||
|       return "UNKNOWN"; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| #endif | #endif | ||||||
| #ifdef USE_FAN | #ifdef USE_FAN | ||||||
| template<> const char *proto_enum_to_string<enums::FanSpeed>(enums::FanSpeed value) { |  | ||||||
|   switch (value) { |  | ||||||
|     case enums::FAN_SPEED_LOW: |  | ||||||
|       return "FAN_SPEED_LOW"; |  | ||||||
|     case enums::FAN_SPEED_MEDIUM: |  | ||||||
|       return "FAN_SPEED_MEDIUM"; |  | ||||||
|     case enums::FAN_SPEED_HIGH: |  | ||||||
|       return "FAN_SPEED_HIGH"; |  | ||||||
|     default: |  | ||||||
|       return "UNKNOWN"; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| template<> const char *proto_enum_to_string<enums::FanDirection>(enums::FanDirection value) { | template<> const char *proto_enum_to_string<enums::FanDirection>(enums::FanDirection value) { | ||||||
|   switch (value) { |   switch (value) { | ||||||
|     case enums::FAN_DIRECTION_FORWARD: |     case enums::FAN_DIRECTION_FORWARD: | ||||||
| @@ -127,18 +93,6 @@ template<> const char *proto_enum_to_string<enums::SensorStateClass>(enums::Sens | |||||||
|       return "UNKNOWN"; |       return "UNKNOWN"; | ||||||
|   } |   } | ||||||
| } | } | ||||||
| template<> const char *proto_enum_to_string<enums::SensorLastResetType>(enums::SensorLastResetType value) { |  | ||||||
|   switch (value) { |  | ||||||
|     case enums::LAST_RESET_NONE: |  | ||||||
|       return "LAST_RESET_NONE"; |  | ||||||
|     case enums::LAST_RESET_NEVER: |  | ||||||
|       return "LAST_RESET_NEVER"; |  | ||||||
|     case enums::LAST_RESET_AUTO: |  | ||||||
|       return "LAST_RESET_AUTO"; |  | ||||||
|     default: |  | ||||||
|       return "UNKNOWN"; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| #endif | #endif | ||||||
| template<> const char *proto_enum_to_string<enums::LogLevel>(enums::LogLevel value) { | template<> const char *proto_enum_to_string<enums::LogLevel>(enums::LogLevel value) { | ||||||
|   switch (value) { |   switch (value) { | ||||||
| @@ -737,13 +691,6 @@ void DeviceInfoResponse::dump_to(std::string &out) const { | |||||||
|   out.append(buffer); |   out.append(buffer); | ||||||
|   out.append("\n"); |   out.append("\n"); | ||||||
|  |  | ||||||
| #endif |  | ||||||
| #ifdef USE_BLUETOOTH_PROXY |  | ||||||
|   out.append("  legacy_bluetooth_proxy_version: "); |  | ||||||
|   snprintf(buffer, sizeof(buffer), "%" PRIu32, this->legacy_bluetooth_proxy_version); |  | ||||||
|   out.append(buffer); |  | ||||||
|   out.append("\n"); |  | ||||||
|  |  | ||||||
| #endif | #endif | ||||||
| #ifdef USE_BLUETOOTH_PROXY | #ifdef USE_BLUETOOTH_PROXY | ||||||
|   out.append("  bluetooth_proxy_feature_flags: "); |   out.append("  bluetooth_proxy_feature_flags: "); | ||||||
| @@ -760,13 +707,6 @@ void DeviceInfoResponse::dump_to(std::string &out) const { | |||||||
|   out.append("'").append(this->friendly_name).append("'"); |   out.append("'").append(this->friendly_name).append("'"); | ||||||
|   out.append("\n"); |   out.append("\n"); | ||||||
|  |  | ||||||
| #ifdef USE_VOICE_ASSISTANT |  | ||||||
|   out.append("  legacy_voice_assistant_version: "); |  | ||||||
|   snprintf(buffer, sizeof(buffer), "%" PRIu32, this->legacy_voice_assistant_version); |  | ||||||
|   out.append(buffer); |  | ||||||
|   out.append("\n"); |  | ||||||
|  |  | ||||||
| #endif |  | ||||||
| #ifdef USE_VOICE_ASSISTANT | #ifdef USE_VOICE_ASSISTANT | ||||||
|   out.append("  voice_assistant_feature_flags: "); |   out.append("  voice_assistant_feature_flags: "); | ||||||
|   snprintf(buffer, sizeof(buffer), "%" PRIu32, this->voice_assistant_feature_flags); |   snprintf(buffer, sizeof(buffer), "%" PRIu32, this->voice_assistant_feature_flags); | ||||||
| @@ -961,10 +901,6 @@ void CoverStateResponse::dump_to(std::string &out) const { | |||||||
|   out.append(buffer); |   out.append(buffer); | ||||||
|   out.append("\n"); |   out.append("\n"); | ||||||
|  |  | ||||||
|   out.append("  legacy_state: "); |  | ||||||
|   out.append(proto_enum_to_string<enums::LegacyCoverState>(this->legacy_state)); |  | ||||||
|   out.append("\n"); |  | ||||||
|  |  | ||||||
|   out.append("  position: "); |   out.append("  position: "); | ||||||
|   snprintf(buffer, sizeof(buffer), "%g", this->position); |   snprintf(buffer, sizeof(buffer), "%g", this->position); | ||||||
|   out.append(buffer); |   out.append(buffer); | ||||||
| @@ -996,14 +932,6 @@ void CoverCommandRequest::dump_to(std::string &out) const { | |||||||
|   out.append(buffer); |   out.append(buffer); | ||||||
|   out.append("\n"); |   out.append("\n"); | ||||||
|  |  | ||||||
|   out.append("  has_legacy_command: "); |  | ||||||
|   out.append(YESNO(this->has_legacy_command)); |  | ||||||
|   out.append("\n"); |  | ||||||
|  |  | ||||||
|   out.append("  legacy_command: "); |  | ||||||
|   out.append(proto_enum_to_string<enums::LegacyCoverCommand>(this->legacy_command)); |  | ||||||
|   out.append("\n"); |  | ||||||
|  |  | ||||||
|   out.append("  has_position: "); |   out.append("  has_position: "); | ||||||
|   out.append(YESNO(this->has_position)); |   out.append(YESNO(this->has_position)); | ||||||
|   out.append("\n"); |   out.append("\n"); | ||||||
| @@ -1115,10 +1043,6 @@ void FanStateResponse::dump_to(std::string &out) const { | |||||||
|   out.append(YESNO(this->oscillating)); |   out.append(YESNO(this->oscillating)); | ||||||
|   out.append("\n"); |   out.append("\n"); | ||||||
|  |  | ||||||
|   out.append("  speed: "); |  | ||||||
|   out.append(proto_enum_to_string<enums::FanSpeed>(this->speed)); |  | ||||||
|   out.append("\n"); |  | ||||||
|  |  | ||||||
|   out.append("  direction: "); |   out.append("  direction: "); | ||||||
|   out.append(proto_enum_to_string<enums::FanDirection>(this->direction)); |   out.append(proto_enum_to_string<enums::FanDirection>(this->direction)); | ||||||
|   out.append("\n"); |   out.append("\n"); | ||||||
| @@ -1157,14 +1081,6 @@ void FanCommandRequest::dump_to(std::string &out) const { | |||||||
|   out.append(YESNO(this->state)); |   out.append(YESNO(this->state)); | ||||||
|   out.append("\n"); |   out.append("\n"); | ||||||
|  |  | ||||||
|   out.append("  has_speed: "); |  | ||||||
|   out.append(YESNO(this->has_speed)); |  | ||||||
|   out.append("\n"); |  | ||||||
|  |  | ||||||
|   out.append("  speed: "); |  | ||||||
|   out.append(proto_enum_to_string<enums::FanSpeed>(this->speed)); |  | ||||||
|   out.append("\n"); |  | ||||||
|  |  | ||||||
|   out.append("  has_oscillating: "); |   out.append("  has_oscillating: "); | ||||||
|   out.append(YESNO(this->has_oscillating)); |   out.append(YESNO(this->has_oscillating)); | ||||||
|   out.append("\n"); |   out.append("\n"); | ||||||
| @@ -1231,22 +1147,6 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const { | |||||||
|     out.append("\n"); |     out.append("\n"); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   out.append("  legacy_supports_brightness: "); |  | ||||||
|   out.append(YESNO(this->legacy_supports_brightness)); |  | ||||||
|   out.append("\n"); |  | ||||||
|  |  | ||||||
|   out.append("  legacy_supports_rgb: "); |  | ||||||
|   out.append(YESNO(this->legacy_supports_rgb)); |  | ||||||
|   out.append("\n"); |  | ||||||
|  |  | ||||||
|   out.append("  legacy_supports_white_value: "); |  | ||||||
|   out.append(YESNO(this->legacy_supports_white_value)); |  | ||||||
|   out.append("\n"); |  | ||||||
|  |  | ||||||
|   out.append("  legacy_supports_color_temperature: "); |  | ||||||
|   out.append(YESNO(this->legacy_supports_color_temperature)); |  | ||||||
|   out.append("\n"); |  | ||||||
|  |  | ||||||
|   out.append("  min_mireds: "); |   out.append("  min_mireds: "); | ||||||
|   snprintf(buffer, sizeof(buffer), "%g", this->min_mireds); |   snprintf(buffer, sizeof(buffer), "%g", this->min_mireds); | ||||||
|   out.append(buffer); |   out.append(buffer); | ||||||
| @@ -1537,10 +1437,6 @@ void ListEntitiesSensorResponse::dump_to(std::string &out) const { | |||||||
|   out.append(proto_enum_to_string<enums::SensorStateClass>(this->state_class)); |   out.append(proto_enum_to_string<enums::SensorStateClass>(this->state_class)); | ||||||
|   out.append("\n"); |   out.append("\n"); | ||||||
|  |  | ||||||
|   out.append("  legacy_last_reset_type: "); |  | ||||||
|   out.append(proto_enum_to_string<enums::SensorLastResetType>(this->legacy_last_reset_type)); |  | ||||||
|   out.append("\n"); |  | ||||||
|  |  | ||||||
|   out.append("  disabled_by_default: "); |   out.append("  disabled_by_default: "); | ||||||
|   out.append(YESNO(this->disabled_by_default)); |   out.append(YESNO(this->disabled_by_default)); | ||||||
|   out.append("\n"); |   out.append("\n"); | ||||||
| @@ -2107,10 +2003,6 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { | |||||||
|   out.append(buffer); |   out.append(buffer); | ||||||
|   out.append("\n"); |   out.append("\n"); | ||||||
|  |  | ||||||
|   out.append("  legacy_supports_away: "); |  | ||||||
|   out.append(YESNO(this->legacy_supports_away)); |  | ||||||
|   out.append("\n"); |  | ||||||
|  |  | ||||||
|   out.append("  supports_action: "); |   out.append("  supports_action: "); | ||||||
|   out.append(YESNO(this->supports_action)); |   out.append(YESNO(this->supports_action)); | ||||||
|   out.append("\n"); |   out.append("\n"); | ||||||
| @@ -2223,10 +2115,6 @@ void ClimateStateResponse::dump_to(std::string &out) const { | |||||||
|   out.append(buffer); |   out.append(buffer); | ||||||
|   out.append("\n"); |   out.append("\n"); | ||||||
|  |  | ||||||
|   out.append("  unused_legacy_away: "); |  | ||||||
|   out.append(YESNO(this->unused_legacy_away)); |  | ||||||
|   out.append("\n"); |  | ||||||
|  |  | ||||||
|   out.append("  action: "); |   out.append("  action: "); | ||||||
|   out.append(proto_enum_to_string<enums::ClimateAction>(this->action)); |   out.append(proto_enum_to_string<enums::ClimateAction>(this->action)); | ||||||
|   out.append("\n"); |   out.append("\n"); | ||||||
| @@ -2313,14 +2201,6 @@ void ClimateCommandRequest::dump_to(std::string &out) const { | |||||||
|   out.append(buffer); |   out.append(buffer); | ||||||
|   out.append("\n"); |   out.append("\n"); | ||||||
|  |  | ||||||
|   out.append("  unused_has_legacy_away: "); |  | ||||||
|   out.append(YESNO(this->unused_has_legacy_away)); |  | ||||||
|   out.append("\n"); |  | ||||||
|  |  | ||||||
|   out.append("  unused_legacy_away: "); |  | ||||||
|   out.append(YESNO(this->unused_legacy_away)); |  | ||||||
|   out.append("\n"); |  | ||||||
|  |  | ||||||
|   out.append("  has_fan_mode: "); |   out.append("  has_fan_mode: "); | ||||||
|   out.append(YESNO(this->has_fan_mode)); |   out.append(YESNO(this->has_fan_mode)); | ||||||
|   out.append("\n"); |   out.append("\n"); | ||||||
| @@ -3053,66 +2933,6 @@ void SubscribeBluetoothLEAdvertisementsRequest::dump_to(std::string &out) const | |||||||
|   out.append("\n"); |   out.append("\n"); | ||||||
|   out.append("}"); |   out.append("}"); | ||||||
| } | } | ||||||
| void BluetoothServiceData::dump_to(std::string &out) const { |  | ||||||
|   __attribute__((unused)) char buffer[64]; |  | ||||||
|   out.append("BluetoothServiceData {\n"); |  | ||||||
|   out.append("  uuid: "); |  | ||||||
|   out.append("'").append(this->uuid).append("'"); |  | ||||||
|   out.append("\n"); |  | ||||||
|  |  | ||||||
|   for (const auto &it : this->legacy_data) { |  | ||||||
|     out.append("  legacy_data: "); |  | ||||||
|     snprintf(buffer, sizeof(buffer), "%" PRIu32, it); |  | ||||||
|     out.append(buffer); |  | ||||||
|     out.append("\n"); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   out.append("  data: "); |  | ||||||
|   out.append(format_hex_pretty(this->data)); |  | ||||||
|   out.append("\n"); |  | ||||||
|   out.append("}"); |  | ||||||
| } |  | ||||||
| void BluetoothLEAdvertisementResponse::dump_to(std::string &out) const { |  | ||||||
|   __attribute__((unused)) char buffer[64]; |  | ||||||
|   out.append("BluetoothLEAdvertisementResponse {\n"); |  | ||||||
|   out.append("  address: "); |  | ||||||
|   snprintf(buffer, sizeof(buffer), "%llu", this->address); |  | ||||||
|   out.append(buffer); |  | ||||||
|   out.append("\n"); |  | ||||||
|  |  | ||||||
|   out.append("  name: "); |  | ||||||
|   out.append(format_hex_pretty(this->name)); |  | ||||||
|   out.append("\n"); |  | ||||||
|  |  | ||||||
|   out.append("  rssi: "); |  | ||||||
|   snprintf(buffer, sizeof(buffer), "%" PRId32, this->rssi); |  | ||||||
|   out.append(buffer); |  | ||||||
|   out.append("\n"); |  | ||||||
|  |  | ||||||
|   for (const auto &it : this->service_uuids) { |  | ||||||
|     out.append("  service_uuids: "); |  | ||||||
|     out.append("'").append(it).append("'"); |  | ||||||
|     out.append("\n"); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   for (const auto &it : this->service_data) { |  | ||||||
|     out.append("  service_data: "); |  | ||||||
|     it.dump_to(out); |  | ||||||
|     out.append("\n"); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   for (const auto &it : this->manufacturer_data) { |  | ||||||
|     out.append("  manufacturer_data: "); |  | ||||||
|     it.dump_to(out); |  | ||||||
|     out.append("\n"); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   out.append("  address_type: "); |  | ||||||
|   snprintf(buffer, sizeof(buffer), "%" PRIu32, this->address_type); |  | ||||||
|   out.append(buffer); |  | ||||||
|   out.append("\n"); |  | ||||||
|   out.append("}"); |  | ||||||
| } |  | ||||||
| void BluetoothLERawAdvertisement::dump_to(std::string &out) const { | void BluetoothLERawAdvertisement::dump_to(std::string &out) const { | ||||||
|   __attribute__((unused)) char buffer[64]; |   __attribute__((unused)) char buffer[64]; | ||||||
|   out.append("BluetoothLERawAdvertisement {\n"); |   out.append("BluetoothLERawAdvertisement {\n"); | ||||||
|   | |||||||
| @@ -140,46 +140,6 @@ void BluetoothProxy::flush_pending_advertisements() { | |||||||
|   this->advertisement_count_ = 0; |   this->advertisement_count_ = 0; | ||||||
| } | } | ||||||
|  |  | ||||||
| #ifdef USE_ESP32_BLE_DEVICE |  | ||||||
| void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device) { |  | ||||||
|   api::BluetoothLEAdvertisementResponse resp; |  | ||||||
|   resp.address = device.address_uint64(); |  | ||||||
|   resp.address_type = device.get_address_type(); |  | ||||||
|   if (!device.get_name().empty()) |  | ||||||
|     resp.name = device.get_name(); |  | ||||||
|   resp.rssi = device.get_rssi(); |  | ||||||
|  |  | ||||||
|   // Pre-allocate vectors based on known sizes |  | ||||||
|   auto service_uuids = device.get_service_uuids(); |  | ||||||
|   resp.service_uuids.reserve(service_uuids.size()); |  | ||||||
|   for (auto &uuid : service_uuids) { |  | ||||||
|     resp.service_uuids.emplace_back(uuid.to_string()); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   // Pre-allocate service data vector |  | ||||||
|   auto service_datas = device.get_service_datas(); |  | ||||||
|   resp.service_data.reserve(service_datas.size()); |  | ||||||
|   for (auto &data : service_datas) { |  | ||||||
|     resp.service_data.emplace_back(); |  | ||||||
|     auto &service_data = resp.service_data.back(); |  | ||||||
|     service_data.uuid = data.uuid.to_string(); |  | ||||||
|     service_data.data.assign(data.data.begin(), data.data.end()); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   // Pre-allocate manufacturer data vector |  | ||||||
|   auto manufacturer_datas = device.get_manufacturer_datas(); |  | ||||||
|   resp.manufacturer_data.reserve(manufacturer_datas.size()); |  | ||||||
|   for (auto &data : manufacturer_datas) { |  | ||||||
|     resp.manufacturer_data.emplace_back(); |  | ||||||
|     auto &manufacturer_data = resp.manufacturer_data.back(); |  | ||||||
|     manufacturer_data.uuid = data.uuid.to_string(); |  | ||||||
|     manufacturer_data.data.assign(data.data.begin(), data.data.end()); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   this->api_connection_->send_message(resp, api::BluetoothLEAdvertisementResponse::MESSAGE_TYPE); |  | ||||||
| } |  | ||||||
| #endif  // USE_ESP32_BLE_DEVICE |  | ||||||
|  |  | ||||||
| void BluetoothProxy::dump_config() { | void BluetoothProxy::dump_config() { | ||||||
|   ESP_LOGCONFIG(TAG, "Bluetooth Proxy:"); |   ESP_LOGCONFIG(TAG, "Bluetooth Proxy:"); | ||||||
|   ESP_LOGCONFIG(TAG, |   ESP_LOGCONFIG(TAG, | ||||||
|   | |||||||
| @@ -131,9 +131,6 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com | |||||||
|   } |   } | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
| #ifdef USE_ESP32_BLE_DEVICE |  | ||||||
|   void send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device); |  | ||||||
| #endif |  | ||||||
|   void send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerState state); |   void send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerState state); | ||||||
|  |  | ||||||
|   BluetoothConnection *get_connection_(uint64_t address, bool reserve); |   BluetoothConnection *get_connection_(uint64_t address, bool reserve); | ||||||
|   | |||||||
| @@ -192,7 +192,7 @@ class WidgetType: | |||||||
|  |  | ||||||
| class NumberType(WidgetType): | class NumberType(WidgetType): | ||||||
|     def get_max(self, config: dict): |     def get_max(self, config: dict): | ||||||
|         return int(config[CONF_MAX_VALUE] or 100) |         return int(config.get(CONF_MAX_VALUE, 100)) | ||||||
|  |  | ||||||
|     def get_min(self, config: dict): |     def get_min(self, config: dict): | ||||||
|         return int(config[CONF_MIN_VALUE] or 0) |         return int(config.get(CONF_MIN_VALUE, 0)) | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| #pragma once | #pragma once | ||||||
|  |  | ||||||
| #if defined(USE_ESP32) || defined(USE_LIBRETINY) | #if defined(USE_ESP32) | ||||||
|  |  | ||||||
| #include <atomic> | #include <atomic> | ||||||
| #include <cstddef> | #include <cstddef> | ||||||
| @@ -78,4 +78,4 @@ template<class T, uint8_t SIZE> class EventPool { | |||||||
|  |  | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|  |  | ||||||
| #endif  // defined(USE_ESP32) || defined(USE_LIBRETINY) | #endif  // defined(USE_ESP32) | ||||||
|   | |||||||
| @@ -1,17 +1,12 @@ | |||||||
| #pragma once | #pragma once | ||||||
|  |  | ||||||
| #if defined(USE_ESP32) || defined(USE_LIBRETINY) | #if defined(USE_ESP32) | ||||||
|  |  | ||||||
| #include <atomic> | #include <atomic> | ||||||
| #include <cstddef> | #include <cstddef> | ||||||
|  |  | ||||||
| #if defined(USE_ESP32) |  | ||||||
| #include <freertos/FreeRTOS.h> | #include <freertos/FreeRTOS.h> | ||||||
| #include <freertos/task.h> | #include <freertos/task.h> | ||||||
| #elif defined(USE_LIBRETINY) |  | ||||||
| #include <FreeRTOS.h> |  | ||||||
| #include <task.h> |  | ||||||
| #endif |  | ||||||
|  |  | ||||||
| /* | /* | ||||||
|  * Lock-free queue for single-producer single-consumer scenarios. |  * Lock-free queue for single-producer single-consumer scenarios. | ||||||
| @@ -148,4 +143,4 @@ template<class T, uint8_t SIZE> class NotifyingLockFreeQueue : public LockFreeQu | |||||||
|  |  | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|  |  | ||||||
| #endif  // defined(USE_ESP32) || defined(USE_LIBRETINY) | #endif  // defined(USE_ESP32) | ||||||
|   | |||||||
| @@ -138,7 +138,7 @@ lib_deps = | |||||||
|     WiFi                                 ; wifi,web_server_base,ethernet (Arduino built-in) |     WiFi                                 ; wifi,web_server_base,ethernet (Arduino built-in) | ||||||
|     Update                               ; ota,web_server_base (Arduino built-in) |     Update                               ; ota,web_server_base (Arduino built-in) | ||||||
|     ${common:arduino.lib_deps} |     ${common:arduino.lib_deps} | ||||||
|     ESP32Async/AsyncTCP@3.4.4            ; async_tcp |     ESP32Async/AsyncTCP@3.4.5            ; async_tcp | ||||||
|     NetworkClientSecure                  ; http_request,nextion (Arduino built-in) |     NetworkClientSecure                  ; http_request,nextion (Arduino built-in) | ||||||
|     HTTPClient                           ; http_request,nextion (Arduino built-in) |     HTTPClient                           ; http_request,nextion (Arduino built-in) | ||||||
|     ESPmDNS                              ; mdns (Arduino built-in) |     ESPmDNS                              ; mdns (Arduino built-in) | ||||||
|   | |||||||
| @@ -12,7 +12,7 @@ platformio==6.1.18  # When updating platformio, also update /docker/Dockerfile | |||||||
| esptool==4.9.0 | esptool==4.9.0 | ||||||
| click==8.1.7 | click==8.1.7 | ||||||
| esphome-dashboard==20250514.0 | esphome-dashboard==20250514.0 | ||||||
| aioesphomeapi==36.0.1 | aioesphomeapi==37.0.0 | ||||||
| zeroconf==0.147.0 | zeroconf==0.147.0 | ||||||
| puremagic==1.30 | puremagic==1.30 | ||||||
| ruamel.yaml==0.18.14 # dashboard_import | ruamel.yaml==0.18.14 # dashboard_import | ||||||
|   | |||||||
| @@ -971,11 +971,11 @@ class RepeatedTypeInfo(TypeInfo): | |||||||
|  |  | ||||||
| def build_type_usage_map( | def build_type_usage_map( | ||||||
|     file_desc: descriptor.FileDescriptorProto, |     file_desc: descriptor.FileDescriptorProto, | ||||||
| ) -> tuple[dict[str, str | None], dict[str, str | None], dict[str, int]]: | ) -> tuple[dict[str, str | None], dict[str, str | None], dict[str, int], set[str]]: | ||||||
|     """Build mappings for both enums and messages to their ifdefs based on usage. |     """Build mappings for both enums and messages to their ifdefs based on usage. | ||||||
|  |  | ||||||
|     Returns: |     Returns: | ||||||
|         tuple: (enum_ifdef_map, message_ifdef_map, message_source_map) |         tuple: (enum_ifdef_map, message_ifdef_map, message_source_map, used_messages) | ||||||
|     """ |     """ | ||||||
|     enum_ifdef_map: dict[str, str | None] = {} |     enum_ifdef_map: dict[str, str | None] = {} | ||||||
|     message_ifdef_map: dict[str, str | None] = {} |     message_ifdef_map: dict[str, str | None] = {} | ||||||
| @@ -988,6 +988,7 @@ def build_type_usage_map( | |||||||
|     message_usage: dict[ |     message_usage: dict[ | ||||||
|         str, set[str] |         str, set[str] | ||||||
|     ] = {}  # message_name -> set of message names that use it |     ] = {}  # message_name -> set of message names that use it | ||||||
|  |     used_messages: set[str] = set()  # Track which messages are actually used | ||||||
|  |  | ||||||
|     # Build message name to ifdef mapping for quick lookup |     # Build message name to ifdef mapping for quick lookup | ||||||
|     message_to_ifdef: dict[str, str | None] = { |     message_to_ifdef: dict[str, str | None] = { | ||||||
| @@ -996,17 +997,26 @@ def build_type_usage_map( | |||||||
|  |  | ||||||
|     # Analyze field usage |     # Analyze field usage | ||||||
|     for message in file_desc.message_type: |     for message in file_desc.message_type: | ||||||
|  |         # Skip deprecated messages entirely | ||||||
|  |         if message.options.deprecated: | ||||||
|  |             continue | ||||||
|  |  | ||||||
|         for field in message.field: |         for field in message.field: | ||||||
|  |             # Skip deprecated fields when tracking enum usage | ||||||
|  |             if field.options.deprecated: | ||||||
|  |                 continue | ||||||
|  |  | ||||||
|             type_name = field.type_name.split(".")[-1] if field.type_name else None |             type_name = field.type_name.split(".")[-1] if field.type_name else None | ||||||
|             if not type_name: |             if not type_name: | ||||||
|                 continue |                 continue | ||||||
|  |  | ||||||
|             # Track enum usage |             # Track enum usage (only from non-deprecated fields) | ||||||
|             if field.type == 14:  # TYPE_ENUM |             if field.type == 14:  # TYPE_ENUM | ||||||
|                 enum_usage.setdefault(type_name, set()).add(message.name) |                 enum_usage.setdefault(type_name, set()).add(message.name) | ||||||
|             # Track message usage |             # Track message usage | ||||||
|             elif field.type == 11:  # TYPE_MESSAGE |             elif field.type == 11:  # TYPE_MESSAGE | ||||||
|                 message_usage.setdefault(type_name, set()).add(message.name) |                 message_usage.setdefault(type_name, set()).add(message.name) | ||||||
|  |                 used_messages.add(type_name) | ||||||
|  |  | ||||||
|     # Helper to get unique ifdef from a set of messages |     # Helper to get unique ifdef from a set of messages | ||||||
|     def get_unique_ifdef(message_names: set[str]) -> str | None: |     def get_unique_ifdef(message_names: set[str]) -> str | None: | ||||||
| @@ -1069,12 +1079,18 @@ def build_type_usage_map( | |||||||
|     # Build message source map |     # Build message source map | ||||||
|     # First pass: Get explicit sources for messages with source option or id |     # First pass: Get explicit sources for messages with source option or id | ||||||
|     for msg in file_desc.message_type: |     for msg in file_desc.message_type: | ||||||
|  |         # Skip deprecated messages | ||||||
|  |         if msg.options.deprecated: | ||||||
|  |             continue | ||||||
|  |  | ||||||
|         if msg.options.HasExtension(pb.source): |         if msg.options.HasExtension(pb.source): | ||||||
|             # Explicit source option takes precedence |             # Explicit source option takes precedence | ||||||
|             message_source_map[msg.name] = get_opt(msg, pb.source, SOURCE_BOTH) |             message_source_map[msg.name] = get_opt(msg, pb.source, SOURCE_BOTH) | ||||||
|         elif msg.options.HasExtension(pb.id): |         elif msg.options.HasExtension(pb.id): | ||||||
|             # Service messages (with id) default to SOURCE_BOTH |             # Service messages (with id) default to SOURCE_BOTH | ||||||
|             message_source_map[msg.name] = SOURCE_BOTH |             message_source_map[msg.name] = SOURCE_BOTH | ||||||
|  |             # Service messages are always used | ||||||
|  |             used_messages.add(msg.name) | ||||||
|  |  | ||||||
|     # Second pass: Determine sources for embedded messages based on their usage |     # Second pass: Determine sources for embedded messages based on their usage | ||||||
|     for msg in file_desc.message_type: |     for msg in file_desc.message_type: | ||||||
| @@ -1103,7 +1119,12 @@ def build_type_usage_map( | |||||||
|             # Not used by any message and no explicit source - default to encode-only |             # Not used by any message and no explicit source - default to encode-only | ||||||
|             message_source_map[msg.name] = SOURCE_SERVER |             message_source_map[msg.name] = SOURCE_SERVER | ||||||
|  |  | ||||||
|     return enum_ifdef_map, message_ifdef_map, message_source_map |     return ( | ||||||
|  |         enum_ifdef_map, | ||||||
|  |         message_ifdef_map, | ||||||
|  |         message_source_map, | ||||||
|  |         used_messages, | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| def build_enum_type(desc, enum_ifdef_map) -> tuple[str, str, str]: | def build_enum_type(desc, enum_ifdef_map) -> tuple[str, str, str]: | ||||||
| @@ -1145,6 +1166,10 @@ def calculate_message_estimated_size(desc: descriptor.DescriptorProto) -> int: | |||||||
|     total_size = 0 |     total_size = 0 | ||||||
|  |  | ||||||
|     for field in desc.field: |     for field in desc.field: | ||||||
|  |         # Skip deprecated fields | ||||||
|  |         if field.options.deprecated: | ||||||
|  |             continue | ||||||
|  |  | ||||||
|         ti = create_field_type_info(field) |         ti = create_field_type_info(field) | ||||||
|  |  | ||||||
|         # Add estimated size for this field |         # Add estimated size for this field | ||||||
| @@ -1213,6 +1238,10 @@ def build_message_type( | |||||||
|         public_content.append("#endif") |         public_content.append("#endif") | ||||||
|  |  | ||||||
|     for field in desc.field: |     for field in desc.field: | ||||||
|  |         # Skip deprecated fields completely | ||||||
|  |         if field.options.deprecated: | ||||||
|  |             continue | ||||||
|  |  | ||||||
|         ti = create_field_type_info(field) |         ti = create_field_type_info(field) | ||||||
|  |  | ||||||
|         # Skip field declarations for fields that are in the base class |         # Skip field declarations for fields that are in the base class | ||||||
| @@ -1459,8 +1488,10 @@ def find_common_fields( | |||||||
|     if not messages: |     if not messages: | ||||||
|         return [] |         return [] | ||||||
|  |  | ||||||
|     # Start with fields from the first message |     # Start with fields from the first message (excluding deprecated fields) | ||||||
|     first_msg_fields = {field.name: field for field in messages[0].field} |     first_msg_fields = { | ||||||
|  |         field.name: field for field in messages[0].field if not field.options.deprecated | ||||||
|  |     } | ||||||
|     common_fields = [] |     common_fields = [] | ||||||
|  |  | ||||||
|     # Check each field to see if it exists in all messages with same type |     # Check each field to see if it exists in all messages with same type | ||||||
| @@ -1471,6 +1502,9 @@ def find_common_fields( | |||||||
|         for msg in messages[1:]: |         for msg in messages[1:]: | ||||||
|             found = False |             found = False | ||||||
|             for other_field in msg.field: |             for other_field in msg.field: | ||||||
|  |                 # Skip deprecated fields | ||||||
|  |                 if other_field.options.deprecated: | ||||||
|  |                     continue | ||||||
|                 if ( |                 if ( | ||||||
|                     other_field.name == field_name |                     other_field.name == field_name | ||||||
|                     and other_field.type == field.type |                     and other_field.type == field.type | ||||||
| @@ -1495,6 +1529,7 @@ def build_base_class( | |||||||
|     base_class_name: str, |     base_class_name: str, | ||||||
|     common_fields: list[descriptor.FieldDescriptorProto], |     common_fields: list[descriptor.FieldDescriptorProto], | ||||||
|     messages: list[descriptor.DescriptorProto], |     messages: list[descriptor.DescriptorProto], | ||||||
|  |     message_source_map: dict[str, int], | ||||||
| ) -> tuple[str, str, str]: | ) -> tuple[str, str, str]: | ||||||
|     """Build the base class definition and implementation.""" |     """Build the base class definition and implementation.""" | ||||||
|     public_content = [] |     public_content = [] | ||||||
| @@ -1511,7 +1546,7 @@ def build_base_class( | |||||||
|  |  | ||||||
|     # Determine if any message using this base class needs decoding |     # Determine if any message using this base class needs decoding | ||||||
|     needs_decode = any( |     needs_decode = any( | ||||||
|         get_opt(msg, pb.source, SOURCE_BOTH) in (SOURCE_BOTH, SOURCE_CLIENT) |         message_source_map.get(msg.name, SOURCE_BOTH) in (SOURCE_BOTH, SOURCE_CLIENT) | ||||||
|         for msg in messages |         for msg in messages | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
| @@ -1543,6 +1578,7 @@ def build_base_class( | |||||||
|  |  | ||||||
| def generate_base_classes( | def generate_base_classes( | ||||||
|     base_class_groups: dict[str, list[descriptor.DescriptorProto]], |     base_class_groups: dict[str, list[descriptor.DescriptorProto]], | ||||||
|  |     message_source_map: dict[str, int], | ||||||
| ) -> tuple[str, str, str]: | ) -> tuple[str, str, str]: | ||||||
|     """Generate all base classes.""" |     """Generate all base classes.""" | ||||||
|     all_headers = [] |     all_headers = [] | ||||||
| @@ -1556,7 +1592,7 @@ def generate_base_classes( | |||||||
|         if common_fields: |         if common_fields: | ||||||
|             # Generate base class |             # Generate base class | ||||||
|             header, cpp, dump_cpp = build_base_class( |             header, cpp, dump_cpp = build_base_class( | ||||||
|                 base_class_name, common_fields, messages |                 base_class_name, common_fields, messages, message_source_map | ||||||
|             ) |             ) | ||||||
|             all_headers.append(header) |             all_headers.append(header) | ||||||
|             all_cpp.append(cpp) |             all_cpp.append(cpp) | ||||||
| @@ -1567,14 +1603,19 @@ def generate_base_classes( | |||||||
|  |  | ||||||
| def build_service_message_type( | def build_service_message_type( | ||||||
|     mt: descriptor.DescriptorProto, |     mt: descriptor.DescriptorProto, | ||||||
|  |     message_source_map: dict[str, int], | ||||||
| ) -> tuple[str, str] | None: | ) -> tuple[str, str] | None: | ||||||
|     """Builds the service message type.""" |     """Builds the service message type.""" | ||||||
|  |     # Skip deprecated messages | ||||||
|  |     if mt.options.deprecated: | ||||||
|  |         return None | ||||||
|  |  | ||||||
|     snake = camel_to_snake(mt.name) |     snake = camel_to_snake(mt.name) | ||||||
|     id_: int | None = get_opt(mt, pb.id) |     id_: int | None = get_opt(mt, pb.id) | ||||||
|     if id_ is None: |     if id_ is None: | ||||||
|         return None |         return None | ||||||
|  |  | ||||||
|     source: int = get_opt(mt, pb.source, 0) |     source: int = message_source_map.get(mt.name, SOURCE_BOTH) | ||||||
|  |  | ||||||
|     ifdef: str | None = get_opt(mt, pb.ifdef) |     ifdef: str | None = get_opt(mt, pb.ifdef) | ||||||
|     log: bool = get_opt(mt, pb.log, True) |     log: bool = get_opt(mt, pb.log, True) | ||||||
| @@ -1670,12 +1711,18 @@ namespace api { | |||||||
|     content += "namespace enums {\n\n" |     content += "namespace enums {\n\n" | ||||||
|  |  | ||||||
|     # Build dynamic ifdef mappings for both enums and messages |     # Build dynamic ifdef mappings for both enums and messages | ||||||
|     enum_ifdef_map, message_ifdef_map, message_source_map = build_type_usage_map(file) |     enum_ifdef_map, message_ifdef_map, message_source_map, used_messages = ( | ||||||
|  |         build_type_usage_map(file) | ||||||
|  |     ) | ||||||
|  |  | ||||||
|     # Simple grouping of enums by ifdef |     # Simple grouping of enums by ifdef | ||||||
|     current_ifdef = None |     current_ifdef = None | ||||||
|  |  | ||||||
|     for enum in file.enum_type: |     for enum in file.enum_type: | ||||||
|  |         # Skip deprecated enums | ||||||
|  |         if enum.options.deprecated: | ||||||
|  |             continue | ||||||
|  |  | ||||||
|         s, c, dc = build_enum_type(enum, enum_ifdef_map) |         s, c, dc = build_enum_type(enum, enum_ifdef_map) | ||||||
|         enum_ifdef = enum_ifdef_map.get(enum.name) |         enum_ifdef = enum_ifdef_map.get(enum.name) | ||||||
|  |  | ||||||
| @@ -1714,7 +1761,9 @@ namespace api { | |||||||
|  |  | ||||||
|     # Generate base classes |     # Generate base classes | ||||||
|     if base_class_fields: |     if base_class_fields: | ||||||
|         base_headers, base_cpp, base_dump_cpp = generate_base_classes(base_class_groups) |         base_headers, base_cpp, base_dump_cpp = generate_base_classes( | ||||||
|  |             base_class_groups, message_source_map | ||||||
|  |         ) | ||||||
|         content += base_headers |         content += base_headers | ||||||
|         cpp += base_cpp |         cpp += base_cpp | ||||||
|         dump_cpp += base_dump_cpp |         dump_cpp += base_dump_cpp | ||||||
| @@ -1724,6 +1773,14 @@ namespace api { | |||||||
|     current_ifdef = None |     current_ifdef = None | ||||||
|  |  | ||||||
|     for m in mt: |     for m in mt: | ||||||
|  |         # Skip deprecated messages | ||||||
|  |         if m.options.deprecated: | ||||||
|  |             continue | ||||||
|  |  | ||||||
|  |         # Skip messages that aren't used (unless they have an ID/service message) | ||||||
|  |         if m.name not in used_messages and not m.options.HasExtension(pb.id): | ||||||
|  |             continue | ||||||
|  |  | ||||||
|         s, c, dc = build_message_type(m, base_class_fields, message_source_map) |         s, c, dc = build_message_type(m, base_class_fields, message_source_map) | ||||||
|         msg_ifdef = message_ifdef_map.get(m.name) |         msg_ifdef = message_ifdef_map.get(m.name) | ||||||
|  |  | ||||||
| @@ -1832,7 +1889,7 @@ static const char *const TAG = "api.service"; | |||||||
|     cpp += "#endif\n\n" |     cpp += "#endif\n\n" | ||||||
|  |  | ||||||
|     for mt in file.message_type: |     for mt in file.message_type: | ||||||
|         obj = build_service_message_type(mt) |         obj = build_service_message_type(mt, message_source_map) | ||||||
|         if obj is None: |         if obj is None: | ||||||
|             continue |             continue | ||||||
|         hout, cout = obj |         hout, cout = obj | ||||||
|   | |||||||
							
								
								
									
										207
									
								
								tests/integration/fixtures/scheduler_retry_test.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										207
									
								
								tests/integration/fixtures/scheduler_retry_test.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,207 @@ | |||||||
|  | esphome: | ||||||
|  |   name: scheduler-retry-test | ||||||
|  |   on_boot: | ||||||
|  |     priority: -100 | ||||||
|  |     then: | ||||||
|  |       - logger.log: "Starting scheduler retry tests" | ||||||
|  |       # Run all tests sequentially with delays | ||||||
|  |       - script.execute: run_all_tests | ||||||
|  |  | ||||||
|  | host: | ||||||
|  | api: | ||||||
|  | logger: | ||||||
|  |   level: VERBOSE | ||||||
|  |  | ||||||
|  | globals: | ||||||
|  |   - id: simple_retry_counter | ||||||
|  |     type: int | ||||||
|  |     initial_value: '0' | ||||||
|  |   - id: backoff_retry_counter | ||||||
|  |     type: int | ||||||
|  |     initial_value: '0' | ||||||
|  |   - id: immediate_done_counter | ||||||
|  |     type: int | ||||||
|  |     initial_value: '0' | ||||||
|  |   - id: cancel_retry_counter | ||||||
|  |     type: int | ||||||
|  |     initial_value: '0' | ||||||
|  |   - id: empty_name_retry_counter | ||||||
|  |     type: int | ||||||
|  |     initial_value: '0' | ||||||
|  |   - id: script_retry_counter | ||||||
|  |     type: int | ||||||
|  |     initial_value: '0' | ||||||
|  |   - id: multiple_same_name_counter | ||||||
|  |     type: int | ||||||
|  |     initial_value: '0' | ||||||
|  |  | ||||||
|  | sensor: | ||||||
|  |   - platform: template | ||||||
|  |     name: Test Sensor | ||||||
|  |     id: test_sensor | ||||||
|  |     lambda: return 1.0; | ||||||
|  |     update_interval: never | ||||||
|  |  | ||||||
|  | script: | ||||||
|  |   - id: run_all_tests | ||||||
|  |     then: | ||||||
|  |       # Test 1: Simple retry | ||||||
|  |       - logger.log: "=== Test 1: Simple retry ===" | ||||||
|  |       - lambda: |- | ||||||
|  |           auto *component = id(test_sensor); | ||||||
|  |           App.scheduler.set_retry(component, "simple_retry", 50, 3, | ||||||
|  |             [](uint8_t retry_countdown) { | ||||||
|  |               id(simple_retry_counter)++; | ||||||
|  |               ESP_LOGI("test", "Simple retry attempt %d (countdown=%d)", | ||||||
|  |                        id(simple_retry_counter), retry_countdown); | ||||||
|  |  | ||||||
|  |               if (id(simple_retry_counter) >= 2) { | ||||||
|  |                 ESP_LOGI("test", "Simple retry succeeded on attempt %d", id(simple_retry_counter)); | ||||||
|  |                 return RetryResult::DONE; | ||||||
|  |               } | ||||||
|  |               return RetryResult::RETRY; | ||||||
|  |             }); | ||||||
|  |  | ||||||
|  |       # Test 2: Backoff retry | ||||||
|  |       - logger.log: "=== Test 2: Retry with backoff ===" | ||||||
|  |       - lambda: |- | ||||||
|  |           auto *component = id(test_sensor); | ||||||
|  |           static uint32_t backoff_start_time = 0; | ||||||
|  |           static uint32_t last_attempt_time = 0; | ||||||
|  |  | ||||||
|  |           backoff_start_time = millis(); | ||||||
|  |           last_attempt_time = backoff_start_time; | ||||||
|  |  | ||||||
|  |           App.scheduler.set_retry(component, "backoff_retry", 50, 4, | ||||||
|  |             [](uint8_t retry_countdown) { | ||||||
|  |               id(backoff_retry_counter)++; | ||||||
|  |               uint32_t now = millis(); | ||||||
|  |               uint32_t interval = now - last_attempt_time; | ||||||
|  |               last_attempt_time = now; | ||||||
|  |  | ||||||
|  |               ESP_LOGI("test", "Backoff retry attempt %d (countdown=%d, interval=%dms)", | ||||||
|  |                        id(backoff_retry_counter), retry_countdown, interval); | ||||||
|  |  | ||||||
|  |               if (id(backoff_retry_counter) == 1) { | ||||||
|  |                 ESP_LOGI("test", "First call was immediate"); | ||||||
|  |               } else if (id(backoff_retry_counter) == 2) { | ||||||
|  |                 ESP_LOGI("test", "Second call interval: %dms (expected ~50ms)", interval); | ||||||
|  |               } else if (id(backoff_retry_counter) == 3) { | ||||||
|  |                 ESP_LOGI("test", "Third call interval: %dms (expected ~100ms)", interval); | ||||||
|  |               } else if (id(backoff_retry_counter) == 4) { | ||||||
|  |                 ESP_LOGI("test", "Fourth call interval: %dms (expected ~200ms)", interval); | ||||||
|  |                 ESP_LOGI("test", "Backoff retry completed"); | ||||||
|  |                 return RetryResult::DONE; | ||||||
|  |               } | ||||||
|  |  | ||||||
|  |               return RetryResult::RETRY; | ||||||
|  |             }, 2.0f); | ||||||
|  |  | ||||||
|  |       # Test 3: Immediate done | ||||||
|  |       - logger.log: "=== Test 3: Immediate done ===" | ||||||
|  |       - lambda: |- | ||||||
|  |           auto *component = id(test_sensor); | ||||||
|  |           App.scheduler.set_retry(component, "immediate_done", 50, 5, | ||||||
|  |             [](uint8_t retry_countdown) { | ||||||
|  |               id(immediate_done_counter)++; | ||||||
|  |               ESP_LOGI("test", "Immediate done retry called (countdown=%d)", retry_countdown); | ||||||
|  |               return RetryResult::DONE; | ||||||
|  |             }); | ||||||
|  |  | ||||||
|  |       # Test 4: Cancel retry | ||||||
|  |       - logger.log: "=== Test 4: Cancel retry ===" | ||||||
|  |       - lambda: |- | ||||||
|  |           auto *component = id(test_sensor); | ||||||
|  |           App.scheduler.set_retry(component, "cancel_test", 25, 10, | ||||||
|  |             [](uint8_t retry_countdown) { | ||||||
|  |               id(cancel_retry_counter)++; | ||||||
|  |               ESP_LOGI("test", "Cancel test retry attempt %d", id(cancel_retry_counter)); | ||||||
|  |               return RetryResult::RETRY; | ||||||
|  |             }); | ||||||
|  |  | ||||||
|  |           // Cancel it after 100ms | ||||||
|  |           App.scheduler.set_timeout(component, "cancel_timer", 100, []() { | ||||||
|  |             bool cancelled = App.scheduler.cancel_retry(id(test_sensor), "cancel_test"); | ||||||
|  |             ESP_LOGI("test", "Retry cancellation result: %s", cancelled ? "true" : "false"); | ||||||
|  |             ESP_LOGI("test", "Cancel retry ran %d times before cancellation", id(cancel_retry_counter)); | ||||||
|  |           }); | ||||||
|  |  | ||||||
|  |       # Test 5: Empty name retry | ||||||
|  |       - logger.log: "=== Test 5: Empty name retry ===" | ||||||
|  |       - lambda: |- | ||||||
|  |           auto *component = id(test_sensor); | ||||||
|  |           App.scheduler.set_retry(component, "", 50, 5, | ||||||
|  |             [](uint8_t retry_countdown) { | ||||||
|  |               id(empty_name_retry_counter)++; | ||||||
|  |               ESP_LOGI("test", "Empty name retry attempt %d", id(empty_name_retry_counter)); | ||||||
|  |               return RetryResult::RETRY; | ||||||
|  |             }); | ||||||
|  |  | ||||||
|  |           // Try to cancel after 75ms | ||||||
|  |           App.scheduler.set_timeout(component, "empty_cancel_timer", 75, []() { | ||||||
|  |             bool cancelled = App.scheduler.cancel_retry(id(test_sensor), ""); | ||||||
|  |             ESP_LOGI("test", "Empty name retry cancel result: %s", | ||||||
|  |                      cancelled ? "true" : "false"); | ||||||
|  |             ESP_LOGI("test", "Empty name retry ran %d times", id(empty_name_retry_counter)); | ||||||
|  |           }); | ||||||
|  |  | ||||||
|  |       # Test 6: Component method | ||||||
|  |       - logger.log: "=== Test 6: Component::set_retry method ===" | ||||||
|  |       - lambda: |- | ||||||
|  |           class TestRetryComponent : public Component { | ||||||
|  |           public: | ||||||
|  |             void test_retry() { | ||||||
|  |               this->set_retry(50, 3, | ||||||
|  |                 [](uint8_t retry_countdown) { | ||||||
|  |                   id(script_retry_counter)++; | ||||||
|  |                   ESP_LOGI("test", "Component retry attempt %d", id(script_retry_counter)); | ||||||
|  |                   if (id(script_retry_counter) >= 2) { | ||||||
|  |                     return RetryResult::DONE; | ||||||
|  |                   } | ||||||
|  |                   return RetryResult::RETRY; | ||||||
|  |                 }, 1.5f); | ||||||
|  |             } | ||||||
|  |           }; | ||||||
|  |  | ||||||
|  |           static TestRetryComponent test_component; | ||||||
|  |           test_component.test_retry(); | ||||||
|  |  | ||||||
|  |       # Test 7: Multiple same name | ||||||
|  |       - logger.log: "=== Test 7: Multiple retries with same name ===" | ||||||
|  |       - lambda: |- | ||||||
|  |           auto *component = id(test_sensor); | ||||||
|  |  | ||||||
|  |           // Set first retry | ||||||
|  |           App.scheduler.set_retry(component, "duplicate_retry", 100, 5, | ||||||
|  |             [](uint8_t retry_countdown) { | ||||||
|  |               id(multiple_same_name_counter) += 1; | ||||||
|  |               ESP_LOGI("test", "First duplicate retry - should not run"); | ||||||
|  |               return RetryResult::RETRY; | ||||||
|  |             }); | ||||||
|  |  | ||||||
|  |           // Set second retry with same name (should cancel first) | ||||||
|  |           App.scheduler.set_retry(component, "duplicate_retry", 50, 3, | ||||||
|  |             [](uint8_t retry_countdown) { | ||||||
|  |               id(multiple_same_name_counter) += 10; | ||||||
|  |               ESP_LOGI("test", "Second duplicate retry attempt (counter=%d)", | ||||||
|  |                        id(multiple_same_name_counter)); | ||||||
|  |               if (id(multiple_same_name_counter) >= 20) { | ||||||
|  |                 return RetryResult::DONE; | ||||||
|  |               } | ||||||
|  |               return RetryResult::RETRY; | ||||||
|  |             }); | ||||||
|  |  | ||||||
|  |       # Wait for all tests to complete before reporting | ||||||
|  |       - delay: 500ms | ||||||
|  |  | ||||||
|  |       # Final report | ||||||
|  |       - logger.log: "=== Retry Test Results ===" | ||||||
|  |       - lambda: |- | ||||||
|  |           ESP_LOGI("test", "Simple retry counter: %d (expected 2)", id(simple_retry_counter)); | ||||||
|  |           ESP_LOGI("test", "Backoff retry counter: %d (expected 4)", id(backoff_retry_counter)); | ||||||
|  |           ESP_LOGI("test", "Immediate done counter: %d (expected 1)", id(immediate_done_counter)); | ||||||
|  |           ESP_LOGI("test", "Cancel retry counter: %d (expected ~3-4)", id(cancel_retry_counter)); | ||||||
|  |           ESP_LOGI("test", "Empty name retry counter: %d (expected 1-2)", id(empty_name_retry_counter)); | ||||||
|  |           ESP_LOGI("test", "Component retry counter: %d (expected 2)", id(script_retry_counter)); | ||||||
|  |           ESP_LOGI("test", "Multiple same name counter: %d (expected 20+)", id(multiple_same_name_counter)); | ||||||
|  |           ESP_LOGI("test", "All retry tests completed"); | ||||||
							
								
								
									
										234
									
								
								tests/integration/test_scheduler_retry_test.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										234
									
								
								tests/integration/test_scheduler_retry_test.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,234 @@ | |||||||
|  | """Test scheduler retry functionality.""" | ||||||
|  |  | ||||||
|  | import asyncio | ||||||
|  | import re | ||||||
|  |  | ||||||
|  | import pytest | ||||||
|  |  | ||||||
|  | from .types import APIClientConnectedFactory, RunCompiledFunction | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.mark.asyncio | ||||||
|  | async def test_scheduler_retry_test( | ||||||
|  |     yaml_config: str, | ||||||
|  |     run_compiled: RunCompiledFunction, | ||||||
|  |     api_client_connected: APIClientConnectedFactory, | ||||||
|  | ) -> None: | ||||||
|  |     """Test that scheduler retry functionality works correctly.""" | ||||||
|  |     # Track test progress | ||||||
|  |     simple_retry_done = asyncio.Event() | ||||||
|  |     backoff_retry_done = asyncio.Event() | ||||||
|  |     immediate_done_done = asyncio.Event() | ||||||
|  |     cancel_retry_done = asyncio.Event() | ||||||
|  |     empty_name_retry_done = asyncio.Event() | ||||||
|  |     component_retry_done = asyncio.Event() | ||||||
|  |     multiple_name_done = asyncio.Event() | ||||||
|  |     test_complete = asyncio.Event() | ||||||
|  |  | ||||||
|  |     # Track retry counts | ||||||
|  |     simple_retry_count = 0 | ||||||
|  |     backoff_retry_count = 0 | ||||||
|  |     immediate_done_count = 0 | ||||||
|  |     cancel_retry_count = 0 | ||||||
|  |     empty_name_retry_count = 0 | ||||||
|  |     component_retry_count = 0 | ||||||
|  |     multiple_name_count = 0 | ||||||
|  |  | ||||||
|  |     # Track specific test results | ||||||
|  |     cancel_result = None | ||||||
|  |     empty_cancel_result = None | ||||||
|  |     backoff_intervals = [] | ||||||
|  |  | ||||||
|  |     def on_log_line(line: str) -> None: | ||||||
|  |         nonlocal simple_retry_count, backoff_retry_count, immediate_done_count | ||||||
|  |         nonlocal cancel_retry_count, empty_name_retry_count, component_retry_count | ||||||
|  |         nonlocal multiple_name_count, cancel_result, empty_cancel_result | ||||||
|  |  | ||||||
|  |         # Strip ANSI color codes | ||||||
|  |         clean_line = re.sub(r"\x1b\[[0-9;]*m", "", line) | ||||||
|  |  | ||||||
|  |         # Simple retry test | ||||||
|  |         if "Simple retry attempt" in clean_line: | ||||||
|  |             if match := re.search(r"Simple retry attempt (\d+)", clean_line): | ||||||
|  |                 simple_retry_count = int(match.group(1)) | ||||||
|  |  | ||||||
|  |         elif "Simple retry succeeded on attempt" in clean_line: | ||||||
|  |             simple_retry_done.set() | ||||||
|  |  | ||||||
|  |         # Backoff retry test | ||||||
|  |         elif "Backoff retry attempt" in clean_line: | ||||||
|  |             if match := re.search( | ||||||
|  |                 r"Backoff retry attempt (\d+).*interval=(\d+)ms", clean_line | ||||||
|  |             ): | ||||||
|  |                 backoff_retry_count = int(match.group(1)) | ||||||
|  |                 interval = int(match.group(2)) | ||||||
|  |                 if backoff_retry_count > 1:  # Skip first (immediate) call | ||||||
|  |                     backoff_intervals.append(interval) | ||||||
|  |  | ||||||
|  |         elif "Backoff retry completed" in clean_line: | ||||||
|  |             backoff_retry_done.set() | ||||||
|  |  | ||||||
|  |         # Immediate done test | ||||||
|  |         elif "Immediate done retry called" in clean_line: | ||||||
|  |             immediate_done_count += 1 | ||||||
|  |             immediate_done_done.set() | ||||||
|  |  | ||||||
|  |         # Cancel retry test | ||||||
|  |         elif "Cancel test retry attempt" in clean_line: | ||||||
|  |             cancel_retry_count += 1 | ||||||
|  |  | ||||||
|  |         elif "Retry cancellation result:" in clean_line: | ||||||
|  |             cancel_result = "true" in clean_line | ||||||
|  |             cancel_retry_done.set() | ||||||
|  |  | ||||||
|  |         # Empty name retry test | ||||||
|  |         elif "Empty name retry attempt" in clean_line: | ||||||
|  |             if match := re.search(r"Empty name retry attempt (\d+)", clean_line): | ||||||
|  |                 empty_name_retry_count = int(match.group(1)) | ||||||
|  |  | ||||||
|  |         elif "Empty name retry cancel result:" in clean_line: | ||||||
|  |             empty_cancel_result = "true" in clean_line | ||||||
|  |  | ||||||
|  |         elif "Empty name retry ran" in clean_line: | ||||||
|  |             empty_name_retry_done.set() | ||||||
|  |  | ||||||
|  |         # Component retry test | ||||||
|  |         elif "Component retry attempt" in clean_line: | ||||||
|  |             if match := re.search(r"Component retry attempt (\d+)", clean_line): | ||||||
|  |                 component_retry_count = int(match.group(1)) | ||||||
|  |                 if component_retry_count >= 2: | ||||||
|  |                     component_retry_done.set() | ||||||
|  |  | ||||||
|  |         # Multiple same name test | ||||||
|  |         elif "Second duplicate retry attempt" in clean_line: | ||||||
|  |             if match := re.search(r"counter=(\d+)", clean_line): | ||||||
|  |                 multiple_name_count = int(match.group(1)) | ||||||
|  |                 if multiple_name_count >= 20: | ||||||
|  |                     multiple_name_done.set() | ||||||
|  |  | ||||||
|  |         # Test completion | ||||||
|  |         elif "All retry tests completed" in clean_line: | ||||||
|  |             test_complete.set() | ||||||
|  |  | ||||||
|  |     async with ( | ||||||
|  |         run_compiled(yaml_config, line_callback=on_log_line), | ||||||
|  |         api_client_connected() as client, | ||||||
|  |     ): | ||||||
|  |         # Verify we can connect | ||||||
|  |         device_info = await client.device_info() | ||||||
|  |         assert device_info is not None | ||||||
|  |         assert device_info.name == "scheduler-retry-test" | ||||||
|  |  | ||||||
|  |         # Wait for simple retry test | ||||||
|  |         try: | ||||||
|  |             await asyncio.wait_for(simple_retry_done.wait(), timeout=1.0) | ||||||
|  |         except TimeoutError: | ||||||
|  |             pytest.fail( | ||||||
|  |                 f"Simple retry test did not complete. Count: {simple_retry_count}" | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |         assert simple_retry_count == 2, ( | ||||||
|  |             f"Expected 2 simple retry attempts, got {simple_retry_count}" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Wait for backoff retry test | ||||||
|  |         try: | ||||||
|  |             await asyncio.wait_for(backoff_retry_done.wait(), timeout=3.0) | ||||||
|  |         except TimeoutError: | ||||||
|  |             pytest.fail( | ||||||
|  |                 f"Backoff retry test did not complete. Count: {backoff_retry_count}" | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |         assert backoff_retry_count == 4, ( | ||||||
|  |             f"Expected 4 backoff retry attempts, got {backoff_retry_count}" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Verify backoff intervals (allowing for timing variations) | ||||||
|  |         assert len(backoff_intervals) >= 2, ( | ||||||
|  |             f"Expected at least 2 intervals, got {len(backoff_intervals)}" | ||||||
|  |         ) | ||||||
|  |         if len(backoff_intervals) >= 3: | ||||||
|  |             # First interval should be ~50ms | ||||||
|  |             assert 30 <= backoff_intervals[0] <= 70, ( | ||||||
|  |                 f"First interval {backoff_intervals[0]}ms not ~50ms" | ||||||
|  |             ) | ||||||
|  |             # Second interval should be ~100ms (50ms * 2.0) | ||||||
|  |             assert 80 <= backoff_intervals[1] <= 120, ( | ||||||
|  |                 f"Second interval {backoff_intervals[1]}ms not ~100ms" | ||||||
|  |             ) | ||||||
|  |             # Third interval should be ~200ms (100ms * 2.0) | ||||||
|  |             assert 180 <= backoff_intervals[2] <= 220, ( | ||||||
|  |                 f"Third interval {backoff_intervals[2]}ms not ~200ms" | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |         # Wait for immediate done test | ||||||
|  |         try: | ||||||
|  |             await asyncio.wait_for(immediate_done_done.wait(), timeout=3.0) | ||||||
|  |         except TimeoutError: | ||||||
|  |             pytest.fail( | ||||||
|  |                 f"Immediate done test did not complete. Count: {immediate_done_count}" | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |         assert immediate_done_count == 1, ( | ||||||
|  |             f"Expected 1 immediate done call, got {immediate_done_count}" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Wait for cancel retry test | ||||||
|  |         try: | ||||||
|  |             await asyncio.wait_for(cancel_retry_done.wait(), timeout=2.0) | ||||||
|  |         except TimeoutError: | ||||||
|  |             pytest.fail( | ||||||
|  |                 f"Cancel retry test did not complete. Count: {cancel_retry_count}" | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |         assert cancel_result is True, "Retry cancellation should have succeeded" | ||||||
|  |         assert 2 <= cancel_retry_count <= 5, ( | ||||||
|  |             f"Expected 2-5 cancel retry attempts before cancellation, got {cancel_retry_count}" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Wait for empty name retry test | ||||||
|  |         try: | ||||||
|  |             await asyncio.wait_for(empty_name_retry_done.wait(), timeout=1.0) | ||||||
|  |         except TimeoutError: | ||||||
|  |             pytest.fail( | ||||||
|  |                 f"Empty name retry test did not complete. Count: {empty_name_retry_count}" | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |         # Empty name retry should run at least once before being cancelled | ||||||
|  |         assert 1 <= empty_name_retry_count <= 2, ( | ||||||
|  |             f"Expected 1-2 empty name retry attempts, got {empty_name_retry_count}" | ||||||
|  |         ) | ||||||
|  |         assert empty_cancel_result is True, ( | ||||||
|  |             "Empty name retry cancel should have succeeded" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Wait for component retry test | ||||||
|  |         try: | ||||||
|  |             await asyncio.wait_for(component_retry_done.wait(), timeout=1.0) | ||||||
|  |         except TimeoutError: | ||||||
|  |             pytest.fail( | ||||||
|  |                 f"Component retry test did not complete. Count: {component_retry_count}" | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |         assert component_retry_count >= 2, ( | ||||||
|  |             f"Expected at least 2 component retry attempts, got {component_retry_count}" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Wait for multiple same name test | ||||||
|  |         try: | ||||||
|  |             await asyncio.wait_for(multiple_name_done.wait(), timeout=1.0) | ||||||
|  |         except TimeoutError: | ||||||
|  |             pytest.fail( | ||||||
|  |                 f"Multiple same name test did not complete. Count: {multiple_name_count}" | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |         # Should be 20+ (only second retry should run) | ||||||
|  |         assert multiple_name_count >= 20, ( | ||||||
|  |             f"Expected multiple name count >= 20 (second retry only), got {multiple_name_count}" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Wait for test completion | ||||||
|  |         try: | ||||||
|  |             await asyncio.wait_for(test_complete.wait(), timeout=1.0) | ||||||
|  |         except TimeoutError: | ||||||
|  |             pytest.fail("Test did not complete within timeout") | ||||||
		Reference in New Issue
	
	Block a user