mirror of
https://github.com/esphome/esphome.git
synced 2026-05-30 23:54:04 +08:00
Merge branch 'dev' into revert-9439-lib_compat_mode_fix
This commit is contained in:
+1
-1
@@ -1 +1 @@
|
|||||||
90bb12a42dfe2a13378fb292fd67a5fd503b689bcec2be034be366758a5f69c6
|
4df2fc55e977ba821978fac5f1e721ce2338e23647050b7005b4c801b1770739
|
||||||
|
|||||||
+386
-216
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,324 @@
|
|||||||
|
# This workflow automatically requests reviews from codeowners when:
|
||||||
|
# 1. A PR is opened, reopened, or synchronized (updated)
|
||||||
|
# 2. A PR is marked as ready for review
|
||||||
|
#
|
||||||
|
# It reads the CODEOWNERS file and matches all changed files in the PR against
|
||||||
|
# the codeowner patterns, then requests reviews from the appropriate owners
|
||||||
|
# while avoiding duplicate requests for users who have already been requested
|
||||||
|
# or have already reviewed the PR.
|
||||||
|
|
||||||
|
name: Request Codeowner Reviews
|
||||||
|
|
||||||
|
on:
|
||||||
|
# Needs to be pull_request_target to get write permissions
|
||||||
|
pull_request_target:
|
||||||
|
types: [opened, reopened, synchronize, ready_for_review]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
request-codeowner-reviews:
|
||||||
|
name: Run
|
||||||
|
if: ${{ !github.event.pull_request.draft }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Request reviews from component codeowners
|
||||||
|
uses: actions/github-script@v7.0.1
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const owner = context.repo.owner;
|
||||||
|
const repo = context.repo.repo;
|
||||||
|
const pr_number = context.payload.pull_request.number;
|
||||||
|
|
||||||
|
console.log(`Processing PR #${pr_number} for codeowner review requests`);
|
||||||
|
|
||||||
|
// Hidden marker to identify bot comments from this workflow
|
||||||
|
const BOT_COMMENT_MARKER = '<!-- codeowner-review-request-bot -->';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get the list of changed files in this PR
|
||||||
|
const { data: files } = await github.rest.pulls.listFiles({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
pull_number: pr_number
|
||||||
|
});
|
||||||
|
|
||||||
|
const changedFiles = files.map(file => file.filename);
|
||||||
|
console.log(`Found ${changedFiles.length} changed files`);
|
||||||
|
|
||||||
|
if (changedFiles.length === 0) {
|
||||||
|
console.log('No changed files found, skipping codeowner review requests');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch CODEOWNERS file from root
|
||||||
|
const { data: codeownersFile } = await github.rest.repos.getContent({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
path: 'CODEOWNERS',
|
||||||
|
ref: context.payload.pull_request.base.sha
|
||||||
|
});
|
||||||
|
const codeownersContent = Buffer.from(codeownersFile.content, 'base64').toString('utf8');
|
||||||
|
|
||||||
|
// Parse CODEOWNERS file to extract all patterns and their owners
|
||||||
|
const codeownersLines = codeownersContent.split('\n')
|
||||||
|
.map(line => line.trim())
|
||||||
|
.filter(line => line && !line.startsWith('#'));
|
||||||
|
|
||||||
|
const codeownersPatterns = [];
|
||||||
|
|
||||||
|
// Convert CODEOWNERS pattern to regex (robust glob handling)
|
||||||
|
function globToRegex(pattern) {
|
||||||
|
// Escape regex special characters except for glob wildcards
|
||||||
|
let regexStr = pattern
|
||||||
|
.replace(/([.+^=!:${}()|[\]\\])/g, '\\$1') // escape regex chars
|
||||||
|
.replace(/\*\*/g, '.*') // globstar
|
||||||
|
.replace(/\*/g, '[^/]*') // single star
|
||||||
|
.replace(/\?/g, '.'); // question mark
|
||||||
|
return new RegExp('^' + regexStr + '$');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to create comment body
|
||||||
|
function createCommentBody(reviewersList, teamsList, matchedFileCount, isSuccessful = true) {
|
||||||
|
const reviewerMentions = reviewersList.map(r => `@${r}`);
|
||||||
|
const teamMentions = teamsList.map(t => `@${owner}/${t}`);
|
||||||
|
const allMentions = [...reviewerMentions, ...teamMentions].join(', ');
|
||||||
|
|
||||||
|
if (isSuccessful) {
|
||||||
|
return `${BOT_COMMENT_MARKER}\n👋 Hi there! I've automatically requested reviews from codeowners based on the files changed in this PR.\n\n${allMentions} - You've been requested to review this PR as codeowner(s) of ${matchedFileCount} file(s) that were modified. Thanks for your time! 🙏`;
|
||||||
|
} else {
|
||||||
|
return `${BOT_COMMENT_MARKER}\n👋 Hi there! This PR modifies ${matchedFileCount} file(s) with codeowners.\n\n${allMentions} - As codeowner(s) of the affected files, your review would be appreciated! 🙏\n\n_Note: Automatic review request may have failed, but you're still welcome to review._`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const line of codeownersLines) {
|
||||||
|
const parts = line.split(/\s+/);
|
||||||
|
if (parts.length < 2) continue;
|
||||||
|
|
||||||
|
const pattern = parts[0];
|
||||||
|
const owners = parts.slice(1);
|
||||||
|
|
||||||
|
// Use robust glob-to-regex conversion
|
||||||
|
const regex = globToRegex(pattern);
|
||||||
|
codeownersPatterns.push({ pattern, regex, owners });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Parsed ${codeownersPatterns.length} codeowner patterns`);
|
||||||
|
|
||||||
|
// Match changed files against CODEOWNERS patterns
|
||||||
|
const matchedOwners = new Set();
|
||||||
|
const matchedTeams = new Set();
|
||||||
|
const fileMatches = new Map(); // Track which files matched which patterns
|
||||||
|
|
||||||
|
for (const file of changedFiles) {
|
||||||
|
for (const { pattern, regex, owners } of codeownersPatterns) {
|
||||||
|
if (regex.test(file)) {
|
||||||
|
console.log(`File '${file}' matches pattern '${pattern}' with owners: ${owners.join(', ')}`);
|
||||||
|
|
||||||
|
if (!fileMatches.has(file)) {
|
||||||
|
fileMatches.set(file, []);
|
||||||
|
}
|
||||||
|
fileMatches.get(file).push({ pattern, owners });
|
||||||
|
|
||||||
|
// Add owners to the appropriate set (remove @ prefix)
|
||||||
|
for (const owner of owners) {
|
||||||
|
const cleanOwner = owner.startsWith('@') ? owner.slice(1) : owner;
|
||||||
|
if (cleanOwner.includes('/')) {
|
||||||
|
// Team mention (org/team-name)
|
||||||
|
const teamName = cleanOwner.split('/')[1];
|
||||||
|
matchedTeams.add(teamName);
|
||||||
|
} else {
|
||||||
|
// Individual user
|
||||||
|
matchedOwners.add(cleanOwner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchedOwners.size === 0 && matchedTeams.size === 0) {
|
||||||
|
console.log('No codeowners found for any changed files');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the PR author from reviewers
|
||||||
|
const prAuthor = context.payload.pull_request.user.login;
|
||||||
|
matchedOwners.delete(prAuthor);
|
||||||
|
|
||||||
|
// Get current reviewers to avoid duplicate requests (but still mention them)
|
||||||
|
const { data: prData } = await github.rest.pulls.get({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
pull_number: pr_number
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentReviewers = new Set();
|
||||||
|
const currentTeams = new Set();
|
||||||
|
|
||||||
|
if (prData.requested_reviewers) {
|
||||||
|
prData.requested_reviewers.forEach(reviewer => {
|
||||||
|
currentReviewers.add(reviewer.login);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prData.requested_teams) {
|
||||||
|
prData.requested_teams.forEach(team => {
|
||||||
|
currentTeams.add(team.slug);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for completed reviews to avoid re-requesting users who have already reviewed
|
||||||
|
const { data: reviews } = await github.rest.pulls.listReviews({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
pull_number: pr_number
|
||||||
|
});
|
||||||
|
|
||||||
|
const reviewedUsers = new Set();
|
||||||
|
reviews.forEach(review => {
|
||||||
|
reviewedUsers.add(review.user.login);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check for previous comments from this workflow to avoid duplicate pings
|
||||||
|
const comments = await github.paginate(
|
||||||
|
github.rest.issues.listComments,
|
||||||
|
{
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issue_number: pr_number
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const previouslyPingedUsers = new Set();
|
||||||
|
const previouslyPingedTeams = new Set();
|
||||||
|
|
||||||
|
// Look for comments from github-actions bot that contain our bot marker
|
||||||
|
const workflowComments = comments.filter(comment =>
|
||||||
|
comment.user.type === 'Bot' &&
|
||||||
|
comment.body.includes(BOT_COMMENT_MARKER)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Extract previously mentioned users and teams from workflow comments
|
||||||
|
for (const comment of workflowComments) {
|
||||||
|
// Match @username patterns (not team mentions)
|
||||||
|
const userMentions = comment.body.match(/@([a-zA-Z0-9_.-]+)(?![/])/g) || [];
|
||||||
|
userMentions.forEach(mention => {
|
||||||
|
const username = mention.slice(1); // remove @
|
||||||
|
previouslyPingedUsers.add(username);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Match @org/team patterns
|
||||||
|
const teamMentions = comment.body.match(/@[a-zA-Z0-9_.-]+\/([a-zA-Z0-9_.-]+)/g) || [];
|
||||||
|
teamMentions.forEach(mention => {
|
||||||
|
const teamName = mention.split('/')[1];
|
||||||
|
previouslyPingedTeams.add(teamName);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Found ${previouslyPingedUsers.size} previously pinged users and ${previouslyPingedTeams.size} previously pinged teams`);
|
||||||
|
|
||||||
|
// Remove users who have already been pinged in previous workflow comments
|
||||||
|
previouslyPingedUsers.forEach(user => {
|
||||||
|
matchedOwners.delete(user);
|
||||||
|
});
|
||||||
|
|
||||||
|
previouslyPingedTeams.forEach(team => {
|
||||||
|
matchedTeams.delete(team);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove only users who have already submitted reviews (not just requested reviewers)
|
||||||
|
reviewedUsers.forEach(reviewer => {
|
||||||
|
matchedOwners.delete(reviewer);
|
||||||
|
});
|
||||||
|
|
||||||
|
// For teams, we'll still remove already requested teams to avoid API errors
|
||||||
|
currentTeams.forEach(team => {
|
||||||
|
matchedTeams.delete(team);
|
||||||
|
});
|
||||||
|
|
||||||
|
const reviewersList = Array.from(matchedOwners);
|
||||||
|
const teamsList = Array.from(matchedTeams);
|
||||||
|
|
||||||
|
if (reviewersList.length === 0 && teamsList.length === 0) {
|
||||||
|
console.log('No eligible reviewers found (all may already be requested, reviewed, or previously pinged)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalReviewers = reviewersList.length + teamsList.length;
|
||||||
|
console.log(`Requesting reviews from ${reviewersList.length} users and ${teamsList.length} teams for ${fileMatches.size} matched files`);
|
||||||
|
|
||||||
|
// Request reviews
|
||||||
|
try {
|
||||||
|
const requestParams = {
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
pull_number: pr_number
|
||||||
|
};
|
||||||
|
|
||||||
|
// Filter out users who are already requested reviewers for the API call
|
||||||
|
const newReviewers = reviewersList.filter(reviewer => !currentReviewers.has(reviewer));
|
||||||
|
const newTeams = teamsList.filter(team => !currentTeams.has(team));
|
||||||
|
|
||||||
|
if (newReviewers.length > 0) {
|
||||||
|
requestParams.reviewers = newReviewers;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newTeams.length > 0) {
|
||||||
|
requestParams.team_reviewers = newTeams;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only make the API call if there are new reviewers to request
|
||||||
|
if (newReviewers.length > 0 || newTeams.length > 0) {
|
||||||
|
await github.rest.pulls.requestReviewers(requestParams);
|
||||||
|
console.log(`Successfully requested reviews from ${newReviewers.length} new users and ${newTeams.length} new teams`);
|
||||||
|
} else {
|
||||||
|
console.log('All codeowners are already requested reviewers or have reviewed');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only add a comment if there are new codeowners to mention (not previously pinged)
|
||||||
|
if (reviewersList.length > 0 || teamsList.length > 0) {
|
||||||
|
const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, true);
|
||||||
|
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issue_number: pr_number,
|
||||||
|
body: commentBody
|
||||||
|
});
|
||||||
|
console.log(`Added comment mentioning ${reviewersList.length} users and ${teamsList.length} teams`);
|
||||||
|
} else {
|
||||||
|
console.log('No new codeowners to mention in comment (all previously pinged)');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error.status === 422) {
|
||||||
|
console.log('Some reviewers may already be requested or unavailable:', error.message);
|
||||||
|
|
||||||
|
// Only try to add a comment if there are new codeowners to mention
|
||||||
|
if (reviewersList.length > 0 || teamsList.length > 0) {
|
||||||
|
const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, false);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issue_number: pr_number,
|
||||||
|
body: commentBody
|
||||||
|
});
|
||||||
|
console.log(`Added fallback comment mentioning ${reviewersList.length} users and ${teamsList.length} teams`);
|
||||||
|
} catch (commentError) {
|
||||||
|
console.log('Failed to add comment:', commentError.message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('No new codeowners to mention in fallback comment');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Failed to process codeowner review requests:', error.message);
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
@@ -61,7 +61,8 @@ jobs:
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function createComment(octokit, owner, repo, prNumber, esphomeChanges, componentChanges) {
|
async function createComment(octokit, owner, repo, prNumber, esphomeChanges, componentChanges) {
|
||||||
const commentMarker = "<!-- This comment was generated automatically by a GitHub workflow. -->";
|
const commentMarker = "<!-- This comment was generated automatically by the external-component-bot workflow. -->";
|
||||||
|
const legacyCommentMarker = "<!-- This comment was generated automatically by a GitHub workflow. -->";
|
||||||
let commentBody;
|
let commentBody;
|
||||||
if (esphomeChanges.length === 1) {
|
if (esphomeChanges.length === 1) {
|
||||||
commentBody = generateExternalComponentInstructions(prNumber, componentChanges, owner, repo);
|
commentBody = generateExternalComponentInstructions(prNumber, componentChanges, owner, repo);
|
||||||
@@ -71,14 +72,23 @@ jobs:
|
|||||||
commentBody += `\n\n---\n(Added by the PR bot)\n\n${commentMarker}`;
|
commentBody += `\n\n---\n(Added by the PR bot)\n\n${commentMarker}`;
|
||||||
|
|
||||||
// Check for existing bot comment
|
// Check for existing bot comment
|
||||||
const comments = await github.rest.issues.listComments({
|
const comments = await github.paginate(
|
||||||
|
github.rest.issues.listComments,
|
||||||
|
{
|
||||||
owner: owner,
|
owner: owner,
|
||||||
repo: repo,
|
repo: repo,
|
||||||
issue_number: prNumber,
|
issue_number: prNumber,
|
||||||
});
|
per_page: 100,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const botComment = comments.data.find(comment =>
|
const sorted = comments.sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at));
|
||||||
comment.body.includes(commentMarker)
|
|
||||||
|
const botComment = sorted.find(comment =>
|
||||||
|
(
|
||||||
|
comment.body.includes(commentMarker) ||
|
||||||
|
comment.body.includes(legacyCommentMarker)
|
||||||
|
) && comment.user.type === "Bot"
|
||||||
);
|
);
|
||||||
|
|
||||||
if (botComment && botComment.body === commentBody) {
|
if (botComment && botComment.body === commentBody) {
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
# This workflow automatically notifies codeowners when an issue is labeled with component labels.
|
||||||
|
# It reads the CODEOWNERS file to find the maintainers for the labeled components
|
||||||
|
# and posts a comment mentioning them to ensure they're aware of the issue.
|
||||||
|
|
||||||
|
name: Notify Issue Codeowners
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [labeled]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
notify-codeowners:
|
||||||
|
name: Run
|
||||||
|
if: ${{ startsWith(github.event.label.name, format('component{0} ', ':')) }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Notify codeowners for component issues
|
||||||
|
uses: actions/github-script@v7.0.1
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const owner = context.repo.owner;
|
||||||
|
const repo = context.repo.repo;
|
||||||
|
const issue_number = context.payload.issue.number;
|
||||||
|
const labelName = context.payload.label.name;
|
||||||
|
|
||||||
|
console.log(`Processing issue #${issue_number} with label: ${labelName}`);
|
||||||
|
|
||||||
|
// Hidden marker to identify bot comments from this workflow
|
||||||
|
const BOT_COMMENT_MARKER = '<!-- issue-codeowner-notify-bot -->';
|
||||||
|
|
||||||
|
// Extract component name from label
|
||||||
|
const componentName = labelName.replace('component: ', '');
|
||||||
|
console.log(`Component: ${componentName}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fetch CODEOWNERS file from root
|
||||||
|
const { data: codeownersFile } = await github.rest.repos.getContent({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
path: 'CODEOWNERS'
|
||||||
|
});
|
||||||
|
const codeownersContent = Buffer.from(codeownersFile.content, 'base64').toString('utf8');
|
||||||
|
|
||||||
|
// Parse CODEOWNERS file to extract component mappings
|
||||||
|
const codeownersLines = codeownersContent.split('\n')
|
||||||
|
.map(line => line.trim())
|
||||||
|
.filter(line => line && !line.startsWith('#'));
|
||||||
|
|
||||||
|
let componentOwners = null;
|
||||||
|
|
||||||
|
for (const line of codeownersLines) {
|
||||||
|
const parts = line.split(/\s+/);
|
||||||
|
if (parts.length < 2) continue;
|
||||||
|
|
||||||
|
const pattern = parts[0];
|
||||||
|
const owners = parts.slice(1);
|
||||||
|
|
||||||
|
// Look for component patterns: esphome/components/{component}/*
|
||||||
|
const componentMatch = pattern.match(/^esphome\/components\/([^\/]+)\/\*$/);
|
||||||
|
if (componentMatch && componentMatch[1] === componentName) {
|
||||||
|
componentOwners = owners;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!componentOwners) {
|
||||||
|
console.log(`No codeowners found for component: ${componentName}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Found codeowners for '${componentName}': ${componentOwners.join(', ')}`);
|
||||||
|
|
||||||
|
// Separate users and teams
|
||||||
|
const userOwners = [];
|
||||||
|
const teamOwners = [];
|
||||||
|
|
||||||
|
for (const owner of componentOwners) {
|
||||||
|
const cleanOwner = owner.startsWith('@') ? owner.slice(1) : owner;
|
||||||
|
if (cleanOwner.includes('/')) {
|
||||||
|
// Team mention (org/team-name)
|
||||||
|
teamOwners.push(`@${cleanOwner}`);
|
||||||
|
} else {
|
||||||
|
// Individual user
|
||||||
|
userOwners.push(`@${cleanOwner}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove issue author from mentions to avoid self-notification
|
||||||
|
const issueAuthor = context.payload.issue.user.login;
|
||||||
|
const filteredUserOwners = userOwners.filter(mention =>
|
||||||
|
mention !== `@${issueAuthor}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check for previous comments from this workflow to avoid duplicate pings
|
||||||
|
const comments = await github.paginate(
|
||||||
|
github.rest.issues.listComments,
|
||||||
|
{
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issue_number: issue_number
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const previouslyPingedUsers = new Set();
|
||||||
|
const previouslyPingedTeams = new Set();
|
||||||
|
|
||||||
|
// Look for comments from github-actions bot that contain codeowner pings for this component
|
||||||
|
const workflowComments = comments.filter(comment =>
|
||||||
|
comment.user.type === 'Bot' &&
|
||||||
|
comment.body.includes(BOT_COMMENT_MARKER) &&
|
||||||
|
comment.body.includes(`component: ${componentName}`)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Extract previously mentioned users and teams from workflow comments
|
||||||
|
for (const comment of workflowComments) {
|
||||||
|
// Match @username patterns (not team mentions)
|
||||||
|
const userMentions = comment.body.match(/@([a-zA-Z0-9_.-]+)(?![/])/g) || [];
|
||||||
|
userMentions.forEach(mention => {
|
||||||
|
previouslyPingedUsers.add(mention); // Keep @ prefix for easy comparison
|
||||||
|
});
|
||||||
|
|
||||||
|
// Match @org/team patterns
|
||||||
|
const teamMentions = comment.body.match(/@[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+/g) || [];
|
||||||
|
teamMentions.forEach(mention => {
|
||||||
|
previouslyPingedTeams.add(mention);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Found ${previouslyPingedUsers.size} previously pinged users and ${previouslyPingedTeams.size} previously pinged teams for component ${componentName}`);
|
||||||
|
|
||||||
|
// Remove previously pinged users and teams
|
||||||
|
const newUserOwners = filteredUserOwners.filter(mention => !previouslyPingedUsers.has(mention));
|
||||||
|
const newTeamOwners = teamOwners.filter(mention => !previouslyPingedTeams.has(mention));
|
||||||
|
|
||||||
|
const allMentions = [...newUserOwners, ...newTeamOwners];
|
||||||
|
|
||||||
|
if (allMentions.length === 0) {
|
||||||
|
console.log('No new codeowners to notify (all previously pinged or issue author is the only codeowner)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create comment body
|
||||||
|
const mentionString = allMentions.join(', ');
|
||||||
|
const commentBody = `${BOT_COMMENT_MARKER}\n👋 Hey ${mentionString}!\n\nThis issue has been labeled with \`component: ${componentName}\` and you've been identified as a codeowner of this component. Please take a look when you have a chance!\n\nThanks for maintaining this component! 🙏`;
|
||||||
|
|
||||||
|
// Post comment
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issue_number: issue_number,
|
||||||
|
body: commentBody
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Successfully notified new codeowners: ${mentionString}`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Failed to process codeowner notifications:', error.message);
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ ci:
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
# Ruff version.
|
# Ruff version.
|
||||||
rev: v0.12.4
|
rev: v0.12.5
|
||||||
hooks:
|
hooks:
|
||||||
# Run the linter.
|
# Run the linter.
|
||||||
- id: ruff
|
- id: ruff
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
pyproject.toml @esphome/core
|
pyproject.toml @esphome/core
|
||||||
esphome/*.py @esphome/core
|
esphome/*.py @esphome/core
|
||||||
esphome/core/* @esphome/core
|
esphome/core/* @esphome/core
|
||||||
|
.github/** @esphome/core
|
||||||
|
|
||||||
# Integrations
|
# Integrations
|
||||||
esphome/components/a01nyub/* @MrSuicideParrot
|
esphome/components/a01nyub/* @MrSuicideParrot
|
||||||
@@ -245,6 +246,7 @@ esphome/components/lcd_menu/* @numo68
|
|||||||
esphome/components/ld2410/* @regevbr @sebcaps
|
esphome/components/ld2410/* @regevbr @sebcaps
|
||||||
esphome/components/ld2420/* @descipher
|
esphome/components/ld2420/* @descipher
|
||||||
esphome/components/ld2450/* @hareeshmu
|
esphome/components/ld2450/* @hareeshmu
|
||||||
|
esphome/components/ld24xx/* @kbx81
|
||||||
esphome/components/ledc/* @OttoWinter
|
esphome/components/ledc/* @OttoWinter
|
||||||
esphome/components/libretiny/* @kuba2k2
|
esphome/components/libretiny/* @kuba2k2
|
||||||
esphome/components/libretiny_pwm/* @kuba2k2
|
esphome/components/libretiny_pwm/* @kuba2k2
|
||||||
@@ -292,6 +294,7 @@ esphome/components/microphone/* @jesserockz @kahrendt
|
|||||||
esphome/components/mics_4514/* @jesserockz
|
esphome/components/mics_4514/* @jesserockz
|
||||||
esphome/components/midea/* @dudanov
|
esphome/components/midea/* @dudanov
|
||||||
esphome/components/midea_ir/* @dudanov
|
esphome/components/midea_ir/* @dudanov
|
||||||
|
esphome/components/mipi_dsi/* @clydebarrow
|
||||||
esphome/components/mipi_spi/* @clydebarrow
|
esphome/components/mipi_spi/* @clydebarrow
|
||||||
esphome/components/mitsubishi/* @RubyBailey
|
esphome/components/mitsubishi/* @RubyBailey
|
||||||
esphome/components/mixer/speaker/* @kahrendt
|
esphome/components/mixer/speaker/* @kahrendt
|
||||||
|
|||||||
+15
-9
@@ -2,6 +2,7 @@
|
|||||||
import argparse
|
import argparse
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import functools
|
import functools
|
||||||
|
import getpass
|
||||||
import importlib
|
import importlib
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
@@ -34,6 +35,7 @@ from esphome.const import (
|
|||||||
CONF_PORT,
|
CONF_PORT,
|
||||||
CONF_SUBSTITUTIONS,
|
CONF_SUBSTITUTIONS,
|
||||||
CONF_TOPIC,
|
CONF_TOPIC,
|
||||||
|
ENV_NOGITIGNORE,
|
||||||
PLATFORM_ESP32,
|
PLATFORM_ESP32,
|
||||||
PLATFORM_ESP8266,
|
PLATFORM_ESP8266,
|
||||||
PLATFORM_RP2040,
|
PLATFORM_RP2040,
|
||||||
@@ -88,9 +90,9 @@ def choose_prompt(options, purpose: str = None):
|
|||||||
def choose_upload_log_host(
|
def choose_upload_log_host(
|
||||||
default, check_default, show_ota, show_mqtt, show_api, purpose: str = None
|
default, check_default, show_ota, show_mqtt, show_api, purpose: str = None
|
||||||
):
|
):
|
||||||
options = []
|
options = [
|
||||||
for port in get_serial_ports():
|
(f"{port.path} ({port.description})", port.path) for port in get_serial_ports()
|
||||||
options.append((f"{port.path} ({port.description})", port.path))
|
]
|
||||||
if default == "SERIAL":
|
if default == "SERIAL":
|
||||||
return choose_prompt(options, purpose=purpose)
|
return choose_prompt(options, purpose=purpose)
|
||||||
if (show_ota and "ota" in CORE.config) or (show_api and "api" in CORE.config):
|
if (show_ota and "ota" in CORE.config) or (show_api and "api" in CORE.config):
|
||||||
@@ -118,9 +120,7 @@ def mqtt_logging_enabled(mqtt_config):
|
|||||||
return False
|
return False
|
||||||
if CONF_TOPIC not in log_topic:
|
if CONF_TOPIC not in log_topic:
|
||||||
return False
|
return False
|
||||||
if log_topic.get(CONF_LEVEL, None) == "NONE":
|
return log_topic.get(CONF_LEVEL, None) != "NONE"
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def get_port_type(port):
|
def get_port_type(port):
|
||||||
@@ -209,6 +209,9 @@ def wrap_to_code(name, comp):
|
|||||||
|
|
||||||
|
|
||||||
def write_cpp(config):
|
def write_cpp(config):
|
||||||
|
if not get_bool_env(ENV_NOGITIGNORE):
|
||||||
|
writer.write_gitignore()
|
||||||
|
|
||||||
generate_cpp_contents(config)
|
generate_cpp_contents(config)
|
||||||
return write_cpp_file()
|
return write_cpp_file()
|
||||||
|
|
||||||
@@ -225,10 +228,13 @@ def generate_cpp_contents(config):
|
|||||||
|
|
||||||
|
|
||||||
def write_cpp_file():
|
def write_cpp_file():
|
||||||
writer.write_platformio_project()
|
|
||||||
|
|
||||||
code_s = indent(CORE.cpp_main_section)
|
code_s = indent(CORE.cpp_main_section)
|
||||||
writer.write_cpp(code_s)
|
writer.write_cpp(code_s)
|
||||||
|
|
||||||
|
from esphome.build_gen import platformio
|
||||||
|
|
||||||
|
platformio.write_project()
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
@@ -330,7 +336,7 @@ def check_permissions(port):
|
|||||||
raise EsphomeError(
|
raise EsphomeError(
|
||||||
"You do not have read or write permission on the selected serial port. "
|
"You do not have read or write permission on the selected serial port. "
|
||||||
"To resolve this issue, you can add your user to the dialout group "
|
"To resolve this issue, you can add your user to the dialout group "
|
||||||
f"by running the following command: sudo usermod -a -G dialout {os.getlogin()}. "
|
f"by running the following command: sudo usermod -a -G dialout {getpass.getuser()}. "
|
||||||
"You will need to log out & back in or reboot to activate the new group access."
|
"You will need to log out & back in or reboot to activate the new group access."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from esphome.const import __version__
|
||||||
|
from esphome.core import CORE
|
||||||
|
from esphome.helpers import mkdir_p, read_file, write_file_if_changed
|
||||||
|
from esphome.writer import find_begin_end, update_storage_json
|
||||||
|
|
||||||
|
INI_AUTO_GENERATE_BEGIN = "; ========== AUTO GENERATED CODE BEGIN ==========="
|
||||||
|
INI_AUTO_GENERATE_END = "; =========== AUTO GENERATED CODE END ============"
|
||||||
|
|
||||||
|
INI_BASE_FORMAT = (
|
||||||
|
"""; Auto generated code by esphome
|
||||||
|
|
||||||
|
[common]
|
||||||
|
lib_deps =
|
||||||
|
build_flags =
|
||||||
|
upload_flags =
|
||||||
|
|
||||||
|
""",
|
||||||
|
"""
|
||||||
|
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def format_ini(data: dict[str, str | list[str]]) -> str:
|
||||||
|
content = ""
|
||||||
|
for key, value in sorted(data.items()):
|
||||||
|
if isinstance(value, list):
|
||||||
|
content += f"{key} =\n"
|
||||||
|
for x in value:
|
||||||
|
content += f" {x}\n"
|
||||||
|
else:
|
||||||
|
content += f"{key} = {value}\n"
|
||||||
|
return content
|
||||||
|
|
||||||
|
|
||||||
|
def get_ini_content():
|
||||||
|
CORE.add_platformio_option(
|
||||||
|
"lib_deps",
|
||||||
|
[x.as_lib_dep for x in CORE.platformio_libraries.values()]
|
||||||
|
+ ["${common.lib_deps}"],
|
||||||
|
)
|
||||||
|
# Sort to avoid changing build flags order
|
||||||
|
CORE.add_platformio_option("build_flags", sorted(CORE.build_flags))
|
||||||
|
|
||||||
|
# Sort to avoid changing build unflags order
|
||||||
|
CORE.add_platformio_option("build_unflags", sorted(CORE.build_unflags))
|
||||||
|
|
||||||
|
# Add extra script for C++ flags
|
||||||
|
CORE.add_platformio_option("extra_scripts", [f"pre:{CXX_FLAGS_FILE_NAME}"])
|
||||||
|
|
||||||
|
content = "[platformio]\n"
|
||||||
|
content += f"description = ESPHome {__version__}\n"
|
||||||
|
|
||||||
|
content += f"[env:{CORE.name}]\n"
|
||||||
|
content += format_ini(CORE.platformio_options)
|
||||||
|
|
||||||
|
return content
|
||||||
|
|
||||||
|
|
||||||
|
def write_ini(content):
|
||||||
|
update_storage_json()
|
||||||
|
path = CORE.relative_build_path("platformio.ini")
|
||||||
|
|
||||||
|
if os.path.isfile(path):
|
||||||
|
text = read_file(path)
|
||||||
|
content_format = find_begin_end(
|
||||||
|
text, INI_AUTO_GENERATE_BEGIN, INI_AUTO_GENERATE_END
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
content_format = INI_BASE_FORMAT
|
||||||
|
full_file = f"{content_format[0] + INI_AUTO_GENERATE_BEGIN}\n{content}"
|
||||||
|
full_file += INI_AUTO_GENERATE_END + content_format[1]
|
||||||
|
write_file_if_changed(path, full_file)
|
||||||
|
|
||||||
|
|
||||||
|
def write_project():
|
||||||
|
mkdir_p(CORE.build_path)
|
||||||
|
|
||||||
|
content = get_ini_content()
|
||||||
|
write_ini(content)
|
||||||
|
|
||||||
|
# Write extra script for C++ specific flags
|
||||||
|
write_cxx_flags_script()
|
||||||
|
|
||||||
|
|
||||||
|
CXX_FLAGS_FILE_NAME = "cxx_flags.py"
|
||||||
|
CXX_FLAGS_FILE_CONTENTS = """# Auto-generated ESPHome script for C++ specific compiler flags
|
||||||
|
Import("env")
|
||||||
|
|
||||||
|
# Add C++ specific flags
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def write_cxx_flags_script() -> None:
|
||||||
|
path = CORE.relative_build_path(CXX_FLAGS_FILE_NAME)
|
||||||
|
contents = CXX_FLAGS_FILE_CONTENTS
|
||||||
|
if not CORE.is_host:
|
||||||
|
contents += 'env.Append(CXXFLAGS=["-Wno-volatile"])'
|
||||||
|
contents += "\n"
|
||||||
|
write_file_if_changed(path, contents)
|
||||||
@@ -7,7 +7,6 @@ namespace a4988 {
|
|||||||
static const char *const TAG = "a4988.stepper";
|
static const char *const TAG = "a4988.stepper";
|
||||||
|
|
||||||
void A4988::setup() {
|
void A4988::setup() {
|
||||||
ESP_LOGCONFIG(TAG, "Running setup");
|
|
||||||
if (this->sleep_pin_ != nullptr) {
|
if (this->sleep_pin_ != nullptr) {
|
||||||
this->sleep_pin_->setup();
|
this->sleep_pin_->setup();
|
||||||
this->sleep_pin_->digital_write(false);
|
this->sleep_pin_->digital_write(false);
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ namespace absolute_humidity {
|
|||||||
static const char *const TAG = "absolute_humidity.sensor";
|
static const char *const TAG = "absolute_humidity.sensor";
|
||||||
|
|
||||||
void AbsoluteHumidityComponent::setup() {
|
void AbsoluteHumidityComponent::setup() {
|
||||||
ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->get_name().c_str());
|
|
||||||
|
|
||||||
ESP_LOGD(TAG, " Added callback for temperature '%s'", this->temperature_sensor_->get_name().c_str());
|
ESP_LOGD(TAG, " Added callback for temperature '%s'", this->temperature_sensor_->get_name().c_str());
|
||||||
this->temperature_sensor_->add_on_state_callback([this](float state) { this->temperature_callback_(state); });
|
this->temperature_sensor_->add_on_state_callback([this](float state) { this->temperature_callback_(state); });
|
||||||
if (this->temperature_sensor_->has_state()) {
|
if (this->temperature_sensor_->has_state()) {
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ const LogString *adc_unit_to_str(adc_unit_t unit) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ADCSensor::setup() {
|
void ADCSensor::setup() {
|
||||||
ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->get_name().c_str());
|
|
||||||
// Check if another sensor already initialized this ADC unit
|
// Check if another sensor already initialized this ADC unit
|
||||||
if (ADCSensor::shared_adc_handles[this->adc_unit_] == nullptr) {
|
if (ADCSensor::shared_adc_handles[this->adc_unit_] == nullptr) {
|
||||||
adc_oneshot_unit_init_cfg_t init_config = {}; // Zero initialize
|
adc_oneshot_unit_init_cfg_t init_config = {}; // Zero initialize
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ namespace adc {
|
|||||||
static const char *const TAG = "adc.esp8266";
|
static const char *const TAG = "adc.esp8266";
|
||||||
|
|
||||||
void ADCSensor::setup() {
|
void ADCSensor::setup() {
|
||||||
ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->get_name().c_str());
|
|
||||||
#ifndef USE_ADC_SENSOR_VCC
|
#ifndef USE_ADC_SENSOR_VCC
|
||||||
this->pin_->setup();
|
this->pin_->setup();
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ namespace adc {
|
|||||||
static const char *const TAG = "adc.libretiny";
|
static const char *const TAG = "adc.libretiny";
|
||||||
|
|
||||||
void ADCSensor::setup() {
|
void ADCSensor::setup() {
|
||||||
ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->get_name().c_str());
|
|
||||||
#ifndef USE_ADC_SENSOR_VCC
|
#ifndef USE_ADC_SENSOR_VCC
|
||||||
this->pin_->setup();
|
this->pin_->setup();
|
||||||
#endif // !USE_ADC_SENSOR_VCC
|
#endif // !USE_ADC_SENSOR_VCC
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ namespace adc {
|
|||||||
static const char *const TAG = "adc.rp2040";
|
static const char *const TAG = "adc.rp2040";
|
||||||
|
|
||||||
void ADCSensor::setup() {
|
void ADCSensor::setup() {
|
||||||
ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->get_name().c_str());
|
|
||||||
static bool initialized = false;
|
static bool initialized = false;
|
||||||
if (!initialized) {
|
if (!initialized) {
|
||||||
adc_init();
|
adc_init();
|
||||||
|
|||||||
@@ -8,10 +8,7 @@ static const char *const TAG = "adc128s102";
|
|||||||
|
|
||||||
float ADC128S102::get_setup_priority() const { return setup_priority::HARDWARE; }
|
float ADC128S102::get_setup_priority() const { return setup_priority::HARDWARE; }
|
||||||
|
|
||||||
void ADC128S102::setup() {
|
void ADC128S102::setup() { this->spi_setup(); }
|
||||||
ESP_LOGCONFIG(TAG, "Running setup");
|
|
||||||
this->spi_setup();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ADC128S102::dump_config() {
|
void ADC128S102::dump_config() {
|
||||||
ESP_LOGCONFIG(TAG, "ADC128S102:");
|
ESP_LOGCONFIG(TAG, "ADC128S102:");
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ static const uint8_t ADS1115_REGISTER_CONVERSION = 0x00;
|
|||||||
static const uint8_t ADS1115_REGISTER_CONFIG = 0x01;
|
static const uint8_t ADS1115_REGISTER_CONFIG = 0x01;
|
||||||
|
|
||||||
void ADS1115Component::setup() {
|
void ADS1115Component::setup() {
|
||||||
ESP_LOGCONFIG(TAG, "Running setup");
|
|
||||||
uint16_t value;
|
uint16_t value;
|
||||||
if (!this->read_byte_16(ADS1115_REGISTER_CONVERSION, &value)) {
|
if (!this->read_byte_16(ADS1115_REGISTER_CONVERSION, &value)) {
|
||||||
this->mark_failed();
|
this->mark_failed();
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ static const char *const TAG = "ads1118";
|
|||||||
static const uint8_t ADS1118_DATA_RATE_860_SPS = 0b111;
|
static const uint8_t ADS1118_DATA_RATE_860_SPS = 0b111;
|
||||||
|
|
||||||
void ADS1118::setup() {
|
void ADS1118::setup() {
|
||||||
ESP_LOGCONFIG(TAG, "Running setup");
|
|
||||||
this->spi_setup();
|
this->spi_setup();
|
||||||
|
|
||||||
this->config_ = 0;
|
this->config_ = 0;
|
||||||
|
|||||||
@@ -24,8 +24,6 @@ static const uint16_t ZP_CURRENT = 0x0000;
|
|||||||
static const uint16_t ZP_DEFAULT = 0xFFFF;
|
static const uint16_t ZP_DEFAULT = 0xFFFF;
|
||||||
|
|
||||||
void AGS10Component::setup() {
|
void AGS10Component::setup() {
|
||||||
ESP_LOGCONFIG(TAG, "Running setup");
|
|
||||||
|
|
||||||
auto version = this->read_version_();
|
auto version = this->read_version_();
|
||||||
if (version) {
|
if (version) {
|
||||||
ESP_LOGD(TAG, "AGS10 Sensor Version: 0x%02X", *version);
|
ESP_LOGD(TAG, "AGS10 Sensor Version: 0x%02X", *version);
|
||||||
@@ -45,8 +43,6 @@ void AGS10Component::setup() {
|
|||||||
} else {
|
} else {
|
||||||
ESP_LOGE(TAG, "AGS10 Sensor Resistance: unknown");
|
ESP_LOGE(TAG, "AGS10 Sensor Resistance: unknown");
|
||||||
}
|
}
|
||||||
|
|
||||||
ESP_LOGD(TAG, "Sensor initialized");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void AGS10Component::update() {
|
void AGS10Component::update() {
|
||||||
|
|||||||
@@ -38,8 +38,6 @@ static const uint8_t AHT10_STATUS_BUSY = 0x80;
|
|||||||
static const float AHT10_DIVISOR = 1048576.0f; // 2^20, used for temperature and humidity calculations
|
static const float AHT10_DIVISOR = 1048576.0f; // 2^20, used for temperature and humidity calculations
|
||||||
|
|
||||||
void AHT10Component::setup() {
|
void AHT10Component::setup() {
|
||||||
ESP_LOGCONFIG(TAG, "Running setup");
|
|
||||||
|
|
||||||
if (this->write(AHT10_SOFTRESET_CMD, sizeof(AHT10_SOFTRESET_CMD)) != i2c::ERROR_OK) {
|
if (this->write(AHT10_SOFTRESET_CMD, sizeof(AHT10_SOFTRESET_CMD)) != i2c::ERROR_OK) {
|
||||||
ESP_LOGE(TAG, "Reset failed");
|
ESP_LOGE(TAG, "Reset failed");
|
||||||
}
|
}
|
||||||
@@ -80,8 +78,6 @@ void AHT10Component::setup() {
|
|||||||
this->mark_failed();
|
this->mark_failed();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ESP_LOGV(TAG, "Initialization complete");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void AHT10Component::restart_read_() {
|
void AHT10Component::restart_read_() {
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ static const char *const TAG = "aic3204";
|
|||||||
}
|
}
|
||||||
|
|
||||||
void AIC3204::setup() {
|
void AIC3204::setup() {
|
||||||
ESP_LOGCONFIG(TAG, "Running setup");
|
|
||||||
|
|
||||||
// Set register page to 0
|
// Set register page to 0
|
||||||
ERROR_CHECK(this->write_byte(AIC3204_PAGE_CTRL, 0x00), "Set page 0 failed");
|
ERROR_CHECK(this->write_byte(AIC3204_PAGE_CTRL, 0x00), "Set page 0 failed");
|
||||||
// Initiate SW reset (PLL is powered off as part of reset)
|
// Initiate SW reset (PLL is powered off as part of reset)
|
||||||
|
|||||||
@@ -90,8 +90,6 @@ bool AM2315C::convert_(uint8_t *data, float &humidity, float &temperature) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void AM2315C::setup() {
|
void AM2315C::setup() {
|
||||||
ESP_LOGCONFIG(TAG, "Running setup");
|
|
||||||
|
|
||||||
// get status
|
// get status
|
||||||
uint8_t status = 0;
|
uint8_t status = 0;
|
||||||
if (this->read(&status, 1) != i2c::ERROR_OK) {
|
if (this->read(&status, 1) != i2c::ERROR_OK) {
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ void AM2320Component::update() {
|
|||||||
this->status_clear_warning();
|
this->status_clear_warning();
|
||||||
}
|
}
|
||||||
void AM2320Component::setup() {
|
void AM2320Component::setup() {
|
||||||
ESP_LOGCONFIG(TAG, "Running setup");
|
|
||||||
uint8_t data[8];
|
uint8_t data[8];
|
||||||
data[0] = 0;
|
data[0] = 0;
|
||||||
data[1] = 4;
|
data[1] = 4;
|
||||||
|
|||||||
@@ -54,8 +54,6 @@ enum { // APDS9306 registers
|
|||||||
}
|
}
|
||||||
|
|
||||||
void APDS9306::setup() {
|
void APDS9306::setup() {
|
||||||
ESP_LOGCONFIG(TAG, "Running setup");
|
|
||||||
|
|
||||||
uint8_t id;
|
uint8_t id;
|
||||||
if (!this->read_byte(APDS9306_PART_ID, &id)) { // Part ID register
|
if (!this->read_byte(APDS9306_PART_ID, &id)) { // Part ID register
|
||||||
this->error_code_ = COMMUNICATION_FAILED;
|
this->error_code_ = COMMUNICATION_FAILED;
|
||||||
@@ -86,8 +84,6 @@ void APDS9306::setup() {
|
|||||||
|
|
||||||
// Set to active mode
|
// Set to active mode
|
||||||
APDS9306_WRITE_BYTE(APDS9306_MAIN_CTRL, 0x02);
|
APDS9306_WRITE_BYTE(APDS9306_MAIN_CTRL, 0x02);
|
||||||
|
|
||||||
ESP_LOGCONFIG(TAG, "APDS9306 setup complete");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void APDS9306::dump_config() {
|
void APDS9306::dump_config() {
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ static const char *const TAG = "apds9960";
|
|||||||
#define APDS9960_WRITE_BYTE(reg, value) APDS9960_ERROR_CHECK(this->write_byte(reg, value));
|
#define APDS9960_WRITE_BYTE(reg, value) APDS9960_ERROR_CHECK(this->write_byte(reg, value));
|
||||||
|
|
||||||
void APDS9960::setup() {
|
void APDS9960::setup() {
|
||||||
ESP_LOGCONFIG(TAG, "Running setup");
|
|
||||||
uint8_t id;
|
uint8_t id;
|
||||||
if (!this->read_byte(0x92, &id)) { // ID register
|
if (!this->read_byte(0x92, &id)) { // ID register
|
||||||
this->error_code_ = COMMUNICATION_FAILED;
|
this->error_code_ = COMMUNICATION_FAILED;
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ SERVICE_ARG_NATIVE_TYPES = {
|
|||||||
CONF_ENCRYPTION = "encryption"
|
CONF_ENCRYPTION = "encryption"
|
||||||
CONF_BATCH_DELAY = "batch_delay"
|
CONF_BATCH_DELAY = "batch_delay"
|
||||||
CONF_CUSTOM_SERVICES = "custom_services"
|
CONF_CUSTOM_SERVICES = "custom_services"
|
||||||
|
CONF_HOMEASSISTANT_SERVICES = "homeassistant_services"
|
||||||
|
CONF_HOMEASSISTANT_STATES = "homeassistant_states"
|
||||||
|
|
||||||
|
|
||||||
def validate_encryption_key(value):
|
def validate_encryption_key(value):
|
||||||
@@ -118,6 +120,8 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
cv.Range(max=cv.TimePeriod(milliseconds=65535)),
|
cv.Range(max=cv.TimePeriod(milliseconds=65535)),
|
||||||
),
|
),
|
||||||
cv.Optional(CONF_CUSTOM_SERVICES, default=False): cv.boolean,
|
cv.Optional(CONF_CUSTOM_SERVICES, default=False): cv.boolean,
|
||||||
|
cv.Optional(CONF_HOMEASSISTANT_SERVICES, default=False): cv.boolean,
|
||||||
|
cv.Optional(CONF_HOMEASSISTANT_STATES, default=False): cv.boolean,
|
||||||
cv.Optional(CONF_ON_CLIENT_CONNECTED): automation.validate_automation(
|
cv.Optional(CONF_ON_CLIENT_CONNECTED): automation.validate_automation(
|
||||||
single=True
|
single=True
|
||||||
),
|
),
|
||||||
@@ -146,6 +150,12 @@ async def to_code(config):
|
|||||||
if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]:
|
if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]:
|
||||||
cg.add_define("USE_API_SERVICES")
|
cg.add_define("USE_API_SERVICES")
|
||||||
|
|
||||||
|
if config[CONF_HOMEASSISTANT_SERVICES]:
|
||||||
|
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||||
|
|
||||||
|
if config[CONF_HOMEASSISTANT_STATES]:
|
||||||
|
cg.add_define("USE_API_HOMEASSISTANT_STATES")
|
||||||
|
|
||||||
if actions := config.get(CONF_ACTIONS, []):
|
if actions := config.get(CONF_ACTIONS, []):
|
||||||
for conf in actions:
|
for conf in actions:
|
||||||
template_args = []
|
template_args = []
|
||||||
@@ -235,6 +245,7 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
|
|||||||
HOMEASSISTANT_ACTION_ACTION_SCHEMA,
|
HOMEASSISTANT_ACTION_ACTION_SCHEMA,
|
||||||
)
|
)
|
||||||
async def homeassistant_service_to_code(config, action_id, template_arg, args):
|
async def homeassistant_service_to_code(config, action_id, template_arg, args):
|
||||||
|
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||||
serv = await cg.get_variable(config[CONF_ID])
|
serv = await cg.get_variable(config[CONF_ID])
|
||||||
var = cg.new_Pvariable(action_id, template_arg, serv, False)
|
var = cg.new_Pvariable(action_id, template_arg, serv, False)
|
||||||
templ = await cg.templatable(config[CONF_ACTION], args, None)
|
templ = await cg.templatable(config[CONF_ACTION], args, None)
|
||||||
@@ -278,6 +289,7 @@ HOMEASSISTANT_EVENT_ACTION_SCHEMA = cv.Schema(
|
|||||||
HOMEASSISTANT_EVENT_ACTION_SCHEMA,
|
HOMEASSISTANT_EVENT_ACTION_SCHEMA,
|
||||||
)
|
)
|
||||||
async def homeassistant_event_to_code(config, action_id, template_arg, args):
|
async def homeassistant_event_to_code(config, action_id, template_arg, args):
|
||||||
|
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||||
serv = await cg.get_variable(config[CONF_ID])
|
serv = await cg.get_variable(config[CONF_ID])
|
||||||
var = cg.new_Pvariable(action_id, template_arg, serv, True)
|
var = cg.new_Pvariable(action_id, template_arg, serv, True)
|
||||||
templ = await cg.templatable(config[CONF_EVENT], args, None)
|
templ = await cg.templatable(config[CONF_EVENT], args, None)
|
||||||
@@ -323,9 +335,10 @@ async def api_connected_to_code(config, condition_id, template_arg, args):
|
|||||||
|
|
||||||
|
|
||||||
def FILTER_SOURCE_FILES() -> list[str]:
|
def FILTER_SOURCE_FILES() -> list[str]:
|
||||||
"""Filter out api_pb2_dump.cpp when proto message dumping is not enabled
|
"""Filter out api_pb2_dump.cpp when proto message dumping is not enabled,
|
||||||
and user_services.cpp when no services are defined."""
|
user_services.cpp when no services are defined, and protocol-specific
|
||||||
files_to_filter = []
|
implementations based on encryption configuration."""
|
||||||
|
files_to_filter: list[str] = []
|
||||||
|
|
||||||
# api_pb2_dump.cpp is only needed when HAS_PROTO_MESSAGE_DUMP is defined
|
# api_pb2_dump.cpp is only needed when HAS_PROTO_MESSAGE_DUMP is defined
|
||||||
# This is a particularly large file that still needs to be opened and read
|
# This is a particularly large file that still needs to be opened and read
|
||||||
@@ -341,4 +354,16 @@ def FILTER_SOURCE_FILES() -> list[str]:
|
|||||||
if config and not config.get(CONF_ACTIONS) and not config[CONF_CUSTOM_SERVICES]:
|
if config and not config.get(CONF_ACTIONS) and not config[CONF_CUSTOM_SERVICES]:
|
||||||
files_to_filter.append("user_services.cpp")
|
files_to_filter.append("user_services.cpp")
|
||||||
|
|
||||||
|
# Filter protocol-specific implementations based on encryption configuration
|
||||||
|
encryption_config = config.get(CONF_ENCRYPTION) if config else None
|
||||||
|
|
||||||
|
# If encryption is not configured at all, we only need plaintext
|
||||||
|
if encryption_config is None:
|
||||||
|
files_to_filter.append("api_frame_helper_noise.cpp")
|
||||||
|
# If encryption is configured with a key, we only need noise
|
||||||
|
elif encryption_config.get(CONF_KEY):
|
||||||
|
files_to_filter.append("api_frame_helper_plaintext.cpp")
|
||||||
|
# If encryption is configured but no key is provided, we need both
|
||||||
|
# (this allows a plaintext client to provide a noise key)
|
||||||
|
|
||||||
return files_to_filter
|
return files_to_filter
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ message DeviceInfoResponse {
|
|||||||
option (id) = 10;
|
option (id) = 10;
|
||||||
option (source) = SOURCE_SERVER;
|
option (source) = SOURCE_SERVER;
|
||||||
|
|
||||||
bool uses_password = 1;
|
bool uses_password = 1 [(field_ifdef) = "USE_API_PASSWORD"];
|
||||||
|
|
||||||
// The name of the node, given by "App.set_name()"
|
// The name of the node, given by "App.set_name()"
|
||||||
string name = 2;
|
string name = 2;
|
||||||
@@ -230,14 +230,16 @@ message DeviceInfoResponse {
|
|||||||
|
|
||||||
uint32 webserver_port = 10 [(field_ifdef) = "USE_WEBSERVER"];
|
uint32 webserver_port = 10 [(field_ifdef) = "USE_WEBSERVER"];
|
||||||
|
|
||||||
uint32 legacy_bluetooth_proxy_version = 11 [(field_ifdef) = "USE_BLUETOOTH_PROXY"];
|
// Deprecated in API version 1.9
|
||||||
|
uint32 legacy_bluetooth_proxy_version = 11 [deprecated=true, (field_ifdef) = "USE_BLUETOOTH_PROXY"];
|
||||||
uint32 bluetooth_proxy_feature_flags = 15 [(field_ifdef) = "USE_BLUETOOTH_PROXY"];
|
uint32 bluetooth_proxy_feature_flags = 15 [(field_ifdef) = "USE_BLUETOOTH_PROXY"];
|
||||||
|
|
||||||
string manufacturer = 12;
|
string manufacturer = 12;
|
||||||
|
|
||||||
string friendly_name = 13;
|
string friendly_name = 13;
|
||||||
|
|
||||||
uint32 legacy_voice_assistant_version = 14 [(field_ifdef) = "USE_VOICE_ASSISTANT"];
|
// Deprecated in API version 1.10
|
||||||
|
uint32 legacy_voice_assistant_version = 14 [deprecated=true, (field_ifdef) = "USE_VOICE_ASSISTANT"];
|
||||||
uint32 voice_assistant_feature_flags = 17 [(field_ifdef) = "USE_VOICE_ASSISTANT"];
|
uint32 voice_assistant_feature_flags = 17 [(field_ifdef) = "USE_VOICE_ASSISTANT"];
|
||||||
|
|
||||||
string suggested_area = 16 [(field_ifdef) = "USE_AREAS"];
|
string suggested_area = 16 [(field_ifdef) = "USE_AREAS"];
|
||||||
@@ -337,7 +339,9 @@ message ListEntitiesCoverResponse {
|
|||||||
uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"];
|
uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deprecated in API version 1.1
|
||||||
enum LegacyCoverState {
|
enum LegacyCoverState {
|
||||||
|
option deprecated = true;
|
||||||
LEGACY_COVER_STATE_OPEN = 0;
|
LEGACY_COVER_STATE_OPEN = 0;
|
||||||
LEGACY_COVER_STATE_CLOSED = 1;
|
LEGACY_COVER_STATE_CLOSED = 1;
|
||||||
}
|
}
|
||||||
@@ -356,7 +360,8 @@ message CoverStateResponse {
|
|||||||
fixed32 key = 1;
|
fixed32 key = 1;
|
||||||
// legacy: state has been removed in 1.13
|
// legacy: state has been removed in 1.13
|
||||||
// clients/servers must still send/accept it until the next protocol change
|
// clients/servers must still send/accept it until the next protocol change
|
||||||
LegacyCoverState legacy_state = 2;
|
// Deprecated in API version 1.1
|
||||||
|
LegacyCoverState legacy_state = 2 [deprecated=true];
|
||||||
|
|
||||||
float position = 3;
|
float position = 3;
|
||||||
float tilt = 4;
|
float tilt = 4;
|
||||||
@@ -364,7 +369,9 @@ message CoverStateResponse {
|
|||||||
uint32 device_id = 6 [(field_ifdef) = "USE_DEVICES"];
|
uint32 device_id = 6 [(field_ifdef) = "USE_DEVICES"];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deprecated in API version 1.1
|
||||||
enum LegacyCoverCommand {
|
enum LegacyCoverCommand {
|
||||||
|
option deprecated = true;
|
||||||
LEGACY_COVER_COMMAND_OPEN = 0;
|
LEGACY_COVER_COMMAND_OPEN = 0;
|
||||||
LEGACY_COVER_COMMAND_CLOSE = 1;
|
LEGACY_COVER_COMMAND_CLOSE = 1;
|
||||||
LEGACY_COVER_COMMAND_STOP = 2;
|
LEGACY_COVER_COMMAND_STOP = 2;
|
||||||
@@ -380,8 +387,10 @@ message CoverCommandRequest {
|
|||||||
|
|
||||||
// legacy: command has been removed in 1.13
|
// legacy: command has been removed in 1.13
|
||||||
// clients/servers must still send/accept it until the next protocol change
|
// clients/servers must still send/accept it until the next protocol change
|
||||||
bool has_legacy_command = 2;
|
// Deprecated in API version 1.1
|
||||||
LegacyCoverCommand legacy_command = 3;
|
bool has_legacy_command = 2 [deprecated=true];
|
||||||
|
// Deprecated in API version 1.1
|
||||||
|
LegacyCoverCommand legacy_command = 3 [deprecated=true];
|
||||||
|
|
||||||
bool has_position = 4;
|
bool has_position = 4;
|
||||||
float position = 5;
|
float position = 5;
|
||||||
@@ -413,7 +422,9 @@ message ListEntitiesFanResponse {
|
|||||||
repeated string supported_preset_modes = 12;
|
repeated string supported_preset_modes = 12;
|
||||||
uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"];
|
uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"];
|
||||||
}
|
}
|
||||||
|
// Deprecated in API version 1.6 - only used in deprecated fields
|
||||||
enum FanSpeed {
|
enum FanSpeed {
|
||||||
|
option deprecated = true;
|
||||||
FAN_SPEED_LOW = 0;
|
FAN_SPEED_LOW = 0;
|
||||||
FAN_SPEED_MEDIUM = 1;
|
FAN_SPEED_MEDIUM = 1;
|
||||||
FAN_SPEED_HIGH = 2;
|
FAN_SPEED_HIGH = 2;
|
||||||
@@ -432,7 +443,8 @@ message FanStateResponse {
|
|||||||
fixed32 key = 1;
|
fixed32 key = 1;
|
||||||
bool state = 2;
|
bool state = 2;
|
||||||
bool oscillating = 3;
|
bool oscillating = 3;
|
||||||
FanSpeed speed = 4 [deprecated = true];
|
// Deprecated in API version 1.6
|
||||||
|
FanSpeed speed = 4 [deprecated=true];
|
||||||
FanDirection direction = 5;
|
FanDirection direction = 5;
|
||||||
int32 speed_level = 6;
|
int32 speed_level = 6;
|
||||||
string preset_mode = 7;
|
string preset_mode = 7;
|
||||||
@@ -448,8 +460,10 @@ message FanCommandRequest {
|
|||||||
fixed32 key = 1;
|
fixed32 key = 1;
|
||||||
bool has_state = 2;
|
bool has_state = 2;
|
||||||
bool state = 3;
|
bool state = 3;
|
||||||
bool has_speed = 4 [deprecated = true];
|
// Deprecated in API version 1.6
|
||||||
FanSpeed speed = 5 [deprecated = true];
|
bool has_speed = 4 [deprecated=true];
|
||||||
|
// Deprecated in API version 1.6
|
||||||
|
FanSpeed speed = 5 [deprecated=true];
|
||||||
bool has_oscillating = 6;
|
bool has_oscillating = 6;
|
||||||
bool oscillating = 7;
|
bool oscillating = 7;
|
||||||
bool has_direction = 8;
|
bool has_direction = 8;
|
||||||
@@ -488,9 +502,13 @@ message ListEntitiesLightResponse {
|
|||||||
|
|
||||||
repeated ColorMode supported_color_modes = 12;
|
repeated ColorMode supported_color_modes = 12;
|
||||||
// next four supports_* are for legacy clients, newer clients should use color modes
|
// next four supports_* are for legacy clients, newer clients should use color modes
|
||||||
|
// Deprecated in API version 1.6
|
||||||
bool legacy_supports_brightness = 5 [deprecated=true];
|
bool legacy_supports_brightness = 5 [deprecated=true];
|
||||||
|
// Deprecated in API version 1.6
|
||||||
bool legacy_supports_rgb = 6 [deprecated=true];
|
bool legacy_supports_rgb = 6 [deprecated=true];
|
||||||
|
// Deprecated in API version 1.6
|
||||||
bool legacy_supports_white_value = 7 [deprecated=true];
|
bool legacy_supports_white_value = 7 [deprecated=true];
|
||||||
|
// Deprecated in API version 1.6
|
||||||
bool legacy_supports_color_temperature = 8 [deprecated=true];
|
bool legacy_supports_color_temperature = 8 [deprecated=true];
|
||||||
float min_mireds = 9;
|
float min_mireds = 9;
|
||||||
float max_mireds = 10;
|
float max_mireds = 10;
|
||||||
@@ -567,7 +585,9 @@ enum SensorStateClass {
|
|||||||
STATE_CLASS_TOTAL = 3;
|
STATE_CLASS_TOTAL = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deprecated in API version 1.5
|
||||||
enum SensorLastResetType {
|
enum SensorLastResetType {
|
||||||
|
option deprecated = true;
|
||||||
LAST_RESET_NONE = 0;
|
LAST_RESET_NONE = 0;
|
||||||
LAST_RESET_NEVER = 1;
|
LAST_RESET_NEVER = 1;
|
||||||
LAST_RESET_AUTO = 2;
|
LAST_RESET_AUTO = 2;
|
||||||
@@ -591,7 +611,8 @@ message ListEntitiesSensorResponse {
|
|||||||
string device_class = 9;
|
string device_class = 9;
|
||||||
SensorStateClass state_class = 10;
|
SensorStateClass state_class = 10;
|
||||||
// Last reset type removed in 2021.9.0
|
// Last reset type removed in 2021.9.0
|
||||||
SensorLastResetType legacy_last_reset_type = 11;
|
// Deprecated in API version 1.5
|
||||||
|
SensorLastResetType legacy_last_reset_type = 11 [deprecated=true];
|
||||||
bool disabled_by_default = 12;
|
bool disabled_by_default = 12;
|
||||||
EntityCategory entity_category = 13;
|
EntityCategory entity_category = 13;
|
||||||
uint32 device_id = 14 [(field_ifdef) = "USE_DEVICES"];
|
uint32 device_id = 14 [(field_ifdef) = "USE_DEVICES"];
|
||||||
@@ -711,7 +732,6 @@ message SubscribeLogsResponse {
|
|||||||
|
|
||||||
LogLevel level = 1;
|
LogLevel level = 1;
|
||||||
bytes message = 3;
|
bytes message = 3;
|
||||||
bool send_failed = 4;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== NOISE ENCRYPTION ====================
|
// ==================== NOISE ENCRYPTION ====================
|
||||||
@@ -735,17 +755,19 @@ message NoiseEncryptionSetKeyResponse {
|
|||||||
message SubscribeHomeassistantServicesRequest {
|
message SubscribeHomeassistantServicesRequest {
|
||||||
option (id) = 34;
|
option (id) = 34;
|
||||||
option (source) = SOURCE_CLIENT;
|
option (source) = SOURCE_CLIENT;
|
||||||
|
option (ifdef) = "USE_API_HOMEASSISTANT_SERVICES";
|
||||||
}
|
}
|
||||||
|
|
||||||
message HomeassistantServiceMap {
|
message HomeassistantServiceMap {
|
||||||
string key = 1;
|
string key = 1;
|
||||||
string value = 2;
|
string value = 2 [(no_zero_copy) = true];
|
||||||
}
|
}
|
||||||
|
|
||||||
message HomeassistantServiceResponse {
|
message HomeassistantServiceResponse {
|
||||||
option (id) = 35;
|
option (id) = 35;
|
||||||
option (source) = SOURCE_SERVER;
|
option (source) = SOURCE_SERVER;
|
||||||
option (no_delay) = true;
|
option (no_delay) = true;
|
||||||
|
option (ifdef) = "USE_API_HOMEASSISTANT_SERVICES";
|
||||||
|
|
||||||
string service = 1;
|
string service = 1;
|
||||||
repeated HomeassistantServiceMap data = 2;
|
repeated HomeassistantServiceMap data = 2;
|
||||||
@@ -761,11 +783,13 @@ message HomeassistantServiceResponse {
|
|||||||
message SubscribeHomeAssistantStatesRequest {
|
message SubscribeHomeAssistantStatesRequest {
|
||||||
option (id) = 38;
|
option (id) = 38;
|
||||||
option (source) = SOURCE_CLIENT;
|
option (source) = SOURCE_CLIENT;
|
||||||
|
option (ifdef) = "USE_API_HOMEASSISTANT_STATES";
|
||||||
}
|
}
|
||||||
|
|
||||||
message SubscribeHomeAssistantStateResponse {
|
message SubscribeHomeAssistantStateResponse {
|
||||||
option (id) = 39;
|
option (id) = 39;
|
||||||
option (source) = SOURCE_SERVER;
|
option (source) = SOURCE_SERVER;
|
||||||
|
option (ifdef) = "USE_API_HOMEASSISTANT_STATES";
|
||||||
string entity_id = 1;
|
string entity_id = 1;
|
||||||
string attribute = 2;
|
string attribute = 2;
|
||||||
bool once = 3;
|
bool once = 3;
|
||||||
@@ -775,6 +799,7 @@ message HomeAssistantStateResponse {
|
|||||||
option (id) = 40;
|
option (id) = 40;
|
||||||
option (source) = SOURCE_CLIENT;
|
option (source) = SOURCE_CLIENT;
|
||||||
option (no_delay) = true;
|
option (no_delay) = true;
|
||||||
|
option (ifdef) = "USE_API_HOMEASSISTANT_STATES";
|
||||||
|
|
||||||
string entity_id = 1;
|
string entity_id = 1;
|
||||||
string state = 2;
|
string state = 2;
|
||||||
@@ -947,7 +972,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 +1004,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 +1033,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 +1383,17 @@ message SubscribeBluetoothLEAdvertisementsRequest {
|
|||||||
uint32 flags = 1;
|
uint32 flags = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deprecated - only used by deprecated BluetoothLEAdvertisementResponse
|
||||||
message BluetoothServiceData {
|
message BluetoothServiceData {
|
||||||
|
option deprecated = true;
|
||||||
string uuid = 1;
|
string uuid = 1;
|
||||||
repeated uint32 legacy_data = 2 [deprecated = true]; // Removed in api version 1.7
|
// Deprecated in API version 1.7
|
||||||
|
repeated uint32 legacy_data = 2 [deprecated=true]; // Removed in api version 1.7
|
||||||
bytes data = 3; // Added in api version 1.7
|
bytes data = 3; // Added in api version 1.7
|
||||||
}
|
}
|
||||||
|
// Removed in ESPHome 2025.8.0 - use BluetoothLERawAdvertisementsResponse instead
|
||||||
message BluetoothLEAdvertisementResponse {
|
message BluetoothLEAdvertisementResponse {
|
||||||
|
option deprecated = true;
|
||||||
option (id) = 67;
|
option (id) = 67;
|
||||||
option (source) = SOURCE_SERVER;
|
option (source) = SOURCE_SERVER;
|
||||||
option (ifdef) = "USE_BLUETOOTH_PROXY";
|
option (ifdef) = "USE_BLUETOOTH_PROXY";
|
||||||
@@ -1381,7 +1415,7 @@ message BluetoothLERawAdvertisement {
|
|||||||
sint32 rssi = 2;
|
sint32 rssi = 2;
|
||||||
uint32 address_type = 3;
|
uint32 address_type = 3;
|
||||||
|
|
||||||
bytes data = 4;
|
bytes data = 4 [(fixed_array_size) = 62];
|
||||||
}
|
}
|
||||||
|
|
||||||
message BluetoothLERawAdvertisementsResponse {
|
message BluetoothLERawAdvertisementsResponse {
|
||||||
@@ -1434,19 +1468,19 @@ message BluetoothGATTGetServicesRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message BluetoothGATTDescriptor {
|
message BluetoothGATTDescriptor {
|
||||||
repeated uint64 uuid = 1;
|
repeated uint64 uuid = 1 [(fixed_array_size) = 2];
|
||||||
uint32 handle = 2;
|
uint32 handle = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message BluetoothGATTCharacteristic {
|
message BluetoothGATTCharacteristic {
|
||||||
repeated uint64 uuid = 1;
|
repeated uint64 uuid = 1 [(fixed_array_size) = 2];
|
||||||
uint32 handle = 2;
|
uint32 handle = 2;
|
||||||
uint32 properties = 3;
|
uint32 properties = 3;
|
||||||
repeated BluetoothGATTDescriptor descriptors = 4;
|
repeated BluetoothGATTDescriptor descriptors = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message BluetoothGATTService {
|
message BluetoothGATTService {
|
||||||
repeated uint64 uuid = 1;
|
repeated uint64 uuid = 1 [(fixed_array_size) = 2];
|
||||||
uint32 handle = 2;
|
uint32 handle = 2;
|
||||||
repeated BluetoothGATTCharacteristic characteristics = 3;
|
repeated BluetoothGATTCharacteristic characteristics = 3;
|
||||||
}
|
}
|
||||||
@@ -1457,7 +1491,7 @@ message BluetoothGATTGetServicesResponse {
|
|||||||
option (ifdef) = "USE_BLUETOOTH_PROXY";
|
option (ifdef) = "USE_BLUETOOTH_PROXY";
|
||||||
|
|
||||||
uint64 address = 1;
|
uint64 address = 1;
|
||||||
repeated BluetoothGATTService services = 2;
|
repeated BluetoothGATTService services = 2 [(fixed_array_size) = 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
message BluetoothGATTGetServicesDoneResponse {
|
message BluetoothGATTGetServicesDoneResponse {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -13,13 +13,36 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome::api {
|
||||||
namespace api {
|
|
||||||
|
// Client information structure
|
||||||
|
struct ClientInfo {
|
||||||
|
std::string name; // Client name from Hello message
|
||||||
|
std::string peername; // IP:port from socket
|
||||||
|
|
||||||
|
std::string get_combined_info() const {
|
||||||
|
if (name == peername) {
|
||||||
|
// Before Hello message, both are the same
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
return name + " (" + peername + ")";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Keepalive timeout in milliseconds
|
// Keepalive timeout in milliseconds
|
||||||
static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000;
|
static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000;
|
||||||
// Maximum number of entities to process in a single batch during initial state/info sending
|
// Maximum number of entities to process in a single batch during initial state/info sending
|
||||||
static constexpr size_t MAX_INITIAL_PER_BATCH = 20;
|
// This was increased from 20 to 24 after removing the unique_id field from entity info messages,
|
||||||
|
// which reduced message sizes allowing more entities per batch without exceeding packet limits
|
||||||
|
static constexpr size_t MAX_INITIAL_PER_BATCH = 24;
|
||||||
|
// Maximum number of packets to process in a single batch (platform-dependent)
|
||||||
|
// This limit exists to prevent stack overflow from the PacketInfo array in process_batch_
|
||||||
|
// Each PacketInfo is 8 bytes, so 64 * 8 = 512 bytes, 32 * 8 = 256 bytes
|
||||||
|
#if defined(USE_ESP32) || defined(USE_HOST)
|
||||||
|
static constexpr size_t MAX_PACKETS_PER_BATCH = 64; // ESP32 has 8KB+ stack, HOST has plenty
|
||||||
|
#else
|
||||||
|
static constexpr size_t MAX_PACKETS_PER_BATCH = 32; // ESP8266/RP2040/etc have smaller stacks
|
||||||
|
#endif
|
||||||
|
|
||||||
class APIConnection : public APIServerConnection {
|
class APIConnection : public APIServerConnection {
|
||||||
public:
|
public:
|
||||||
@@ -108,15 +131,16 @@ class APIConnection : public APIServerConnection {
|
|||||||
void media_player_command(const MediaPlayerCommandRequest &msg) override;
|
void media_player_command(const MediaPlayerCommandRequest &msg) override;
|
||||||
#endif
|
#endif
|
||||||
bool try_send_log_message(int level, const char *tag, const char *line, size_t message_len);
|
bool try_send_log_message(int level, const char *tag, const char *line, size_t message_len);
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||||
void send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
|
void send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
|
||||||
if (!this->flags_.service_call_subscription)
|
if (!this->flags_.service_call_subscription)
|
||||||
return;
|
return;
|
||||||
this->send_message(call, HomeassistantServiceResponse::MESSAGE_TYPE);
|
this->send_message(call, HomeassistantServiceResponse::MESSAGE_TYPE);
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
#ifdef USE_BLUETOOTH_PROXY
|
#ifdef USE_BLUETOOTH_PROXY
|
||||||
void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
|
void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
|
||||||
void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override;
|
void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override;
|
||||||
bool send_bluetooth_le_advertisement(const BluetoothLEAdvertisementResponse &msg);
|
|
||||||
|
|
||||||
void bluetooth_device_request(const BluetoothDeviceRequest &msg) override;
|
void bluetooth_device_request(const BluetoothDeviceRequest &msg) override;
|
||||||
void bluetooth_gatt_read(const BluetoothGATTReadRequest &msg) override;
|
void bluetooth_gatt_read(const BluetoothGATTReadRequest &msg) override;
|
||||||
@@ -125,8 +149,7 @@ class APIConnection : public APIServerConnection {
|
|||||||
void bluetooth_gatt_write_descriptor(const BluetoothGATTWriteDescriptorRequest &msg) override;
|
void bluetooth_gatt_write_descriptor(const BluetoothGATTWriteDescriptorRequest &msg) override;
|
||||||
void bluetooth_gatt_get_services(const BluetoothGATTGetServicesRequest &msg) override;
|
void bluetooth_gatt_get_services(const BluetoothGATTGetServicesRequest &msg) override;
|
||||||
void bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) override;
|
void bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) override;
|
||||||
BluetoothConnectionsFreeResponse subscribe_bluetooth_connections_free(
|
bool send_subscribe_bluetooth_connections_free_response(const SubscribeBluetoothConnectionsFreeRequest &msg) override;
|
||||||
const SubscribeBluetoothConnectionsFreeRequest &msg) override;
|
|
||||||
void bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) override;
|
void bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) override;
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
@@ -144,8 +167,7 @@ class APIConnection : public APIServerConnection {
|
|||||||
void on_voice_assistant_audio(const VoiceAssistantAudio &msg) override;
|
void on_voice_assistant_audio(const VoiceAssistantAudio &msg) override;
|
||||||
void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &msg) override;
|
void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &msg) override;
|
||||||
void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &msg) override;
|
void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &msg) override;
|
||||||
VoiceAssistantConfigurationResponse voice_assistant_get_configuration(
|
bool send_voice_assistant_get_configuration_response(const VoiceAssistantConfigurationRequest &msg) override;
|
||||||
const VoiceAssistantConfigurationRequest &msg) override;
|
|
||||||
void voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) override;
|
void voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) override;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -168,15 +190,17 @@ class APIConnection : public APIServerConnection {
|
|||||||
// we initiated ping
|
// we initiated ping
|
||||||
this->flags_.sent_ping = false;
|
this->flags_.sent_ping = false;
|
||||||
}
|
}
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||||
void on_home_assistant_state_response(const HomeAssistantStateResponse &msg) override;
|
void on_home_assistant_state_response(const HomeAssistantStateResponse &msg) override;
|
||||||
|
#endif
|
||||||
#ifdef USE_HOMEASSISTANT_TIME
|
#ifdef USE_HOMEASSISTANT_TIME
|
||||||
void on_get_time_response(const GetTimeResponse &value) override;
|
void on_get_time_response(const GetTimeResponse &value) override;
|
||||||
#endif
|
#endif
|
||||||
HelloResponse hello(const HelloRequest &msg) override;
|
bool send_hello_response(const HelloRequest &msg) override;
|
||||||
ConnectResponse connect(const ConnectRequest &msg) override;
|
bool send_connect_response(const ConnectRequest &msg) override;
|
||||||
DisconnectResponse disconnect(const DisconnectRequest &msg) override;
|
bool send_disconnect_response(const DisconnectRequest &msg) override;
|
||||||
PingResponse ping(const PingRequest &msg) override { return {}; }
|
bool send_ping_response(const PingRequest &msg) override;
|
||||||
DeviceInfoResponse device_info(const DeviceInfoRequest &msg) override;
|
bool send_device_info_response(const DeviceInfoRequest &msg) override;
|
||||||
void list_entities(const ListEntitiesRequest &msg) override { this->list_entities_iterator_.begin(); }
|
void list_entities(const ListEntitiesRequest &msg) override { this->list_entities_iterator_.begin(); }
|
||||||
void subscribe_states(const SubscribeStatesRequest &msg) override {
|
void subscribe_states(const SubscribeStatesRequest &msg) override {
|
||||||
this->flags_.state_subscription = true;
|
this->flags_.state_subscription = true;
|
||||||
@@ -187,19 +211,20 @@ class APIConnection : public APIServerConnection {
|
|||||||
if (msg.dump_config)
|
if (msg.dump_config)
|
||||||
App.schedule_dump_config();
|
App.schedule_dump_config();
|
||||||
}
|
}
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||||
void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) override {
|
void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) override {
|
||||||
this->flags_.service_call_subscription = true;
|
this->flags_.service_call_subscription = true;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||||
void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) override;
|
void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) override;
|
||||||
GetTimeResponse get_time(const GetTimeRequest &msg) override {
|
#endif
|
||||||
// TODO
|
bool send_get_time_response(const GetTimeRequest &msg) override;
|
||||||
return {};
|
|
||||||
}
|
|
||||||
#ifdef USE_API_SERVICES
|
#ifdef USE_API_SERVICES
|
||||||
void execute_service(const ExecuteServiceRequest &msg) override;
|
void execute_service(const ExecuteServiceRequest &msg) override;
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_API_NOISE
|
#ifdef USE_API_NOISE
|
||||||
NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) override;
|
bool send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) override;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
bool is_authenticated() override {
|
bool is_authenticated() override {
|
||||||
@@ -211,7 +236,9 @@ class APIConnection : public APIServerConnection {
|
|||||||
}
|
}
|
||||||
uint8_t get_log_subscription_level() const { return this->flags_.log_subscription; }
|
uint8_t get_log_subscription_level() const { return this->flags_.log_subscription; }
|
||||||
void on_fatal_error() override;
|
void on_fatal_error() override;
|
||||||
|
#ifdef USE_API_PASSWORD
|
||||||
void on_unauthenticated_access() override;
|
void on_unauthenticated_access() override;
|
||||||
|
#endif
|
||||||
void on_no_setup_connection() override;
|
void on_no_setup_connection() override;
|
||||||
ProtoWriteBuffer create_buffer(uint32_t reserve_size) override {
|
ProtoWriteBuffer create_buffer(uint32_t reserve_size) override {
|
||||||
// FIXME: ensure no recursive writes can happen
|
// FIXME: ensure no recursive writes can happen
|
||||||
@@ -261,13 +288,7 @@ class APIConnection : public APIServerConnection {
|
|||||||
bool try_to_clear_buffer(bool log_out_of_space);
|
bool try_to_clear_buffer(bool log_out_of_space);
|
||||||
bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override;
|
bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override;
|
||||||
|
|
||||||
std::string get_client_combined_info() const {
|
std::string get_client_combined_info() const { return this->client_info_.get_combined_info(); }
|
||||||
if (this->client_info_ == this->client_peername_) {
|
|
||||||
// Before Hello message, both are the same (just IP:port)
|
|
||||||
return this->client_info_;
|
|
||||||
}
|
|
||||||
return this->client_info_ + " (" + this->client_peername_ + ")";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Buffer allocator methods for batch processing
|
// Buffer allocator methods for batch processing
|
||||||
ProtoWriteBuffer allocate_single_message_buffer(uint16_t size);
|
ProtoWriteBuffer allocate_single_message_buffer(uint16_t size);
|
||||||
@@ -277,6 +298,10 @@ class APIConnection : public APIServerConnection {
|
|||||||
// Helper function to handle authentication completion
|
// Helper function to handle authentication completion
|
||||||
void complete_authentication_();
|
void complete_authentication_();
|
||||||
|
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||||
|
void process_state_subscriptions_();
|
||||||
|
#endif
|
||||||
|
|
||||||
// Non-template helper to encode any ProtoMessage
|
// Non-template helper to encode any ProtoMessage
|
||||||
static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint8_t message_type, APIConnection *conn,
|
static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint8_t message_type, APIConnection *conn,
|
||||||
uint32_t remaining_size, bool is_single);
|
uint32_t remaining_size, bool is_single);
|
||||||
@@ -296,13 +321,18 @@ class APIConnection : public APIServerConnection {
|
|||||||
APIConnection *conn, uint32_t remaining_size, bool is_single) {
|
APIConnection *conn, uint32_t remaining_size, bool is_single) {
|
||||||
// Set common fields that are shared by all entity types
|
// Set common fields that are shared by all entity types
|
||||||
msg.key = entity->get_object_id_hash();
|
msg.key = entity->get_object_id_hash();
|
||||||
msg.object_id = entity->get_object_id();
|
// IMPORTANT: get_object_id() may return a temporary std::string
|
||||||
|
std::string object_id = entity->get_object_id();
|
||||||
|
msg.set_object_id(StringRef(object_id));
|
||||||
|
|
||||||
if (entity->has_own_name())
|
if (entity->has_own_name()) {
|
||||||
msg.name = entity->get_name();
|
msg.set_name(entity->get_name());
|
||||||
|
}
|
||||||
|
|
||||||
// Set common EntityBase properties
|
// Set common EntityBase properties
|
||||||
msg.icon = entity->get_icon();
|
#ifdef USE_ENTITY_ICON
|
||||||
|
msg.set_icon(entity->get_icon_ref());
|
||||||
|
#endif
|
||||||
msg.disabled_by_default = entity->is_disabled_by_default();
|
msg.disabled_by_default = entity->is_disabled_by_default();
|
||||||
msg.entity_category = static_cast<enums::EntityCategory>(entity->get_entity_category());
|
msg.entity_category = static_cast<enums::EntityCategory>(entity->get_entity_category());
|
||||||
#ifdef USE_DEVICES
|
#ifdef USE_DEVICES
|
||||||
@@ -471,13 +501,14 @@ class APIConnection : public APIServerConnection {
|
|||||||
std::unique_ptr<camera::CameraImageReader> image_reader_;
|
std::unique_ptr<camera::CameraImageReader> image_reader_;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Group 3: Strings (12 bytes each on 32-bit, 4-byte aligned)
|
// Group 3: Client info struct (24 bytes on 32-bit: 2 strings × 12 bytes each)
|
||||||
std::string client_info_;
|
ClientInfo client_info_;
|
||||||
std::string client_peername_;
|
|
||||||
|
|
||||||
// Group 4: 4-byte types
|
// Group 4: 4-byte types
|
||||||
uint32_t last_traffic_;
|
uint32_t last_traffic_;
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||||
int state_subs_at_ = -1;
|
int state_subs_at_ = -1;
|
||||||
|
#endif
|
||||||
|
|
||||||
// Function pointer type for message encoding
|
// Function pointer type for message encoding
|
||||||
using MessageCreatorPtr = uint16_t (*)(EntityBase *, APIConnection *, uint32_t remaining_size, bool is_single);
|
using MessageCreatorPtr = uint16_t (*)(EntityBase *, APIConnection *, uint32_t remaining_size, bool is_single);
|
||||||
@@ -707,6 +738,5 @@ class APIConnection : public APIServerConnection {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace api
|
} // namespace esphome::api
|
||||||
} // namespace esphome
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -8,16 +8,17 @@
|
|||||||
|
|
||||||
#include "esphome/core/defines.h"
|
#include "esphome/core/defines.h"
|
||||||
#ifdef USE_API
|
#ifdef USE_API
|
||||||
#ifdef USE_API_NOISE
|
|
||||||
#include "noise/protocol.h"
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include "api_noise_context.h"
|
|
||||||
#include "esphome/components/socket/socket.h"
|
#include "esphome/components/socket/socket.h"
|
||||||
#include "esphome/core/application.h"
|
#include "esphome/core/application.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome::api {
|
||||||
namespace api {
|
|
||||||
|
// uncomment to log raw packets
|
||||||
|
//#define HELPER_LOG_PACKETS
|
||||||
|
|
||||||
|
// Forward declaration
|
||||||
|
struct ClientInfo;
|
||||||
|
|
||||||
class ProtoWriteBuffer;
|
class ProtoWriteBuffer;
|
||||||
|
|
||||||
@@ -40,7 +41,6 @@ struct PacketInfo {
|
|||||||
enum class APIError : uint16_t {
|
enum class APIError : uint16_t {
|
||||||
OK = 0,
|
OK = 0,
|
||||||
WOULD_BLOCK = 1001,
|
WOULD_BLOCK = 1001,
|
||||||
BAD_HANDSHAKE_PACKET_LEN = 1002,
|
|
||||||
BAD_INDICATOR = 1003,
|
BAD_INDICATOR = 1003,
|
||||||
BAD_DATA_PACKET = 1004,
|
BAD_DATA_PACKET = 1004,
|
||||||
TCP_NODELAY_FAILED = 1005,
|
TCP_NODELAY_FAILED = 1005,
|
||||||
@@ -51,16 +51,19 @@ enum class APIError : uint16_t {
|
|||||||
BAD_ARG = 1010,
|
BAD_ARG = 1010,
|
||||||
SOCKET_READ_FAILED = 1011,
|
SOCKET_READ_FAILED = 1011,
|
||||||
SOCKET_WRITE_FAILED = 1012,
|
SOCKET_WRITE_FAILED = 1012,
|
||||||
|
OUT_OF_MEMORY = 1018,
|
||||||
|
CONNECTION_CLOSED = 1022,
|
||||||
|
#ifdef USE_API_NOISE
|
||||||
|
BAD_HANDSHAKE_PACKET_LEN = 1002,
|
||||||
HANDSHAKESTATE_READ_FAILED = 1013,
|
HANDSHAKESTATE_READ_FAILED = 1013,
|
||||||
HANDSHAKESTATE_WRITE_FAILED = 1014,
|
HANDSHAKESTATE_WRITE_FAILED = 1014,
|
||||||
HANDSHAKESTATE_BAD_STATE = 1015,
|
HANDSHAKESTATE_BAD_STATE = 1015,
|
||||||
CIPHERSTATE_DECRYPT_FAILED = 1016,
|
CIPHERSTATE_DECRYPT_FAILED = 1016,
|
||||||
CIPHERSTATE_ENCRYPT_FAILED = 1017,
|
CIPHERSTATE_ENCRYPT_FAILED = 1017,
|
||||||
OUT_OF_MEMORY = 1018,
|
|
||||||
HANDSHAKESTATE_SETUP_FAILED = 1019,
|
HANDSHAKESTATE_SETUP_FAILED = 1019,
|
||||||
HANDSHAKESTATE_SPLIT_FAILED = 1020,
|
HANDSHAKESTATE_SPLIT_FAILED = 1020,
|
||||||
BAD_HANDSHAKE_ERROR_BYTE = 1021,
|
BAD_HANDSHAKE_ERROR_BYTE = 1021,
|
||||||
CONNECTION_CLOSED = 1022,
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
const char *api_error_to_str(APIError err);
|
const char *api_error_to_str(APIError err);
|
||||||
@@ -68,7 +71,8 @@ const char *api_error_to_str(APIError err);
|
|||||||
class APIFrameHelper {
|
class APIFrameHelper {
|
||||||
public:
|
public:
|
||||||
APIFrameHelper() = default;
|
APIFrameHelper() = default;
|
||||||
explicit APIFrameHelper(std::unique_ptr<socket::Socket> socket) : socket_owned_(std::move(socket)) {
|
explicit APIFrameHelper(std::unique_ptr<socket::Socket> socket, const ClientInfo *client_info)
|
||||||
|
: socket_owned_(std::move(socket)), client_info_(client_info) {
|
||||||
socket_ = socket_owned_.get();
|
socket_ = socket_owned_.get();
|
||||||
}
|
}
|
||||||
virtual ~APIFrameHelper() = default;
|
virtual ~APIFrameHelper() = default;
|
||||||
@@ -94,8 +98,6 @@ class APIFrameHelper {
|
|||||||
}
|
}
|
||||||
return APIError::OK;
|
return APIError::OK;
|
||||||
}
|
}
|
||||||
// Give this helper a name for logging
|
|
||||||
void set_log_info(std::string info) { info_ = std::move(info); }
|
|
||||||
virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0;
|
virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0;
|
||||||
// Write multiple protobuf packets in a single operation
|
// Write multiple protobuf packets in a single operation
|
||||||
// packets contains (message_type, offset, length) for each message in the buffer
|
// packets contains (message_type, offset, length) for each message in the buffer
|
||||||
@@ -109,29 +111,28 @@ class APIFrameHelper {
|
|||||||
bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); }
|
bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
// Struct for holding parsed frame data
|
|
||||||
struct ParsedFrame {
|
|
||||||
std::vector<uint8_t> msg;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Buffer containing data to be sent
|
// Buffer containing data to be sent
|
||||||
struct SendBuffer {
|
struct SendBuffer {
|
||||||
std::vector<uint8_t> data;
|
std::unique_ptr<uint8_t[]> data;
|
||||||
uint16_t offset{0}; // Current offset within the buffer (uint16_t to reduce memory usage)
|
uint16_t size{0}; // Total size of the buffer
|
||||||
|
uint16_t offset{0}; // Current offset within the buffer
|
||||||
|
|
||||||
// Using uint16_t reduces memory usage since ESPHome API messages are limited to UINT16_MAX (65535) bytes
|
// Using uint16_t reduces memory usage since ESPHome API messages are limited to UINT16_MAX (65535) bytes
|
||||||
uint16_t remaining() const { return static_cast<uint16_t>(data.size()) - offset; }
|
uint16_t remaining() const { return size - offset; }
|
||||||
const uint8_t *current_data() const { return data.data() + offset; }
|
const uint8_t *current_data() const { return data.get() + offset; }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Common implementation for writing raw data to socket
|
// Common implementation for writing raw data to socket
|
||||||
APIError write_raw_(const struct iovec *iov, int iovcnt);
|
APIError write_raw_(const struct iovec *iov, int iovcnt, uint16_t total_write_len);
|
||||||
|
|
||||||
// Try to send data from the tx buffer
|
// Try to send data from the tx buffer
|
||||||
APIError try_send_tx_buf_();
|
APIError try_send_tx_buf_();
|
||||||
|
|
||||||
// Helper method to buffer data from IOVs
|
// Helper method to buffer data from IOVs
|
||||||
void buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len);
|
void buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len, uint16_t offset);
|
||||||
|
|
||||||
|
// Common socket write error handling
|
||||||
|
APIError handle_socket_write_error_();
|
||||||
template<typename StateEnum>
|
template<typename StateEnum>
|
||||||
APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf,
|
APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf,
|
||||||
const std::string &info, StateEnum &state, StateEnum failed_state);
|
const std::string &info, StateEnum &state, StateEnum failed_state);
|
||||||
@@ -161,10 +162,13 @@ class APIFrameHelper {
|
|||||||
|
|
||||||
// Containers (size varies, but typically 12+ bytes on 32-bit)
|
// Containers (size varies, but typically 12+ bytes on 32-bit)
|
||||||
std::deque<SendBuffer> tx_buf_;
|
std::deque<SendBuffer> tx_buf_;
|
||||||
std::string info_;
|
|
||||||
std::vector<struct iovec> reusable_iovs_;
|
std::vector<struct iovec> reusable_iovs_;
|
||||||
std::vector<uint8_t> rx_buf_;
|
std::vector<uint8_t> rx_buf_;
|
||||||
|
|
||||||
|
// Pointer to client info (4 bytes on 32-bit)
|
||||||
|
// Note: The pointed-to ClientInfo object must outlive this APIFrameHelper instance.
|
||||||
|
const ClientInfo *client_info_{nullptr};
|
||||||
|
|
||||||
// Group smaller types together
|
// Group smaller types together
|
||||||
uint16_t rx_buf_len_ = 0;
|
uint16_t rx_buf_len_ = 0;
|
||||||
State state_{State::INITIALIZE};
|
State state_{State::INITIALIZE};
|
||||||
@@ -179,105 +183,6 @@ class APIFrameHelper {
|
|||||||
APIError handle_socket_read_result_(ssize_t received);
|
APIError handle_socket_read_result_(ssize_t received);
|
||||||
};
|
};
|
||||||
|
|
||||||
#ifdef USE_API_NOISE
|
} // namespace esphome::api
|
||||||
class APINoiseFrameHelper : public APIFrameHelper {
|
|
||||||
public:
|
|
||||||
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, std::shared_ptr<APINoiseContext> ctx)
|
|
||||||
: APIFrameHelper(std::move(socket)), ctx_(std::move(ctx)) {
|
|
||||||
// Noise header structure:
|
|
||||||
// Pos 0: indicator (0x01)
|
|
||||||
// Pos 1-2: encrypted payload size (16-bit big-endian)
|
|
||||||
// Pos 3-6: encrypted type (16-bit) + data_len (16-bit)
|
|
||||||
// Pos 7+: actual payload data
|
|
||||||
frame_header_padding_ = 7;
|
|
||||||
}
|
|
||||||
~APINoiseFrameHelper() override;
|
|
||||||
APIError init() override;
|
|
||||||
APIError loop() override;
|
|
||||||
APIError read_packet(ReadPacketBuffer *buffer) override;
|
|
||||||
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
|
|
||||||
APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override;
|
|
||||||
// Get the frame header padding required by this protocol
|
|
||||||
uint8_t frame_header_padding() override { return frame_header_padding_; }
|
|
||||||
// Get the frame footer size required by this protocol
|
|
||||||
uint8_t frame_footer_size() override { return frame_footer_size_; }
|
|
||||||
|
|
||||||
protected:
|
#endif // USE_API
|
||||||
APIError state_action_();
|
|
||||||
APIError try_read_frame_(ParsedFrame *frame);
|
|
||||||
APIError write_frame_(const uint8_t *data, uint16_t len);
|
|
||||||
APIError init_handshake_();
|
|
||||||
APIError check_handshake_finished_();
|
|
||||||
void send_explicit_handshake_reject_(const std::string &reason);
|
|
||||||
|
|
||||||
// Pointers first (4 bytes each)
|
|
||||||
NoiseHandshakeState *handshake_{nullptr};
|
|
||||||
NoiseCipherState *send_cipher_{nullptr};
|
|
||||||
NoiseCipherState *recv_cipher_{nullptr};
|
|
||||||
|
|
||||||
// Shared pointer (8 bytes on 32-bit = 4 bytes control block pointer + 4 bytes object pointer)
|
|
||||||
std::shared_ptr<APINoiseContext> ctx_;
|
|
||||||
|
|
||||||
// Vector (12 bytes on 32-bit)
|
|
||||||
std::vector<uint8_t> prologue_;
|
|
||||||
|
|
||||||
// NoiseProtocolId (size depends on implementation)
|
|
||||||
NoiseProtocolId nid_;
|
|
||||||
|
|
||||||
// Group small types together
|
|
||||||
// Fixed-size header buffer for noise protocol:
|
|
||||||
// 1 byte for indicator + 2 bytes for message size (16-bit value, not varint)
|
|
||||||
// Note: Maximum message size is UINT16_MAX (65535), with a limit of 128 bytes during handshake phase
|
|
||||||
uint8_t rx_header_buf_[3];
|
|
||||||
uint8_t rx_header_buf_len_ = 0;
|
|
||||||
// 4 bytes total, no padding
|
|
||||||
};
|
|
||||||
#endif // USE_API_NOISE
|
|
||||||
|
|
||||||
#ifdef USE_API_PLAINTEXT
|
|
||||||
class APIPlaintextFrameHelper : public APIFrameHelper {
|
|
||||||
public:
|
|
||||||
APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket) : APIFrameHelper(std::move(socket)) {
|
|
||||||
// Plaintext header structure (worst case):
|
|
||||||
// Pos 0: indicator (0x00)
|
|
||||||
// Pos 1-3: payload size varint (up to 3 bytes)
|
|
||||||
// Pos 4-5: message type varint (up to 2 bytes)
|
|
||||||
// Pos 6+: actual payload data
|
|
||||||
frame_header_padding_ = 6;
|
|
||||||
}
|
|
||||||
~APIPlaintextFrameHelper() override = default;
|
|
||||||
APIError init() override;
|
|
||||||
APIError loop() override;
|
|
||||||
APIError read_packet(ReadPacketBuffer *buffer) override;
|
|
||||||
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
|
|
||||||
APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override;
|
|
||||||
uint8_t frame_header_padding() override { return frame_header_padding_; }
|
|
||||||
// Get the frame footer size required by this protocol
|
|
||||||
uint8_t frame_footer_size() override { return frame_footer_size_; }
|
|
||||||
|
|
||||||
protected:
|
|
||||||
APIError try_read_frame_(ParsedFrame *frame);
|
|
||||||
|
|
||||||
// Group 2-byte aligned types
|
|
||||||
uint16_t rx_header_parsed_type_ = 0;
|
|
||||||
uint16_t rx_header_parsed_len_ = 0;
|
|
||||||
|
|
||||||
// Group 1-byte types together
|
|
||||||
// Fixed-size header buffer for plaintext protocol:
|
|
||||||
// We now store the indicator byte + the two varints.
|
|
||||||
// To match noise protocol's maximum message size (UINT16_MAX = 65535), we need:
|
|
||||||
// 1 byte for indicator + 3 bytes for message size varint (supports up to 2097151) + 2 bytes for message type varint
|
|
||||||
//
|
|
||||||
// While varints could theoretically be up to 10 bytes each for 64-bit values,
|
|
||||||
// attempting to process messages with headers that large would likely crash the
|
|
||||||
// ESP32 due to memory constraints.
|
|
||||||
uint8_t rx_header_buf_[6]; // 1 byte indicator + 5 bytes for varints (3 for size + 2 for type)
|
|
||||||
uint8_t rx_header_buf_pos_ = 0;
|
|
||||||
bool rx_header_parsed_ = false;
|
|
||||||
// 8 bytes total, no padding needed
|
|
||||||
};
|
|
||||||
#endif
|
|
||||||
|
|
||||||
} // namespace api
|
|
||||||
} // namespace esphome
|
|
||||||
#endif
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,68 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "api_frame_helper.h"
|
||||||
|
#ifdef USE_API
|
||||||
|
#ifdef USE_API_NOISE
|
||||||
|
#include "noise/protocol.h"
|
||||||
|
#include "api_noise_context.h"
|
||||||
|
|
||||||
|
namespace esphome::api {
|
||||||
|
|
||||||
|
class APINoiseFrameHelper : public APIFrameHelper {
|
||||||
|
public:
|
||||||
|
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, std::shared_ptr<APINoiseContext> ctx,
|
||||||
|
const ClientInfo *client_info)
|
||||||
|
: APIFrameHelper(std::move(socket), client_info), ctx_(std::move(ctx)) {
|
||||||
|
// Noise header structure:
|
||||||
|
// Pos 0: indicator (0x01)
|
||||||
|
// Pos 1-2: encrypted payload size (16-bit big-endian)
|
||||||
|
// Pos 3-6: encrypted type (16-bit) + data_len (16-bit)
|
||||||
|
// Pos 7+: actual payload data
|
||||||
|
frame_header_padding_ = 7;
|
||||||
|
}
|
||||||
|
~APINoiseFrameHelper() override;
|
||||||
|
APIError init() override;
|
||||||
|
APIError loop() override;
|
||||||
|
APIError read_packet(ReadPacketBuffer *buffer) override;
|
||||||
|
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
|
||||||
|
APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override;
|
||||||
|
// Get the frame header padding required by this protocol
|
||||||
|
uint8_t frame_header_padding() override { return frame_header_padding_; }
|
||||||
|
// Get the frame footer size required by this protocol
|
||||||
|
uint8_t frame_footer_size() override { return frame_footer_size_; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
APIError state_action_();
|
||||||
|
APIError try_read_frame_(std::vector<uint8_t> *frame);
|
||||||
|
APIError write_frame_(const uint8_t *data, uint16_t len);
|
||||||
|
APIError init_handshake_();
|
||||||
|
APIError check_handshake_finished_();
|
||||||
|
void send_explicit_handshake_reject_(const std::string &reason);
|
||||||
|
APIError handle_handshake_frame_error_(APIError aerr);
|
||||||
|
APIError handle_noise_error_(int err, const char *func_name, APIError api_err);
|
||||||
|
|
||||||
|
// Pointers first (4 bytes each)
|
||||||
|
NoiseHandshakeState *handshake_{nullptr};
|
||||||
|
NoiseCipherState *send_cipher_{nullptr};
|
||||||
|
NoiseCipherState *recv_cipher_{nullptr};
|
||||||
|
|
||||||
|
// Shared pointer (8 bytes on 32-bit = 4 bytes control block pointer + 4 bytes object pointer)
|
||||||
|
std::shared_ptr<APINoiseContext> ctx_;
|
||||||
|
|
||||||
|
// Vector (12 bytes on 32-bit)
|
||||||
|
std::vector<uint8_t> prologue_;
|
||||||
|
|
||||||
|
// NoiseProtocolId (size depends on implementation)
|
||||||
|
NoiseProtocolId nid_;
|
||||||
|
|
||||||
|
// Group small types together
|
||||||
|
// Fixed-size header buffer for noise protocol:
|
||||||
|
// 1 byte for indicator + 2 bytes for message size (16-bit value, not varint)
|
||||||
|
// Note: Maximum message size is UINT16_MAX (65535), with a limit of 128 bytes during handshake phase
|
||||||
|
uint8_t rx_header_buf_[3];
|
||||||
|
uint8_t rx_header_buf_len_ = 0;
|
||||||
|
// 4 bytes total, no padding
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace esphome::api
|
||||||
|
#endif // USE_API_NOISE
|
||||||
|
#endif // USE_API
|
||||||
@@ -0,0 +1,290 @@
|
|||||||
|
#include "api_frame_helper_plaintext.h"
|
||||||
|
#ifdef USE_API
|
||||||
|
#ifdef USE_API_PLAINTEXT
|
||||||
|
#include "api_connection.h" // For ClientInfo struct
|
||||||
|
#include "esphome/core/application.h"
|
||||||
|
#include "esphome/core/hal.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
#include "proto.h"
|
||||||
|
#include <cstring>
|
||||||
|
#include <cinttypes>
|
||||||
|
|
||||||
|
namespace esphome::api {
|
||||||
|
|
||||||
|
static const char *const TAG = "api.plaintext";
|
||||||
|
|
||||||
|
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->client_info_->get_combined_info().c_str(), ##__VA_ARGS__)
|
||||||
|
|
||||||
|
#ifdef HELPER_LOG_PACKETS
|
||||||
|
#define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str())
|
||||||
|
#define LOG_PACKET_SENDING(data, len) ESP_LOGVV(TAG, "Sending raw: %s", format_hex_pretty(data, len).c_str())
|
||||||
|
#else
|
||||||
|
#define LOG_PACKET_RECEIVED(buffer) ((void) 0)
|
||||||
|
#define LOG_PACKET_SENDING(data, len) ((void) 0)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/// Initialize the frame helper, returns OK if successful.
|
||||||
|
APIError APIPlaintextFrameHelper::init() {
|
||||||
|
APIError err = init_common_();
|
||||||
|
if (err != APIError::OK) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
state_ = State::DATA;
|
||||||
|
return APIError::OK;
|
||||||
|
}
|
||||||
|
APIError APIPlaintextFrameHelper::loop() {
|
||||||
|
if (state_ != State::DATA) {
|
||||||
|
return APIError::BAD_STATE;
|
||||||
|
}
|
||||||
|
// Use base class implementation for buffer sending
|
||||||
|
return APIFrameHelper::loop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter
|
||||||
|
*
|
||||||
|
* @param frame: The struct to hold the frame information in.
|
||||||
|
* msg: store the parsed frame in that struct
|
||||||
|
*
|
||||||
|
* @return See APIError
|
||||||
|
*
|
||||||
|
* error API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame.
|
||||||
|
*/
|
||||||
|
APIError APIPlaintextFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
|
||||||
|
if (frame == nullptr) {
|
||||||
|
HELPER_LOG("Bad argument for try_read_frame_");
|
||||||
|
return APIError::BAD_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
// read header
|
||||||
|
while (!rx_header_parsed_) {
|
||||||
|
// Now that we know when the socket is ready, we can read up to 3 bytes
|
||||||
|
// into the rx_header_buf_ before we have to switch back to reading
|
||||||
|
// one byte at a time to ensure we don't read past the message and
|
||||||
|
// into the next one.
|
||||||
|
|
||||||
|
// Read directly into rx_header_buf_ at the current position
|
||||||
|
// Try to get to at least 3 bytes total (indicator + 2 varint bytes), then read one byte at a time
|
||||||
|
ssize_t received =
|
||||||
|
this->socket_->read(&rx_header_buf_[rx_header_buf_pos_], rx_header_buf_pos_ < 3 ? 3 - rx_header_buf_pos_ : 1);
|
||||||
|
APIError err = handle_socket_read_result_(received);
|
||||||
|
if (err != APIError::OK) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this was the first read, validate the indicator byte
|
||||||
|
if (rx_header_buf_pos_ == 0 && received > 0) {
|
||||||
|
if (rx_header_buf_[0] != 0x00) {
|
||||||
|
state_ = State::FAILED;
|
||||||
|
HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]);
|
||||||
|
return APIError::BAD_INDICATOR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rx_header_buf_pos_ += received;
|
||||||
|
|
||||||
|
// Check for buffer overflow
|
||||||
|
if (rx_header_buf_pos_ >= sizeof(rx_header_buf_)) {
|
||||||
|
state_ = State::FAILED;
|
||||||
|
HELPER_LOG("Header buffer overflow");
|
||||||
|
return APIError::BAD_DATA_PACKET;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Need at least 3 bytes total (indicator + 2 varint bytes) before trying to parse
|
||||||
|
if (rx_header_buf_pos_ < 3) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this point, we have at least 3 bytes total:
|
||||||
|
// - Validated indicator byte (0x00) stored at position 0
|
||||||
|
// - At least 2 bytes in the buffer for the varints
|
||||||
|
// Buffer layout:
|
||||||
|
// [0]: indicator byte (0x00)
|
||||||
|
// [1-3]: Message size varint (variable length)
|
||||||
|
// - 2 bytes would only allow up to 16383, which is less than noise's UINT16_MAX (65535)
|
||||||
|
// - 3 bytes allows up to 2097151, ensuring we support at least as much as noise
|
||||||
|
// [2-5]: Message type varint (variable length)
|
||||||
|
// We now attempt to parse both varints. If either is incomplete,
|
||||||
|
// we'll continue reading more bytes.
|
||||||
|
|
||||||
|
// Skip indicator byte at position 0
|
||||||
|
uint8_t varint_pos = 1;
|
||||||
|
uint32_t consumed = 0;
|
||||||
|
|
||||||
|
auto msg_size_varint = ProtoVarInt::parse(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos, &consumed);
|
||||||
|
if (!msg_size_varint.has_value()) {
|
||||||
|
// not enough data there yet
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg_size_varint->as_uint32() > std::numeric_limits<uint16_t>::max()) {
|
||||||
|
state_ = State::FAILED;
|
||||||
|
HELPER_LOG("Bad packet: message size %" PRIu32 " exceeds maximum %u", msg_size_varint->as_uint32(),
|
||||||
|
std::numeric_limits<uint16_t>::max());
|
||||||
|
return APIError::BAD_DATA_PACKET;
|
||||||
|
}
|
||||||
|
rx_header_parsed_len_ = msg_size_varint->as_uint16();
|
||||||
|
|
||||||
|
// Move to next varint position
|
||||||
|
varint_pos += consumed;
|
||||||
|
|
||||||
|
auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos, &consumed);
|
||||||
|
if (!msg_type_varint.has_value()) {
|
||||||
|
// not enough data there yet
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (msg_type_varint->as_uint32() > std::numeric_limits<uint16_t>::max()) {
|
||||||
|
state_ = State::FAILED;
|
||||||
|
HELPER_LOG("Bad packet: message type %" PRIu32 " exceeds maximum %u", msg_type_varint->as_uint32(),
|
||||||
|
std::numeric_limits<uint16_t>::max());
|
||||||
|
return APIError::BAD_DATA_PACKET;
|
||||||
|
}
|
||||||
|
rx_header_parsed_type_ = msg_type_varint->as_uint16();
|
||||||
|
rx_header_parsed_ = true;
|
||||||
|
}
|
||||||
|
// header reading done
|
||||||
|
|
||||||
|
// reserve space for body
|
||||||
|
if (rx_buf_.size() != rx_header_parsed_len_) {
|
||||||
|
rx_buf_.resize(rx_header_parsed_len_);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rx_buf_len_ < rx_header_parsed_len_) {
|
||||||
|
// more data to read
|
||||||
|
uint16_t to_read = rx_header_parsed_len_ - rx_buf_len_;
|
||||||
|
ssize_t received = this->socket_->read(&rx_buf_[rx_buf_len_], to_read);
|
||||||
|
APIError err = handle_socket_read_result_(received);
|
||||||
|
if (err != APIError::OK) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
rx_buf_len_ += static_cast<uint16_t>(received);
|
||||||
|
if (static_cast<uint16_t>(received) != to_read) {
|
||||||
|
// not all read
|
||||||
|
return APIError::WOULD_BLOCK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_PACKET_RECEIVED(rx_buf_);
|
||||||
|
*frame = std::move(rx_buf_);
|
||||||
|
// consume msg
|
||||||
|
rx_buf_ = {};
|
||||||
|
rx_buf_len_ = 0;
|
||||||
|
rx_header_buf_pos_ = 0;
|
||||||
|
rx_header_parsed_ = false;
|
||||||
|
return APIError::OK;
|
||||||
|
}
|
||||||
|
APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
||||||
|
APIError aerr;
|
||||||
|
|
||||||
|
if (state_ != State::DATA) {
|
||||||
|
return APIError::WOULD_BLOCK;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<uint8_t> frame;
|
||||||
|
aerr = try_read_frame_(&frame);
|
||||||
|
if (aerr != APIError::OK) {
|
||||||
|
if (aerr == APIError::BAD_INDICATOR) {
|
||||||
|
// Make sure to tell the remote that we don't
|
||||||
|
// understand the indicator byte so it knows
|
||||||
|
// we do not support it.
|
||||||
|
struct iovec iov[1];
|
||||||
|
// The \x00 first byte is the marker for plaintext.
|
||||||
|
//
|
||||||
|
// The remote will know how to handle the indicator byte,
|
||||||
|
// but it likely won't understand the rest of the message.
|
||||||
|
//
|
||||||
|
// We must send at least 3 bytes to be read, so we add
|
||||||
|
// a message after the indicator byte to ensures its long
|
||||||
|
// enough and can aid in debugging.
|
||||||
|
const char msg[] = "\x00"
|
||||||
|
"Bad indicator byte";
|
||||||
|
iov[0].iov_base = (void *) msg;
|
||||||
|
iov[0].iov_len = 19;
|
||||||
|
this->write_raw_(iov, 1, 19);
|
||||||
|
}
|
||||||
|
return aerr;
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer->container = std::move(frame);
|
||||||
|
buffer->data_offset = 0;
|
||||||
|
buffer->data_len = rx_header_parsed_len_;
|
||||||
|
buffer->type = rx_header_parsed_type_;
|
||||||
|
return APIError::OK;
|
||||||
|
}
|
||||||
|
APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
|
||||||
|
PacketInfo packet{type, 0, static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_)};
|
||||||
|
return write_protobuf_packets(buffer, std::span<const PacketInfo>(&packet, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) {
|
||||||
|
if (state_ != State::DATA) {
|
||||||
|
return APIError::BAD_STATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (packets.empty()) {
|
||||||
|
return APIError::OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<uint8_t> *raw_buffer = buffer.get_buffer();
|
||||||
|
uint8_t *buffer_data = raw_buffer->data(); // Cache buffer pointer
|
||||||
|
|
||||||
|
this->reusable_iovs_.clear();
|
||||||
|
this->reusable_iovs_.reserve(packets.size());
|
||||||
|
uint16_t total_write_len = 0;
|
||||||
|
|
||||||
|
for (const auto &packet : packets) {
|
||||||
|
// Calculate varint sizes for header layout
|
||||||
|
uint8_t size_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(packet.payload_size));
|
||||||
|
uint8_t type_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(packet.message_type));
|
||||||
|
uint8_t total_header_len = 1 + size_varint_len + type_varint_len;
|
||||||
|
|
||||||
|
// Calculate where to start writing the header
|
||||||
|
// The header starts at the latest possible position to minimize unused padding
|
||||||
|
//
|
||||||
|
// Example 1 (small values): total_header_len = 3, header_offset = 6 - 3 = 3
|
||||||
|
// [0-2] - Unused padding
|
||||||
|
// [3] - 0x00 indicator byte
|
||||||
|
// [4] - Payload size varint (1 byte, for sizes 0-127)
|
||||||
|
// [5] - Message type varint (1 byte, for types 0-127)
|
||||||
|
// [6...] - Actual payload data
|
||||||
|
//
|
||||||
|
// Example 2 (medium values): total_header_len = 4, header_offset = 6 - 4 = 2
|
||||||
|
// [0-1] - Unused padding
|
||||||
|
// [2] - 0x00 indicator byte
|
||||||
|
// [3-4] - Payload size varint (2 bytes, for sizes 128-16383)
|
||||||
|
// [5] - Message type varint (1 byte, for types 0-127)
|
||||||
|
// [6...] - Actual payload data
|
||||||
|
//
|
||||||
|
// Example 3 (large values): total_header_len = 6, header_offset = 6 - 6 = 0
|
||||||
|
// [0] - 0x00 indicator byte
|
||||||
|
// [1-3] - Payload size varint (3 bytes, for sizes 16384-2097151)
|
||||||
|
// [4-5] - Message type varint (2 bytes, for types 128-32767)
|
||||||
|
// [6...] - Actual payload data
|
||||||
|
//
|
||||||
|
// The message starts at offset + frame_header_padding_
|
||||||
|
// So we write the header starting at offset + frame_header_padding_ - total_header_len
|
||||||
|
uint8_t *buf_start = buffer_data + packet.offset;
|
||||||
|
uint32_t header_offset = frame_header_padding_ - total_header_len;
|
||||||
|
|
||||||
|
// Write the plaintext header
|
||||||
|
buf_start[header_offset] = 0x00; // indicator
|
||||||
|
|
||||||
|
// Encode varints directly into buffer
|
||||||
|
ProtoVarInt(packet.payload_size).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len);
|
||||||
|
ProtoVarInt(packet.message_type)
|
||||||
|
.encode_to_buffer_unchecked(buf_start + header_offset + 1 + size_varint_len, type_varint_len);
|
||||||
|
|
||||||
|
// Add iovec for this packet (header + payload)
|
||||||
|
size_t packet_len = static_cast<size_t>(total_header_len + packet.payload_size);
|
||||||
|
this->reusable_iovs_.push_back({buf_start + header_offset, packet_len});
|
||||||
|
total_write_len += packet_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send all packets in one writev call
|
||||||
|
return write_raw_(this->reusable_iovs_.data(), this->reusable_iovs_.size(), total_write_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace esphome::api
|
||||||
|
#endif // USE_API_PLAINTEXT
|
||||||
|
#endif // USE_API
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "api_frame_helper.h"
|
||||||
|
#ifdef USE_API
|
||||||
|
#ifdef USE_API_PLAINTEXT
|
||||||
|
|
||||||
|
namespace esphome::api {
|
||||||
|
|
||||||
|
class APIPlaintextFrameHelper : public APIFrameHelper {
|
||||||
|
public:
|
||||||
|
APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket, const ClientInfo *client_info)
|
||||||
|
: APIFrameHelper(std::move(socket), client_info) {
|
||||||
|
// Plaintext header structure (worst case):
|
||||||
|
// Pos 0: indicator (0x00)
|
||||||
|
// Pos 1-3: payload size varint (up to 3 bytes)
|
||||||
|
// Pos 4-5: message type varint (up to 2 bytes)
|
||||||
|
// Pos 6+: actual payload data
|
||||||
|
frame_header_padding_ = 6;
|
||||||
|
}
|
||||||
|
~APIPlaintextFrameHelper() override = default;
|
||||||
|
APIError init() override;
|
||||||
|
APIError loop() override;
|
||||||
|
APIError read_packet(ReadPacketBuffer *buffer) override;
|
||||||
|
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
|
||||||
|
APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override;
|
||||||
|
uint8_t frame_header_padding() override { return frame_header_padding_; }
|
||||||
|
// Get the frame footer size required by this protocol
|
||||||
|
uint8_t frame_footer_size() override { return frame_footer_size_; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
APIError try_read_frame_(std::vector<uint8_t> *frame);
|
||||||
|
|
||||||
|
// Group 2-byte aligned types
|
||||||
|
uint16_t rx_header_parsed_type_ = 0;
|
||||||
|
uint16_t rx_header_parsed_len_ = 0;
|
||||||
|
|
||||||
|
// Group 1-byte types together
|
||||||
|
// Fixed-size header buffer for plaintext protocol:
|
||||||
|
// We now store the indicator byte + the two varints.
|
||||||
|
// To match noise protocol's maximum message size (UINT16_MAX = 65535), we need:
|
||||||
|
// 1 byte for indicator + 3 bytes for message size varint (supports up to 2097151) + 2 bytes for message type varint
|
||||||
|
//
|
||||||
|
// While varints could theoretically be up to 10 bytes each for 64-bit values,
|
||||||
|
// attempting to process messages with headers that large would likely crash the
|
||||||
|
// ESP32 due to memory constraints.
|
||||||
|
uint8_t rx_header_buf_[6]; // 1 byte indicator + 5 bytes for varints (3 for size + 2 for type)
|
||||||
|
uint8_t rx_header_buf_pos_ = 0;
|
||||||
|
bool rx_header_parsed_ = false;
|
||||||
|
// 8 bytes total, no padding needed
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace esphome::api
|
||||||
|
#endif // USE_API_PLAINTEXT
|
||||||
|
#endif // USE_API
|
||||||
@@ -3,8 +3,7 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include "esphome/core/defines.h"
|
#include "esphome/core/defines.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome::api {
|
||||||
namespace api {
|
|
||||||
|
|
||||||
#ifdef USE_API_NOISE
|
#ifdef USE_API_NOISE
|
||||||
using psk_t = std::array<uint8_t, 32>;
|
using psk_t = std::array<uint8_t, 32>;
|
||||||
@@ -28,5 +27,4 @@ class APINoiseContext {
|
|||||||
};
|
};
|
||||||
#endif // USE_API_NOISE
|
#endif // USE_API_NOISE
|
||||||
|
|
||||||
} // namespace api
|
} // namespace esphome::api
|
||||||
} // namespace esphome
|
|
||||||
|
|||||||
@@ -26,4 +26,6 @@ extend google.protobuf.MessageOptions {
|
|||||||
|
|
||||||
extend google.protobuf.FieldOptions {
|
extend google.protobuf.FieldOptions {
|
||||||
optional string field_ifdef = 1042;
|
optional string field_ifdef = 1042;
|
||||||
|
optional uint32 fixed_array_size = 50007;
|
||||||
|
optional bool no_zero_copy = 50008 [default=false];
|
||||||
}
|
}
|
||||||
|
|||||||
+288
-383
File diff suppressed because it is too large
Load Diff
+181
-157
File diff suppressed because it is too large
Load Diff
+872
-3401
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -6,8 +6,7 @@
|
|||||||
|
|
||||||
#include "api_pb2.h"
|
#include "api_pb2.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome::api {
|
||||||
namespace api {
|
|
||||||
|
|
||||||
class APIServerConnectionBase : public ProtoService {
|
class APIServerConnectionBase : public ProtoService {
|
||||||
public:
|
public:
|
||||||
@@ -61,11 +60,17 @@ class APIServerConnectionBase : public ProtoService {
|
|||||||
virtual void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &value){};
|
virtual void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &value){};
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||||
virtual void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &value){};
|
virtual void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &value){};
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||||
virtual void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &value){};
|
virtual void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &value){};
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||||
virtual void on_home_assistant_state_response(const HomeAssistantStateResponse &value){};
|
virtual void on_home_assistant_state_response(const HomeAssistantStateResponse &value){};
|
||||||
|
#endif
|
||||||
virtual void on_get_time_request(const GetTimeRequest &value){};
|
virtual void on_get_time_request(const GetTimeRequest &value){};
|
||||||
virtual void on_get_time_response(const GetTimeResponse &value){};
|
virtual void on_get_time_response(const GetTimeResponse &value){};
|
||||||
|
|
||||||
@@ -207,22 +212,26 @@ class APIServerConnectionBase : public ProtoService {
|
|||||||
|
|
||||||
class APIServerConnection : public APIServerConnectionBase {
|
class APIServerConnection : public APIServerConnectionBase {
|
||||||
public:
|
public:
|
||||||
virtual HelloResponse hello(const HelloRequest &msg) = 0;
|
virtual bool send_hello_response(const HelloRequest &msg) = 0;
|
||||||
virtual ConnectResponse connect(const ConnectRequest &msg) = 0;
|
virtual bool send_connect_response(const ConnectRequest &msg) = 0;
|
||||||
virtual DisconnectResponse disconnect(const DisconnectRequest &msg) = 0;
|
virtual bool send_disconnect_response(const DisconnectRequest &msg) = 0;
|
||||||
virtual PingResponse ping(const PingRequest &msg) = 0;
|
virtual bool send_ping_response(const PingRequest &msg) = 0;
|
||||||
virtual DeviceInfoResponse device_info(const DeviceInfoRequest &msg) = 0;
|
virtual bool send_device_info_response(const DeviceInfoRequest &msg) = 0;
|
||||||
virtual void list_entities(const ListEntitiesRequest &msg) = 0;
|
virtual void list_entities(const ListEntitiesRequest &msg) = 0;
|
||||||
virtual void subscribe_states(const SubscribeStatesRequest &msg) = 0;
|
virtual void subscribe_states(const SubscribeStatesRequest &msg) = 0;
|
||||||
virtual void subscribe_logs(const SubscribeLogsRequest &msg) = 0;
|
virtual void subscribe_logs(const SubscribeLogsRequest &msg) = 0;
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||||
virtual void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) = 0;
|
virtual void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) = 0;
|
||||||
|
#endif
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||||
virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0;
|
virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0;
|
||||||
virtual GetTimeResponse get_time(const GetTimeRequest &msg) = 0;
|
#endif
|
||||||
|
virtual bool send_get_time_response(const GetTimeRequest &msg) = 0;
|
||||||
#ifdef USE_API_SERVICES
|
#ifdef USE_API_SERVICES
|
||||||
virtual void execute_service(const ExecuteServiceRequest &msg) = 0;
|
virtual void execute_service(const ExecuteServiceRequest &msg) = 0;
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_API_NOISE
|
#ifdef USE_API_NOISE
|
||||||
virtual NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) = 0;
|
virtual bool send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) = 0;
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_BUTTON
|
#ifdef USE_BUTTON
|
||||||
virtual void button_command(const ButtonCommandRequest &msg) = 0;
|
virtual void button_command(const ButtonCommandRequest &msg) = 0;
|
||||||
@@ -303,7 +312,7 @@ class APIServerConnection : public APIServerConnectionBase {
|
|||||||
virtual void bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) = 0;
|
virtual void bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) = 0;
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_BLUETOOTH_PROXY
|
#ifdef USE_BLUETOOTH_PROXY
|
||||||
virtual BluetoothConnectionsFreeResponse subscribe_bluetooth_connections_free(
|
virtual bool send_subscribe_bluetooth_connections_free_response(
|
||||||
const SubscribeBluetoothConnectionsFreeRequest &msg) = 0;
|
const SubscribeBluetoothConnectionsFreeRequest &msg) = 0;
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_BLUETOOTH_PROXY
|
#ifdef USE_BLUETOOTH_PROXY
|
||||||
@@ -316,8 +325,7 @@ class APIServerConnection : public APIServerConnectionBase {
|
|||||||
virtual void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) = 0;
|
virtual void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) = 0;
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_VOICE_ASSISTANT
|
#ifdef USE_VOICE_ASSISTANT
|
||||||
virtual VoiceAssistantConfigurationResponse voice_assistant_get_configuration(
|
virtual bool send_voice_assistant_get_configuration_response(const VoiceAssistantConfigurationRequest &msg) = 0;
|
||||||
const VoiceAssistantConfigurationRequest &msg) = 0;
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_VOICE_ASSISTANT
|
#ifdef USE_VOICE_ASSISTANT
|
||||||
virtual void voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) = 0;
|
virtual void voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) = 0;
|
||||||
@@ -334,8 +342,12 @@ class APIServerConnection : public APIServerConnectionBase {
|
|||||||
void on_list_entities_request(const ListEntitiesRequest &msg) override;
|
void on_list_entities_request(const ListEntitiesRequest &msg) override;
|
||||||
void on_subscribe_states_request(const SubscribeStatesRequest &msg) override;
|
void on_subscribe_states_request(const SubscribeStatesRequest &msg) override;
|
||||||
void on_subscribe_logs_request(const SubscribeLogsRequest &msg) override;
|
void on_subscribe_logs_request(const SubscribeLogsRequest &msg) override;
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||||
void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &msg) override;
|
void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &msg) override;
|
||||||
|
#endif
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||||
void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override;
|
void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override;
|
||||||
|
#endif
|
||||||
void on_get_time_request(const GetTimeRequest &msg) override;
|
void on_get_time_request(const GetTimeRequest &msg) override;
|
||||||
#ifdef USE_API_SERVICES
|
#ifdef USE_API_SERVICES
|
||||||
void on_execute_service_request(const ExecuteServiceRequest &msg) override;
|
void on_execute_service_request(const ExecuteServiceRequest &msg) override;
|
||||||
@@ -445,5 +457,4 @@ class APIServerConnection : public APIServerConnectionBase {
|
|||||||
#endif
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace api
|
} // namespace esphome::api
|
||||||
} // namespace esphome
|
|
||||||
|
|||||||
@@ -16,8 +16,7 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome::api {
|
||||||
namespace api {
|
|
||||||
|
|
||||||
static const char *const TAG = "api";
|
static const char *const TAG = "api";
|
||||||
|
|
||||||
@@ -184,9 +183,9 @@ void APIServer::loop() {
|
|||||||
|
|
||||||
// Rare case: handle disconnection
|
// Rare case: handle disconnection
|
||||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||||
this->client_disconnected_trigger_->trigger(client->client_info_, client->client_peername_);
|
this->client_disconnected_trigger_->trigger(client->client_info_.name, client->client_info_.peername);
|
||||||
#endif
|
#endif
|
||||||
ESP_LOGV(TAG, "Remove connection %s", client->client_info_.c_str());
|
ESP_LOGV(TAG, "Remove connection %s", client->client_info_.name.c_str());
|
||||||
|
|
||||||
// Swap with the last element and pop (avoids expensive vector shifts)
|
// Swap with the last element and pop (avoids expensive vector shifts)
|
||||||
if (client_index < this->clients_.size() - 1) {
|
if (client_index < this->clients_.size() - 1) {
|
||||||
@@ -370,12 +369,15 @@ void APIServer::set_password(const std::string &password) { this->password_ = pa
|
|||||||
|
|
||||||
void APIServer::set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = batch_delay; }
|
void APIServer::set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = batch_delay; }
|
||||||
|
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||||
void APIServer::send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
|
void APIServer::send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
|
||||||
for (auto &client : this->clients_) {
|
for (auto &client : this->clients_) {
|
||||||
client->send_homeassistant_service_call(call);
|
client->send_homeassistant_service_call(call);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||||
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
||||||
std::function<void(std::string)> f) {
|
std::function<void(std::string)> f) {
|
||||||
this->state_subs_.push_back(HomeAssistantStateSubscription{
|
this->state_subs_.push_back(HomeAssistantStateSubscription{
|
||||||
@@ -399,6 +401,7 @@ void APIServer::get_home_assistant_state(std::string entity_id, optional<std::st
|
|||||||
const std::vector<APIServer::HomeAssistantStateSubscription> &APIServer::get_state_subs() const {
|
const std::vector<APIServer::HomeAssistantStateSubscription> &APIServer::get_state_subs() const {
|
||||||
return this->state_subs_;
|
return this->state_subs_;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
uint16_t APIServer::get_port() const { return this->port_; }
|
uint16_t APIServer::get_port() const { return this->port_; }
|
||||||
|
|
||||||
@@ -483,6 +486,5 @@ bool APIServer::teardown() {
|
|||||||
return this->clients_.empty();
|
return this->clients_.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace api
|
} // namespace esphome::api
|
||||||
} // namespace esphome
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -18,8 +18,7 @@
|
|||||||
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome::api {
|
||||||
namespace api {
|
|
||||||
|
|
||||||
#ifdef USE_API_NOISE
|
#ifdef USE_API_NOISE
|
||||||
struct SavedNoisePsk {
|
struct SavedNoisePsk {
|
||||||
@@ -107,7 +106,9 @@ class APIServer : public Component, public Controller {
|
|||||||
#ifdef USE_MEDIA_PLAYER
|
#ifdef USE_MEDIA_PLAYER
|
||||||
void on_media_player_update(media_player::MediaPlayer *obj) override;
|
void on_media_player_update(media_player::MediaPlayer *obj) override;
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||||
void send_homeassistant_service_call(const HomeassistantServiceResponse &call);
|
void send_homeassistant_service_call(const HomeassistantServiceResponse &call);
|
||||||
|
#endif
|
||||||
#ifdef USE_API_SERVICES
|
#ifdef USE_API_SERVICES
|
||||||
void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }
|
void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }
|
||||||
#endif
|
#endif
|
||||||
@@ -127,6 +128,7 @@ class APIServer : public Component, public Controller {
|
|||||||
|
|
||||||
bool is_connected() const;
|
bool is_connected() const;
|
||||||
|
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||||
struct HomeAssistantStateSubscription {
|
struct HomeAssistantStateSubscription {
|
||||||
std::string entity_id;
|
std::string entity_id;
|
||||||
optional<std::string> attribute;
|
optional<std::string> attribute;
|
||||||
@@ -139,6 +141,7 @@ class APIServer : public Component, public Controller {
|
|||||||
void get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
void get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
||||||
std::function<void(std::string)> f);
|
std::function<void(std::string)> f);
|
||||||
const std::vector<HomeAssistantStateSubscription> &get_state_subs() const;
|
const std::vector<HomeAssistantStateSubscription> &get_state_subs() const;
|
||||||
|
#endif
|
||||||
#ifdef USE_API_SERVICES
|
#ifdef USE_API_SERVICES
|
||||||
const std::vector<UserServiceDescriptor *> &get_user_services() const { return this->user_services_; }
|
const std::vector<UserServiceDescriptor *> &get_user_services() const { return this->user_services_; }
|
||||||
#endif
|
#endif
|
||||||
@@ -172,7 +175,9 @@ class APIServer : public Component, public Controller {
|
|||||||
std::string password_;
|
std::string password_;
|
||||||
#endif
|
#endif
|
||||||
std::vector<uint8_t> shared_write_buffer_; // Shared proto write buffer for all connections
|
std::vector<uint8_t> shared_write_buffer_; // Shared proto write buffer for all connections
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||||
std::vector<HomeAssistantStateSubscription> state_subs_;
|
std::vector<HomeAssistantStateSubscription> state_subs_;
|
||||||
|
#endif
|
||||||
#ifdef USE_API_SERVICES
|
#ifdef USE_API_SERVICES
|
||||||
std::vector<UserServiceDescriptor *> user_services_;
|
std::vector<UserServiceDescriptor *> user_services_;
|
||||||
#endif
|
#endif
|
||||||
@@ -196,6 +201,5 @@ template<typename... Ts> class APIConnectedCondition : public Condition<Ts...> {
|
|||||||
bool check(Ts... x) override { return global_api_server->is_connected(); }
|
bool check(Ts... x) override { return global_api_server->is_connected(); }
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace api
|
} // namespace esphome::api
|
||||||
} // namespace esphome
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ with warnings.catch_warnings():
|
|||||||
from aioesphomeapi import APIClient, parse_log_message
|
from aioesphomeapi import APIClient, parse_log_message
|
||||||
from aioesphomeapi.log_runner import async_run
|
from aioesphomeapi.log_runner import async_run
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
|
||||||
from esphome.const import CONF_KEY, CONF_PASSWORD, CONF_PORT, __version__
|
from esphome.const import CONF_KEY, CONF_PASSWORD, CONF_PORT, __version__
|
||||||
from esphome.core import CORE
|
from esphome.core import CORE
|
||||||
|
|
||||||
@@ -66,7 +68,5 @@ async def async_run_logs(config: dict[str, Any], address: str) -> None:
|
|||||||
|
|
||||||
def run_logs(config: dict[str, Any], address: str) -> None:
|
def run_logs(config: dict[str, Any], address: str) -> None:
|
||||||
"""Run the logs command."""
|
"""Run the logs command."""
|
||||||
try:
|
with contextlib.suppress(KeyboardInterrupt):
|
||||||
asyncio.run(async_run_logs(config, address))
|
asyncio.run(async_run_logs(config, address))
|
||||||
except KeyboardInterrupt:
|
|
||||||
pass
|
|
||||||
|
|||||||
@@ -6,8 +6,7 @@
|
|||||||
#ifdef USE_API_SERVICES
|
#ifdef USE_API_SERVICES
|
||||||
#include "user_services.h"
|
#include "user_services.h"
|
||||||
#endif
|
#endif
|
||||||
namespace esphome {
|
namespace esphome::api {
|
||||||
namespace api {
|
|
||||||
|
|
||||||
#ifdef USE_API_SERVICES
|
#ifdef USE_API_SERVICES
|
||||||
template<typename T, typename... Ts> class CustomAPIDeviceService : public UserServiceBase<Ts...> {
|
template<typename T, typename... Ts> class CustomAPIDeviceService : public UserServiceBase<Ts...> {
|
||||||
@@ -84,6 +83,7 @@ class CustomAPIDevice {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||||
/** Subscribe to the state (or attribute state) of an entity from Home Assistant.
|
/** Subscribe to the state (or attribute state) of an entity from Home Assistant.
|
||||||
*
|
*
|
||||||
* Usage:
|
* Usage:
|
||||||
@@ -135,7 +135,9 @@ class CustomAPIDevice {
|
|||||||
auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1);
|
auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1);
|
||||||
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), f);
|
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), f);
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||||
/** Call a Home Assistant service from ESPHome.
|
/** Call a Home Assistant service from ESPHome.
|
||||||
*
|
*
|
||||||
* Usage:
|
* Usage:
|
||||||
@@ -148,7 +150,7 @@ class CustomAPIDevice {
|
|||||||
*/
|
*/
|
||||||
void call_homeassistant_service(const std::string &service_name) {
|
void call_homeassistant_service(const std::string &service_name) {
|
||||||
HomeassistantServiceResponse resp;
|
HomeassistantServiceResponse resp;
|
||||||
resp.service = service_name;
|
resp.set_service(StringRef(service_name));
|
||||||
global_api_server->send_homeassistant_service_call(resp);
|
global_api_server->send_homeassistant_service_call(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,12 +170,12 @@ class CustomAPIDevice {
|
|||||||
*/
|
*/
|
||||||
void call_homeassistant_service(const std::string &service_name, const std::map<std::string, std::string> &data) {
|
void call_homeassistant_service(const std::string &service_name, const std::map<std::string, std::string> &data) {
|
||||||
HomeassistantServiceResponse resp;
|
HomeassistantServiceResponse resp;
|
||||||
resp.service = service_name;
|
resp.set_service(StringRef(service_name));
|
||||||
for (auto &it : data) {
|
for (auto &it : data) {
|
||||||
HomeassistantServiceMap kv;
|
resp.data.emplace_back();
|
||||||
kv.key = it.first;
|
auto &kv = resp.data.back();
|
||||||
|
kv.set_key(StringRef(it.first));
|
||||||
kv.value = it.second;
|
kv.value = it.second;
|
||||||
resp.data.push_back(kv);
|
|
||||||
}
|
}
|
||||||
global_api_server->send_homeassistant_service_call(resp);
|
global_api_server->send_homeassistant_service_call(resp);
|
||||||
}
|
}
|
||||||
@@ -190,7 +192,7 @@ class CustomAPIDevice {
|
|||||||
*/
|
*/
|
||||||
void fire_homeassistant_event(const std::string &event_name) {
|
void fire_homeassistant_event(const std::string &event_name) {
|
||||||
HomeassistantServiceResponse resp;
|
HomeassistantServiceResponse resp;
|
||||||
resp.service = event_name;
|
resp.set_service(StringRef(event_name));
|
||||||
resp.is_event = true;
|
resp.is_event = true;
|
||||||
global_api_server->send_homeassistant_service_call(resp);
|
global_api_server->send_homeassistant_service_call(resp);
|
||||||
}
|
}
|
||||||
@@ -210,18 +212,18 @@ class CustomAPIDevice {
|
|||||||
*/
|
*/
|
||||||
void fire_homeassistant_event(const std::string &service_name, const std::map<std::string, std::string> &data) {
|
void fire_homeassistant_event(const std::string &service_name, const std::map<std::string, std::string> &data) {
|
||||||
HomeassistantServiceResponse resp;
|
HomeassistantServiceResponse resp;
|
||||||
resp.service = service_name;
|
resp.set_service(StringRef(service_name));
|
||||||
resp.is_event = true;
|
resp.is_event = true;
|
||||||
for (auto &it : data) {
|
for (auto &it : data) {
|
||||||
HomeassistantServiceMap kv;
|
resp.data.emplace_back();
|
||||||
kv.key = it.first;
|
auto &kv = resp.data.back();
|
||||||
|
kv.set_key(StringRef(it.first));
|
||||||
kv.value = it.second;
|
kv.value = it.second;
|
||||||
resp.data.push_back(kv);
|
|
||||||
}
|
}
|
||||||
global_api_server->send_homeassistant_service_call(resp);
|
global_api_server->send_homeassistant_service_call(resp);
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace api
|
} // namespace esphome::api
|
||||||
} // namespace esphome
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
|
|
||||||
#include "api_server.h"
|
#include "api_server.h"
|
||||||
#ifdef USE_API
|
#ifdef USE_API
|
||||||
|
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||||
#include "api_pb2.h"
|
#include "api_pb2.h"
|
||||||
#include "esphome/core/automation.h"
|
#include "esphome/core/automation.h"
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome::api {
|
||||||
namespace api {
|
|
||||||
|
|
||||||
template<typename... X> class TemplatableStringValue : public TemplatableValue<std::string, X...> {
|
template<typename... X> class TemplatableStringValue : public TemplatableValue<std::string, X...> {
|
||||||
private:
|
private:
|
||||||
@@ -36,6 +36,9 @@ template<typename... X> class TemplatableStringValue : public TemplatableValue<s
|
|||||||
|
|
||||||
template<typename... Ts> class TemplatableKeyValuePair {
|
template<typename... Ts> class TemplatableKeyValuePair {
|
||||||
public:
|
public:
|
||||||
|
// Keys are always string literals from YAML dictionary keys (e.g., "code", "event")
|
||||||
|
// and never templatable values or lambdas. Only the value parameter can be a lambda/template.
|
||||||
|
// Using pass-by-value with std::move allows optimal performance for both lvalues and rvalues.
|
||||||
template<typename T> TemplatableKeyValuePair(std::string key, T value) : key(std::move(key)), value(value) {}
|
template<typename T> TemplatableKeyValuePair(std::string key, T value) : key(std::move(key)), value(value) {}
|
||||||
std::string key;
|
std::string key;
|
||||||
TemplatableStringValue<Ts...> value;
|
TemplatableStringValue<Ts...> value;
|
||||||
@@ -47,37 +50,39 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
|||||||
|
|
||||||
template<typename T> void set_service(T service) { this->service_ = service; }
|
template<typename T> void set_service(T service) { this->service_ = service; }
|
||||||
|
|
||||||
template<typename T> void add_data(std::string key, T value) {
|
// Keys are always string literals from the Python code generation (e.g., cg.add(var.add_data("tag_id", templ))).
|
||||||
this->data_.push_back(TemplatableKeyValuePair<Ts...>(key, value));
|
// The value parameter can be a lambda/template, but keys are never templatable.
|
||||||
}
|
// Using pass-by-value allows the compiler to optimize for both lvalues and rvalues.
|
||||||
|
template<typename T> void add_data(std::string key, T value) { this->data_.emplace_back(std::move(key), value); }
|
||||||
template<typename T> void add_data_template(std::string key, T value) {
|
template<typename T> void add_data_template(std::string key, T value) {
|
||||||
this->data_template_.push_back(TemplatableKeyValuePair<Ts...>(key, value));
|
this->data_template_.emplace_back(std::move(key), value);
|
||||||
}
|
}
|
||||||
template<typename T> void add_variable(std::string key, T value) {
|
template<typename T> void add_variable(std::string key, T value) {
|
||||||
this->variables_.push_back(TemplatableKeyValuePair<Ts...>(key, value));
|
this->variables_.emplace_back(std::move(key), value);
|
||||||
}
|
}
|
||||||
|
|
||||||
void play(Ts... x) override {
|
void play(Ts... x) override {
|
||||||
HomeassistantServiceResponse resp;
|
HomeassistantServiceResponse resp;
|
||||||
resp.service = this->service_.value(x...);
|
std::string service_value = this->service_.value(x...);
|
||||||
|
resp.set_service(StringRef(service_value));
|
||||||
resp.is_event = this->is_event_;
|
resp.is_event = this->is_event_;
|
||||||
for (auto &it : this->data_) {
|
for (auto &it : this->data_) {
|
||||||
HomeassistantServiceMap kv;
|
resp.data.emplace_back();
|
||||||
kv.key = it.key;
|
auto &kv = resp.data.back();
|
||||||
|
kv.set_key(StringRef(it.key));
|
||||||
kv.value = it.value.value(x...);
|
kv.value = it.value.value(x...);
|
||||||
resp.data.push_back(kv);
|
|
||||||
}
|
}
|
||||||
for (auto &it : this->data_template_) {
|
for (auto &it : this->data_template_) {
|
||||||
HomeassistantServiceMap kv;
|
resp.data_template.emplace_back();
|
||||||
kv.key = it.key;
|
auto &kv = resp.data_template.back();
|
||||||
|
kv.set_key(StringRef(it.key));
|
||||||
kv.value = it.value.value(x...);
|
kv.value = it.value.value(x...);
|
||||||
resp.data_template.push_back(kv);
|
|
||||||
}
|
}
|
||||||
for (auto &it : this->variables_) {
|
for (auto &it : this->variables_) {
|
||||||
HomeassistantServiceMap kv;
|
resp.variables.emplace_back();
|
||||||
kv.key = it.key;
|
auto &kv = resp.variables.back();
|
||||||
|
kv.set_key(StringRef(it.key));
|
||||||
kv.value = it.value.value(x...);
|
kv.value = it.value.value(x...);
|
||||||
resp.variables.push_back(kv);
|
|
||||||
}
|
}
|
||||||
this->parent_->send_homeassistant_service_call(resp);
|
this->parent_->send_homeassistant_service_call(resp);
|
||||||
}
|
}
|
||||||
@@ -91,6 +96,6 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
|||||||
std::vector<TemplatableKeyValuePair<Ts...>> variables_;
|
std::vector<TemplatableKeyValuePair<Ts...>> variables_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace api
|
} // namespace esphome::api
|
||||||
} // namespace esphome
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -6,8 +6,7 @@
|
|||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
#include "esphome/core/util.h"
|
#include "esphome/core/util.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome::api {
|
||||||
namespace api {
|
|
||||||
|
|
||||||
// Generate entity handler implementations using macros
|
// Generate entity handler implementations using macros
|
||||||
#ifdef USE_BINARY_SENSOR
|
#ifdef USE_BINARY_SENSOR
|
||||||
@@ -90,6 +89,5 @@ bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
} // namespace api
|
} // namespace esphome::api
|
||||||
} // namespace esphome
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -4,8 +4,7 @@
|
|||||||
#ifdef USE_API
|
#ifdef USE_API
|
||||||
#include "esphome/core/component.h"
|
#include "esphome/core/component.h"
|
||||||
#include "esphome/core/component_iterator.h"
|
#include "esphome/core/component_iterator.h"
|
||||||
namespace esphome {
|
namespace esphome::api {
|
||||||
namespace api {
|
|
||||||
|
|
||||||
class APIConnection;
|
class APIConnection;
|
||||||
|
|
||||||
@@ -96,6 +95,5 @@ class ListEntitiesIterator : public ComponentIterator {
|
|||||||
APIConnection *client_;
|
APIConnection *client_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace api
|
} // namespace esphome::api
|
||||||
} // namespace esphome
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user